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

@ -113,19 +113,13 @@ clock_ns(int clock)
return sec * 1000000000ULL + usec * 1000ULL;
}
// One tick, or until a signal or kill makes waiting pointless.
// One tick, a poll_kick, or a signal or kill that makes waiting pointless:
// poll's wait, so data arriving wakes an epoll_wait as it wakes a poll.
// Returns 1 for a tick, 0 for a kick, -EINTR.
static int
ev_block(void)
{
struct proc *p = myproc();
if (p->killed || (p->pending_signals & ~p->signal_mask))
return -EINTR;
acquire(&tickslock);
uint t0 = ticks;
while (ticks == t0 && !p->killed && !(p->pending_signals & ~p->signal_mask))
sleep(&ticks, &tickslock);
release(&tickslock);
return 0;
return poll_wait_tick();
}
static int
@ -178,6 +172,19 @@ ev_arg(int n, int kind, struct file **pf)
return f->ev;
}
// The name Linux gives each kind in /proc/<pid>/fd.
const char *
ev_kind_name(struct evobj *o)
{
switch (o ? o->kind : 0) {
case EV_EVENTFD: return "anon_inode:[eventfd]";
case EV_TIMERFD: return "anon_inode:[timerfd]";
case EV_SIGNALFD: return "anon_inode:[signalfd]";
case EV_EPOLL: return "anon_inode:[eventpoll]";
}
return "anon_inode:[event]";
}
// Called by fileclose() for the last reference.
void
ev_close(struct evobj *o)
@ -714,11 +721,12 @@ epoll_wait_common(int timeout_ms)
got = ep_ready(o, buf, max);
if (got > 0 || ticks_left == 0)
break;
if (ev_block() < 0) {
int w = ev_block();
if (w < 0) {
got = -EINTR;
break;
}
if (ticks_left > 0)
if (w > 0 && ticks_left > 0)
ticks_left--;
}
if (got > 0 && copy_to_user(uevents, buf, got * sizeof(*buf)) < 0)

View file

@ -13,6 +13,7 @@
#include "uapi/posix.h"
#include "core/syscall.h"
#include "core/uaccess.h"
#include "core/kmem.h"
#include "uapi/errno.h"
#include "fs/schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */
#include "gfx/gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */
@ -139,6 +140,53 @@ fdalloc(struct file *f)
return -1;
}
// Remember the absolute path `f` was opened by, for /proc/<pid>/fd. `path` is
// joined to the directory it was resolved from -- the cwd, or dirfd's own
// remembered path -- and `.`, `..` and repeated slashes are folded out. This
// is lexical: a symlink along the way stays in the name. Nothing depends on
// it being there, so running out of memory just leaves it unset.
static void
file_setpath(struct file *f, int dirfd, const char *path)
{
const char *base = "";
if (path[0] != '/') {
struct file *d;
if (dirfd == AT_FDCWD)
base = myproc()->cwd_path;
else if (dirfd >= 0 && dirfd < NOFILE && (d = myproc()->ofile[dirfd]) && d->path)
base = d->path;
else
return;
}
char *buf = kalloc();
if (!buf) return;
int n = 0; // buf is "/a/b", no trailing slash
for (int pass = 0; pass < 2; pass++) {
const char *s = pass ? path : base;
for (;;) {
while (*s == '/') s++;
const char *e = s;
while (*e && *e != '/') e++;
int len = (int)(e - s);
if (len == 0) break;
if (len == 2 && s[0] == '.' && s[1] == '.') {
while (n > 0 && buf[--n] != '/') ;
} else if (!(len == 1 && s[0] == '.')) {
if (n + 1 + len >= PGSIZE) { kfree(buf); return; }
buf[n++] = '/';
memmove(buf + n, s, len);
n += len;
}
s = e;
}
}
if (n == 0) buf[n++] = '/';
buf[n] = 0;
if ((f->path = kmalloc(n + 1)) != 0)
memmove(f->path, buf, n + 1);
kfree(buf);
}
// Resolves a dirfd (AT_FDCWD or a real fd) to the inode a relative path
// should be resolved from. 0 on an invalid dirfd.
static struct inode *
@ -602,6 +650,7 @@ addr_t sys_open(void)
}
iunlock(ip); end_op();
f->type = FD_INODE; f->ip = ip;
file_setpath(f, AT_FDCWD, path);
f->off = (omode & O_APPEND) ? ip->size : 0;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
@ -659,6 +708,7 @@ addr_t sys_openat(void)
uint64 start = (omode & O_APPEND) ? ip->size : 0;
iunlock(ip); end_op();
f->type = FD_INODE; f->ip = ip; f->off = start;
file_setpath(f, dirfd, path);
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
f->nonblock = (omode & O_NONBLOCK) != 0;
@ -697,6 +747,19 @@ addr_t sys_fstat(void)
}
return 0;
}
// A socket, a pipe or an anonymous inode: Linux answers with its type and
// an inode number, which is what `ls -l /proc/<pid>/fd` and every program
// asking "is my stdin a socket?" look at. ino matches /proc/<pid>/fd.
if (f->type != FD_INODE && f->type != FD_MEMFD && myproc()->syscall_map) {
struct linux_stat *ls = (struct linux_stat *)st;
memset(ls, 0, sizeof(*ls));
ls->st_mode = f->type == FD_SOCKET ? LINUX_S_IFSOCK | 0777
: f->type == FD_PIPE ? LINUX_S_IFIFO | 0600 : 0600;
ls->st_ino = f->ino;
ls->st_nlink = 1;
ls->st_blksize = PGSIZE;
return 0;
}
if (f->type != FD_INODE) return -EINVAL;
ilock(f->ip);
if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st);
@ -1229,10 +1292,14 @@ addr_t sys_readlink(void)
if (!(ip = namei(path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; }
// A /proc link can change after its inode was loaded -- an fd dup2()ed
// over -- so its size is not trusted: the read says how long it is.
int proc = ip->dev == PROCFS_DEV;
int n = (proc || (int)ip->size > bufsiz) ? bufsiz : (int)ip->size;
int got = readi(ip, 0, (uint64)buf, 0, n);
if (got < 0 || (!proc && got != n)) { iunlockput(ip); return -EIO; }
iunlockput(ip);
return n;
return got;
}
// -----------------------------------------------------------------------
@ -2090,10 +2157,14 @@ addr_t sys_readlinkat(void)
if (!(ip = at_namei(dirfd, path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; }
// A /proc link can change after its inode was loaded -- an fd dup2()ed
// over -- so its size is not trusted: the read says how long it is.
int proc = ip->dev == PROCFS_DEV;
int n = (proc || (int)ip->size > bufsiz) ? bufsiz : (int)ip->size;
int got = readi(ip, 0, (uint64)buf, 0, n);
if (got < 0 || (!proc && got != n)) { iunlockput(ip); return -EIO; }
iunlockput(ip);
return n;
return got;
}
addr_t sys_fchmodat(void) { return 0; } // stub

View file

@ -656,9 +656,8 @@ sock_name(int peer)
SYSCALL_DEFINE(getsockname) { return sock_name(0); }
SYSCALL_DEFINE(getpeername) { return sock_name(1); }
// shutdown(2). Half-close is not modelled -- there is one buffer per
// direction and no per-direction flag -- so SHUT_WR and SHUT_RDWR both mean
// "the peer sees end-of-file", which is the behaviour programs depend on.
// shutdown(2). SHUT_WR is a half-close: the peer reads EOF and can still
// write back (see sock_shutdown).
SYSCALL_DEFINE(sockshutdown)
{
struct socket *s;

View file

@ -24,22 +24,47 @@
#define POLL_MAX_FDS 64
// Sleep one timer tick, or until killed/signaled. Returns 0 to keep
// waiting, -EINTR if the wait must be abandoned.
static int
poll_block_one_tick(void)
// Bumped, under tickslock, by anything that can make a descriptor ready
// sooner than the next tick: see poll_kick().
static uint poll_seq;
// Something a poller may be waiting for has happened. The wait below sleeps
// on the tick channel, so this wakes it now rather than at the tick.
//
// Without it every poll() that found nothing slept out the rest of a 10 ms
// tick whatever arrived in the meantime. busybox's wget polls before each
// read, and downloaded at 85 KB/s -- a segment per tick -- where apk, which
// blocks in read() and is woken by the data, managed 5.8 MB/s over the same
// connection. Called from the network stack's wakeup; anything else that
// makes a file readable can call it too, and until it does it is no worse
// than it was.
void
poll_kick(void)
{
if (myproc()->killed)
return -EINTR;
if (myproc()->pending_signals & ~myproc()->signal_mask)
acquire(&tickslock);
poll_seq++;
release(&tickslock);
wakeup(&ticks);
}
// Sleep one timer tick, or until a poll_kick, or until killed/signaled.
// Returns 1 if a tick went by, 0 if it was a kick -- which must not count
// against a timeout, or a timed poll on a busy network would end early --
// and -EINTR if the wait must be abandoned. epoll waits with it too.
int
poll_wait_tick(void)
{
struct proc *p = myproc();
if (p->killed || (p->pending_signals & ~p->signal_mask))
return -EINTR;
acquire(&tickslock);
uint t0 = ticks;
while (ticks == t0 && !myproc()->killed &&
!(myproc()->pending_signals & ~myproc()->signal_mask))
uint t0 = ticks, s0 = poll_seq;
while (ticks == t0 && poll_seq == s0 && !p->killed &&
!(p->pending_signals & ~p->signal_mask))
sleep(&ticks, &tickslock);
int ticked = ticks != t0;
release(&tickslock);
return 0;
return ticked;
}
// Shared core: fds[] already in kernel memory. timeout_ticks < 0 means
@ -67,9 +92,10 @@ do_poll(struct pollfd *fds, int nfds, int timeout_ticks)
}
if (nready > 0 || timeout_ticks == 0)
return nready;
if (poll_block_one_tick() < 0)
int w = poll_wait_tick();
if (w < 0)
return -EINTR;
if (timeout_ticks > 0)
if (w > 0 && timeout_ticks > 0)
timeout_ticks--;
}
}
@ -215,9 +241,10 @@ sys_select(void)
}
return nready;
}
if (poll_block_one_tick() < 0)
int w = poll_wait_tick();
if (w < 0)
return -EINTR;
if (timeout_ticks > 0)
if (w > 0 && timeout_ticks > 0)
timeout_ticks--;
}
}
@ -287,9 +314,10 @@ sys_pselect6(void)
}
return nready;
}
if (poll_block_one_tick() < 0)
int w = poll_wait_tick();
if (w < 0)
return -EINTR;
if (timeout_ticks > 0)
if (w > 0 && timeout_ticks > 0)
timeout_ticks--;
}
}