kernel: HTTPS through wget, half-close, and /proc/<pid>/fd

busybox wget hands TLS to ssl_client over a socketpair and then does
shutdown(SHUT_WR). That cut the AF_UNIX link both ways, so ssl_client's
reply was a SIGPIPE and every https:// download hung. SHUT_WR is now a
half-close: the peer reads EOF and can still answer. TCP sockets get
shutdown() at all (tcp_shutdown for the FIN); it used to do nothing.

- exec keeps ignored signals and the signal mask, as execve(2) says.
- /proc/<pid>/fd: a link per descriptor -- the path it was opened by,
  socket:[n], pipe:[n], anon_inode:[eventpoll]. /proc/self too. The
  global inode bit moves above pid << 8; at 0x10000 it was /proc/256.
- fstat on a socket or pipe answers S_IFSOCK / S_IFIFO instead of EINVAL.
- poll, select and epoll wake as soon as a pipe, socket or input device
  changes, not on the next tick.
- Pipes are 64 KB of lazily allocated pages, copied in blocks, with
  PIPE_BUF atomicity: 300 MB through `yes | head | wc` in 7 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-24 20:28:03 +02:00
commit 8351e0323b
15 changed files with 434 additions and 71 deletions

View file

@ -574,11 +574,15 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
// first delivered signal jump to a stale address in the new image.
// mmap_next likewise belongs to the old address space; left alone it
// could hand out mmap ranges overlapping the new image's layout.
// Ignored signals stay ignored, and the mask is inherited (execve(2)):
// neither points into the old image. `nohup` and every program that ignores
// SIGPIPE before running a helper depend on it.
memset(myproc()->vma, 0, sizeof(myproc()->vma));
memset(myproc()->sig_handler, 0, sizeof(myproc()->sig_handler));
for (int i = 0; i < 32; i++)
if (myproc()->sig_handler[i] != 1)
myproc()->sig_handler[i] = 0;
memset(myproc()->sig_restorer, 0, sizeof(myproc()->sig_restorer));
myproc()->fs_base = 0;
myproc()->signal_mask = 0;
myproc()->pending_signals = 0;
myproc()->in_signal = 0;
myproc()->sigsusp_active = 0;

View file

@ -24,6 +24,7 @@ struct {
struct file *free;
struct kmem_cache *more;
uint n;
uint64 serial; // the last ino handed out
} ftable;
void
@ -63,6 +64,8 @@ filealloc(void)
ftable.n++;
}
f->ref = 1;
f->ino = ++ftable.serial;
f->path = 0;
FSSTAT_TAKE(fsstat.file_used, fsstat.file_peak); // 0 -> 1
// Cleared here rather than left to the caller: every creator sets
// readable/writable/off, but only open() sets nonblock, so a struct file
@ -101,12 +104,16 @@ fileclose(struct file *f)
lock_close_file(f);
ff = *f;
f->ref = 0;
f->path = 0;
f->type = FD_NONE;
f->fnext = ftable.free;
ftable.free = f;
FSSTAT_DROP(fsstat.file_used); // 1 -> 0
release(&ftable.lock);
if(ff.path)
kmfree(ff.path);
if(ff.type == FD_MEMFD)
shm_detach(ff.memfd_seg); // the last mapper frees the pages
else if(ff.type == FD_EVENT)
@ -129,6 +136,42 @@ fileclose(struct file *f)
}
}
// What /proc/<pid>/fd/N points at, in Linux's spelling: the path, or
// "socket:[ino]", "pipe:[ino]", "anon_inode:[eventpoll]". Returns the length.
// ftable.lock is what keeps `path` alive: fileclose() takes it from the file
// under the lock and frees it after.
int
file_describe(struct file *f, char *buf, int cap)
{
const char *pre = 0, *s = 0;
int n = 0;
acquire(&ftable.lock);
if(f->ref > 0){
switch(f->type){
case FD_INODE: s = f->path; if(!s) pre = "inode:["; break;
case FD_SOCKET: pre = "socket:["; break;
case FD_PIPE: pre = "pipe:["; break;
case FD_MEMFD: s = "/memfd: (deleted)"; break;
case FD_EVENT: s = ev_kind_name(f->ev); break;
default: break;
}
}
if(pre){
char num[24];
int k = 0;
uint64 v = f->type == FD_INODE && f->ip ? f->ip->inum : f->ino;
do { num[k++] = '0' + v % 10; v /= 10; } while(v);
while(*pre && n < cap) buf[n++] = *pre++;
while(k && n < cap) buf[n++] = num[--k];
if(n < cap) buf[n++] = ']';
} else if(s){
while(*s && n < cap) buf[n++] = *s++;
}
release(&ftable.lock);
return n;
}
int
filestat(struct file *f, struct stat *st)
{

View file

@ -11,24 +11,40 @@
#include "uapi/posix.h"
#include "uapi/errno.h"
#define PIPESIZE 512
// 64 KB, as on Linux, in pages taken the first time each is written.
//
// It was 512 bytes in the struct. That is xv6's size, and it made every pipe a
// handoff per half kilobyte: busybox's wget does TLS in a helper process that
// talks to it over pipes, a TLS record is 16 KB, and an HTTPS download that
// should have taken nine seconds was still going after a minute and a half. A
// shell pipeline that only ever moves a line still costs one page.
#define PIPESIZE 65536
#define PIPE_PAGES (PIPESIZE / PGSIZE)
#define PIPE_BUF 4096 // writes up to this size are not interleaved
struct pipe {
struct spinlock lock;
char data[PIPESIZE];
char *page[PIPE_PAGES];
uint nread; // number of bytes read
uint nwrite; // number of bytes written
int readopen; // read fd is still open
int writeopen; // write fd is still open
};
// A pipe is about 560 bytes and used to come from kalloc(), which is a whole
// 4096-byte page -- so every `|` in a shell command burned three and a half
// kilobytes it never touched. This is what kmem_cache exists for, and the
// cache is created on first use rather than at boot so a kernel that never
// opens a pipe holds no page for one.
// The struct is small and comes from a cache; the buffer is pages. The cache
// is created on first use so a kernel that never opens a pipe holds no page
// for one.
static struct kmem_cache *pipe_cache;
static void
pipe_free(struct pipe *p)
{
for(int i = 0; i < PIPE_PAGES; i++)
if(p->page[i])
kfree(p->page[i]);
kmem_cache_free(pipe_cache, p);
}
int
pipealloc(struct file **f0, struct file **f1)
{
@ -42,6 +58,7 @@ pipealloc(struct file **f0, struct file **f1)
goto bad;
if((p = kmem_cache_alloc(pipe_cache)) == 0)
goto bad;
memset(p->page, 0, sizeof(p->page));
p->readopen = 1;
p->writeopen = 1;
p->nwrite = 0;
@ -55,6 +72,7 @@ pipealloc(struct file **f0, struct file **f1)
(*f1)->readable = 0;
(*f1)->writable = 1;
(*f1)->pipe = p;
(*f1)->ino = (*f0)->ino; // one pipe, as /proc/<pid>/fd names it
return 0;
//PAGEBREAK: 20
@ -81,9 +99,10 @@ pipeclose(struct pipe *p, int writable)
}
if(p->readopen == 0 && p->writeopen == 0){
release(&p->lock);
kmem_cache_free(pipe_cache, p);
pipe_free(p);
} else
release(&p->lock);
poll_kick(); // a poller sees the hangup now, not at the tick
}
//PAGEBREAK: 40
@ -108,8 +127,12 @@ pipewrite(struct pipe *p, char *addr, int n)
return -EPIPE;
}
for(i = 0; i < n; i++){
while(p->nwrite == p->nread + PIPESIZE){ //DOC: pipewrite-full
// A write of PIPE_BUF or less goes in whole or waits for room, so two
// writers' records never interleave; a larger one goes in as room appears.
uint want_room = (n <= PIPE_BUF) ? (uint)n : 1;
i = 0;
while(i < n){
while(PIPESIZE - (p->nwrite - p->nread) < want_room){ //DOC: pipewrite-full
if(p->readopen == 0){
release(&p->lock);
sigsend(myproc()->pid, SIGPIPE);
@ -120,12 +143,32 @@ pipewrite(struct pipe *p, char *addr, int n)
return -1;
}
wakeup(&p->nread);
release(&p->lock);
poll_kick();
acquire(&p->lock);
if(PIPESIZE - (p->nwrite - p->nread) >= want_room)
break;
sleep(&p->nwrite, &p->lock); //DOC: pipewrite-sleep
}
p->data[p->nwrite++ % PIPESIZE] = addr[i];
// One run: up to the end of this page, of what is left, of the room.
uint off = p->nwrite % PIPESIZE;
uint run = PGSIZE - off % PGSIZE;
uint room = PIPESIZE - (p->nwrite - p->nread);
if(run > (uint)(n - i)) run = (uint)(n - i);
if(run > room) run = room;
char **pg = &p->page[off / PGSIZE];
if(*pg == 0 && (*pg = kalloc()) == 0){
release(&p->lock);
return i > 0 ? i : -ENOMEM;
}
memmove(*pg + off % PGSIZE, addr + i, run);
p->nwrite += run;
i += run;
want_room = 1;
}
wakeup(&p->nread); //DOC: pipewrite-wakeup1
release(&p->lock);
poll_kick();
return n;
}
@ -142,13 +185,19 @@ piperead(struct pipe *p, char *addr, int n)
}
sleep(&p->nread, &p->lock); //DOC: piperead-sleep
}
for(i = 0; i < n; i++){ //DOC: piperead-copy
if(p->nread == p->nwrite)
break;
addr[i] = p->data[p->nread++ % PIPESIZE];
i = 0;
while(i < n && p->nread != p->nwrite){ //DOC: piperead-copy
uint off = p->nread % PIPESIZE;
uint run = PGSIZE - off % PGSIZE;
if(run > (uint)(n - i)) run = (uint)(n - i);
if(run > p->nwrite - p->nread) run = p->nwrite - p->nread;
memmove(addr + i, p->page[off / PGSIZE] + off % PGSIZE, run);
p->nread += run;
i += run;
}
wakeup(&p->nwrite); //DOC: piperead-wakeup
release(&p->lock);
poll_kick(); // a writer polling for room
return i;
}
@ -168,8 +217,8 @@ pipe_poll(struct pipe *p, int readable, int writable)
if(writable){
if(p->readopen == 0)
rev |= POLLERR;
else if(p->nwrite < p->nread + PIPESIZE)
rev |= POLLOUT;
else if(PIPESIZE - (p->nwrite - p->nread) >= PIPE_BUF)
rev |= POLLOUT; // room for a whole PIPE_BUF write, as on Linux
}
release(&p->lock);
return rev;

View file

@ -43,18 +43,22 @@
// identity of the object.
//
// 1 the root, /proc
// 2 /proc/self
// PROCFS_GLOBAL | id a global file, id being the PROC_* minor that
// kernel/dev/procfs.c already uses
// pid << 8 the directory /proc/<pid>
// (pid << 8) | fileid a file inside it
// (pid << 8) | 0x80 | fd /proc/<pid>/fd/<fd>
//
// pid is bounded by NPROC and file ids by PF_MAX, so the ranges cannot
// collide. Shifting by 8 rather than packing tightly keeps the numbers
// readable in a debugger, which matters more than density here.
// File ids stay below 0x80 and fds below 128 (NOFILE). pids are not bounded
// by NPROC -- they count up -- so the global bit sits far above any pid << 8;
// at 0x10000 it was /proc/256. Shifting by 8 rather than packing tightly keeps
// the numbers readable in a debugger, which matters more than density here.
// ---------------------------------------------------------------------------
#define PROCFS_ROOT 1ULL
#define PROCFS_GLOBAL 0x10000ULL
#define PROCFS_SELF 2ULL
#define PROCFS_GLOBAL (1ULL << 40)
#define PF_DIR 0 // the directory itself
#define PF_STAT 1
@ -63,10 +67,14 @@
#define PF_STATM 4
#define PF_EXE 5
#define PF_SCHEDSTAT 6
#define PF_MAX 6
#define PF_FD 7 // the directory /proc/<pid>/fd
#define PF_MAX 7
#define PF_FDENT 0x80 // | fd: one link in it
#define INO_PID(ino) ((int)((ino) >> 8))
#define INO_FILE(ino) ((int)((ino) & 0xFF))
#define INO_IS_FDENT(ino) (!INO_IS_GLOBAL(ino) && (INO_FILE(ino) & PF_FDENT))
#define INO_FD(ino) (INO_FILE(ino) & 0x7F)
#define INO_IS_GLOBAL(ino) (((ino) & PROCFS_GLOBAL) != 0)
// The global files, by the name they appear under and the PROC_* id that
@ -121,6 +129,9 @@ static const struct global_file pidfiles[] = {
// inventing a wait time that is not measured would be worse than saying
// something true in its place.
{ "schedstat", PF_SCHEDSTAT },
// One symlink per open descriptor, named by its number and pointing at the
// path it was opened by, or at "socket:[ino]" and the like.
{ "fd", PF_FD },
};
#define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0])))
@ -267,6 +278,15 @@ gen_file(uint64 ino, char *buf, int cap)
if (INO_IS_GLOBAL(ino))
return proc_gen_global((int)(ino & 0xFF), buf, cap);
// Whoever is asking: the one file whose content depends on the reader.
if (ino == PROCFS_SELF) {
sbputi(&s, myproc()->pid);
return s.n;
}
if (INO_IS_FDENT(ino)) {
int n = proc_fd_link(INO_PID(ino), INO_FD(ino), buf, cap);
return n > 0 ? n : 0;
}
if (proc_snapshot_pid(INO_PID(ino), &pi) < 0)
return 0; // it exited between lookup and read
@ -310,7 +330,8 @@ procfs_iload(struct inode *ip)
ip->showmode = 1;
ip->atime = ip->mtime = ip->ctime = 0;
if (ino == PROCFS_ROOT || (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_DIR)) {
if (ino == PROCFS_ROOT ||
(!INO_IS_GLOBAL(ino) && (INO_FILE(ino) == PF_DIR || INO_FILE(ino) == PF_FD))) {
ip->type = T_DIR;
ip->size = 0;
ip->nlink = 2;
@ -329,8 +350,12 @@ procfs_iload(struct inode *ip)
// symlink whose size is 0 resolves to the empty string -- which is worse
// than the regular file was, because it looks like it worked. Generating it
// here costs one small render per stat() of one file.
if (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_EXE) {
char path[64];
// /proc/self and /proc/<pid>/fd/N are links too. Their targets can change
// under a cached inode, which is why readlink() does not trust this size
// for procfs.
if (ino == PROCFS_SELF || INO_IS_FDENT(ino) ||
(!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_EXE)) {
char path[256];
int len = gen_file(ino, path, sizeof(path));
ip->type = T_SYMLINK;
ip->size = len > 0 ? (uint64)len : 0;
@ -469,8 +494,19 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff)
if (namecmp(name, ".") == 0)
return iget(dp->dev, dp->inum);
if (namecmp(name, "..") == 0)
return iget(dp->dev, PROCFS_ROOT); // /proc/<pid>/.. is /proc
if (namecmp(name, "..") == 0) // /proc/<pid>/.. is /proc
return iget(dp->dev, INO_FILE(dp->inum) == PF_FD ? (dp->inum & ~0xFFULL)
: PROCFS_ROOT);
// Inside /proc/<pid>/fd/: a number that is an open descriptor.
if (dp->inum != PROCFS_ROOT && INO_FILE(dp->inum) == PF_FD) {
char tmp[4];
int fd = name_to_pid(name);
if (fd < 0 || fd >= NOFILE || fd >= 128 ||
proc_fd_link(INO_PID(dp->inum), fd, tmp, sizeof(tmp)) < 0)
return 0;
return iget(dp->dev, (dp->inum & ~0xFFULL) | PF_FDENT | fd);
}
// Inside /proc/<pid>/
if (dp->inum != PROCFS_ROOT) {
@ -480,6 +516,9 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff)
return 0;
}
if (namecmp(name, "self") == 0)
return iget(dp->dev, PROCFS_SELF);
// In /proc itself: a number is a process, a word is a global file.
pid = name_to_pid(name);
if (pid > 0) {
@ -516,6 +555,7 @@ struct linux_dirent64 {
#define DT_DIR_ 4
#define DT_REG_ 8
#define DT_LNK_ 10
static int
emit(char *out, int cap, int used, uint64 ino, int type, const char *name)
@ -574,17 +614,30 @@ procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
if (dp->inum == PROCFS_ROOT) {
EMIT(PROCFS_ROOT, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
EMIT(PROCFS_SELF, DT_LNK_, "self");
for (i = 0; i < NGLOBALS; i++)
EMIT(PROCFS_GLOBAL | globals[i].id, DT_REG_, globals[i].name);
for (i = 0; proc_snapshot(i, &pi) == 0; i++) {
utoa((uint)pi.pid, name);
EMIT(((uint64)pi.pid << 8) | PF_DIR, DT_DIR_, name);
}
} else if (INO_FILE(dp->inum) == PF_FD) {
uint64 mask[2];
EMIT(dp->inum, DT_DIR_, ".");
EMIT(dp->inum & ~0xFFULL, DT_DIR_, "..");
if (proc_fd_mask(INO_PID(dp->inum), mask) == 0)
for (i = 0; i < 128; i++)
if (mask[i / 64] & (1ULL << (i % 64))) {
utoa((uint)i, name);
EMIT((dp->inum & ~0xFFULL) | PF_FDENT | i, DT_LNK_, name);
}
} else {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
for (i = 0; i < NPIDFILES; i++)
EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id, DT_REG_, pidfiles[i].name);
EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id,
pidfiles[i].id == PF_FD ? DT_DIR_ : pidfiles[i].id == PF_EXE ? DT_LNK_ : DT_REG_,
pidfiles[i].name);
}
full: