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

@ -115,6 +115,7 @@ struct inet_sock {
int connected;
int conn_failed;
int peer_closed; // FIN seen, or the connection died
int wr_shut; // shutdown(SHUT_WR): our FIN is sent
int listening;
// Connections lwIP has completed but nobody has accept()ed yet.
@ -194,6 +195,7 @@ net_wake(void)
netwait_seq++;
release(&netwait_lock);
wakeup(&netwait_seq);
poll_kick(); // and poll/select/epoll waiters, not at the tick
}
/* ------------------------------------------------------------------ */
@ -958,7 +960,7 @@ inet_sendto(void *h, char *buf, int n, uint addr, ushort port)
uint seen;
lwip_sys_lock();
if (is->peer_closed || is->tpcb == 0) {
if (is->peer_closed || is->wr_shut || is->tpcb == 0) {
lwip_sys_unlock();
// Same contract as a pipe with no reader, which is what the AF_UNIX side
// does: the signal first, then the error.
@ -1005,6 +1007,32 @@ inet_write(void *h, char *buf, int n)
return inet_sendto(h, buf, n, 0, 0);
}
// shutdown(2) on a TCP socket. SHUT_WR sends our FIN and the peer can keep
// sending; SHUT_RD is local, reads see EOF. The pcb is never freed here --
// tcp_shutdown(pcb, 1, 1) would free it -- so close() still owns it.
int
inet_shutdown(void *h, int how)
{
struct inet_sock *is = h;
if (is->type != SOCK_STREAM)
return 0;
lwip_sys_lock();
if (is->tpcb == 0 || is->listening || !is->connected) {
lwip_sys_unlock();
return -ENOTCONN;
}
if ((how == SHUT_WR || how == SHUT_RDWR) && !is->wr_shut) {
is->wr_shut = 1;
tcp_shutdown(is->tpcb, 0, 1);
}
if (how == SHUT_RD || how == SHUT_RDWR)
is->peer_closed = 1;
lwip_sys_unlock();
net_wake();
return 0;
}
int
inet_readable(void *h)
{

View file

@ -230,6 +230,7 @@ sockclose(struct socket *s)
s->ref = 0;
wakeup(&s->nqueue); // an accept() blocked on this listener
release(&stab.lock);
poll_kick(); // the peer's poll() sees the hangup now
}
void
@ -459,6 +460,7 @@ sockread_fds(struct socket *s, char *addr, int n, struct file **fout, int *nfout
// on a full buffer that this call has just made room in.
wakeup(&s->nwrite);
release(&stab.lock);
poll_kick(); // a writer polling for room
// Outside the lock, because fileclose can sleep.
if(fout){
@ -520,7 +522,7 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf)
// look for it. Same contract as a pipe with no reader: a signal, then an
// error. A process that writes to a dead connection and is not told about
// it spins forever believing it is making progress.
if(s->peer == 0){
if(s->peer == 0 || s->wr_shut){
release(&stab.lock);
sigsend(myproc()->pid, SIGPIPE);
return -EPIPE;
@ -569,6 +571,9 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf)
wakeup(&s->peer->nread);
release(&stab.lock);
// poll() and epoll waiters, now rather than at the next tick: an X client
// and its server talk over these, a request and a reply at a time.
poll_kick();
return i;
}
@ -854,6 +859,7 @@ sockconnect(struct socket *s, uint dev, uint64 ino)
wakeup(&srv->nqueue); // an accept() may be blocked on this
release(&stab.lock);
poll_kick(); // or a listener in poll(), as Xorg's is
// Deliberately does *not* wait for accept(). On Unix a queued connection is
// already established: the client may write, and the bytes sit in the
@ -872,6 +878,9 @@ sockconnect(struct socket *s, uint dev, uint64 ino)
int
sock_shutdown(struct socket *s, int how)
{
if(s->domain == AF_INET)
return inet_shutdown(s->inet, how);
acquire(&stab.lock);
if(s->state != SS_CONNECTED){
release(&stab.lock);
@ -882,9 +891,20 @@ sock_shutdown(struct socket *s, int how)
s->peer_closed = 1;
wakeup(&s->nread);
}
if(how == SHUT_WR || how == SHUT_RDWR)
if(how == SHUT_RDWR)
orphan_peer(s);
else if(how == SHUT_WR){
// Half-close: the peer sees EOF once it has read what is there, and the
// link stays, so its reply still reaches us. Cutting the link here made
// the reply a SIGPIPE -- busybox wget does exactly this to ssl_client.
s->wr_shut = 1;
if(s->peer){
s->peer->peer_closed = 1;
wakeup(&s->peer->nread);
}
}
release(&stab.lock);
poll_kick();
return 0;
}