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:
parent
906318f2b6
commit
8351e0323b
15 changed files with 434 additions and 71 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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--;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue