Netlink datagrams can be measured, credentials name their sender
Epiphany's main thread spun at 100% on GLib's network monitor. GLib sizes each netlink datagram with recvmsg(MSG_PEEK|MSG_TRUNC) and no buffer; that answered 0, GLib read 0 bytes, the answer stayed queued and poll() said readable again, forever. recvmsg on netlink now peeks without consuming and with MSG_TRUNC returns what is waiting. A drained netlink socket also no longer reports hangup -- it has no peer, and sock_hup took that for a closed one. SCM_CREDENTIALS under SO_PASSCRED named the process that created the far end, not the one that wrote to it. For a socketpair those differ: WebKit makes the pair, hands one end to a child, and learns the child's pid from the credentials on its first byte. Each end now records its last writer. LDLS: napi_consume_skb frees the skb. r8169 frees every sent frame with it, and as a stub it leaked one per frame until the DMA heap ran out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
4ad49ed5f2
commit
3676036762
5 changed files with 120 additions and 2 deletions
|
|
@ -533,6 +533,10 @@ int sockname_inet(struct socket*, int peer, uint *addr, ushort *port
|
|||
#define SOCK_ABSTRACT_DEV 0xFFFFFFFFu // no filesystem has it; see unix.c
|
||||
int sockreadable(struct socket*);
|
||||
int sock_hup(struct socket*);
|
||||
uint socknl_pending(struct socket*);
|
||||
int sock_sendercred(struct socket*, int*, uint*, uint*);
|
||||
int socknl_copy(struct socket*, uint, char*, int);
|
||||
void socknl_take(struct socket*, uint);
|
||||
int sock_shutdown(struct socket*, int how);
|
||||
int sockpair(struct socket **a, struct socket **b);
|
||||
const char* sockname(struct socket*);
|
||||
|
|
|
|||
|
|
@ -362,6 +362,13 @@ struct socket {
|
|||
// the listener -- which is what Linux records too.
|
||||
int cred_pid;
|
||||
uint cred_uid, cred_gid;
|
||||
|
||||
// Who last wrote at this end, for SCM_CREDENTIALS on the other. Not the
|
||||
// same thing as cred_*: one end of a socketpair is made by the parent and
|
||||
// written by the child it is handed to, and WebKit learns its children's
|
||||
// pids from exactly that. 0 until the first write.
|
||||
int tx_pid;
|
||||
uint tx_uid, tx_gid;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -509,6 +509,12 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf)
|
|||
if(s->domain == AF_INET)
|
||||
return inet_write(s->inet, addr, n);
|
||||
|
||||
if(myproc()){
|
||||
s->tx_pid = myproc()->pid;
|
||||
s->tx_uid = myproc()->euid;
|
||||
s->tx_gid = myproc()->egid;
|
||||
}
|
||||
|
||||
// A netlink request is answered immediately, into this socket's own buffer
|
||||
// rather than a peer's -- there is no peer. Doing the work here is what lets
|
||||
// the read that follows never block, which matters because musl reads with
|
||||
|
|
@ -922,6 +928,43 @@ sock_shutdown(struct socket *s, int how)
|
|||
// Readiness, for poll/select
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Netlink, for recvmsg's MSG_PEEK and MSG_TRUNC: how much is waiting, a copy
|
||||
// of it from `off` on without taking it, and taking it. Everything waiting
|
||||
// counts as one datagram -- it is the answer to the requests sent so far, and
|
||||
// a reader walks the nlmsghdrs inside it whatever their number.
|
||||
uint
|
||||
socknl_pending(struct socket *s)
|
||||
{
|
||||
acquire(&stab.lock);
|
||||
uint have = s->nwrite - s->nread;
|
||||
release(&stab.lock);
|
||||
return have;
|
||||
}
|
||||
|
||||
int
|
||||
socknl_copy(struct socket *s, uint off, char *addr, int n)
|
||||
{
|
||||
acquire(&stab.lock);
|
||||
uint have = s->nwrite - s->nread;
|
||||
if(off >= have)
|
||||
n = 0;
|
||||
else if((uint)n > have - off)
|
||||
n = (int)(have - off);
|
||||
memmove(addr, s->data + s->nread + off, (uint)n);
|
||||
release(&stab.lock);
|
||||
return n;
|
||||
}
|
||||
|
||||
void
|
||||
socknl_take(struct socket *s, uint n)
|
||||
{
|
||||
acquire(&stab.lock);
|
||||
if(n > s->nwrite - s->nread)
|
||||
n = s->nwrite - s->nread;
|
||||
s->nread += n;
|
||||
release(&stab.lock);
|
||||
}
|
||||
|
||||
int
|
||||
sockreadable(struct socket *s)
|
||||
{
|
||||
|
|
@ -933,11 +976,17 @@ sockreadable(struct socket *s)
|
|||
}
|
||||
|
||||
// The peer is gone and everything it sent has been read.
|
||||
//
|
||||
// A netlink socket's far end is the kernel, which never hangs up. It has no
|
||||
// `peer` either, and once drained it used to read as hung up here -- so every
|
||||
// poll() on GLib's network-monitor socket returned at once, forever.
|
||||
int
|
||||
sock_hup(struct socket *s)
|
||||
{
|
||||
if(s->domain == AF_INET)
|
||||
return inet_hup(s->inet);
|
||||
if(s->domain == AF_NETLINK)
|
||||
return 0;
|
||||
|
||||
if(s->state == SS_LISTENING)
|
||||
return 0;
|
||||
|
|
@ -1033,6 +1082,27 @@ sock_passcred(struct socket *s)
|
|||
return s->domain != AF_INET && s->o_passcred;
|
||||
}
|
||||
|
||||
// SCM_CREDENTIALS for SO_PASSCRED: whoever last wrote at the far end, or the
|
||||
// far end's SO_PEERCRED if nobody has. Per socket, not per message -- a
|
||||
// receiver sees the writer of the latest bytes, which for the one-writer
|
||||
// sockets that ask (WebKit's pid handshake, D-Bus) is the writer.
|
||||
int
|
||||
sock_sendercred(struct socket *s, int *pid, uint *uid, uint *gid)
|
||||
{
|
||||
int r = -1;
|
||||
|
||||
acquire(&stab.lock);
|
||||
if(s->domain != AF_INET && s->peer){
|
||||
struct socket *p = s->peer;
|
||||
*pid = p->tx_pid ? p->tx_pid : p->cred_pid;
|
||||
*uid = p->tx_pid ? p->tx_uid : p->cred_uid;
|
||||
*gid = p->tx_pid ? p->tx_gid : p->cred_gid;
|
||||
r = 0;
|
||||
}
|
||||
release(&stab.lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
// SO_PEERCRED: the credentials recorded for the far end of a connected Unix
|
||||
// socket. -1 if there is no far end.
|
||||
int
|
||||
|
|
|
|||
|
|
@ -1066,6 +1066,38 @@ SYSCALL_DEFINE(recvmsg)
|
|||
if ((file->nonblock || (flags & MSG_DONTWAIT)) && !sockreadable(s))
|
||||
return -EAGAIN;
|
||||
|
||||
// Netlink is datagrams, and its readers size them before reading:
|
||||
// GLib's network monitor asks recvmsg(MSG_PEEK|MSG_TRUNC) with no buffer
|
||||
// at all and expects the datagram's length back. Answered 0, it read 0,
|
||||
// the answer stayed queued, poll said readable, and Epiphany's main
|
||||
// thread spun on that forever.
|
||||
//
|
||||
// A plain read shorter than what is waiting keeps the rest for the next
|
||||
// one rather than discarding it as Linux would: musl's getifaddrs reads
|
||||
// in chunks, and this whole queue is one "datagram" only by convention.
|
||||
int nlret = -1;
|
||||
if (sock_domain(s) == AF_NETLINK) {
|
||||
uint have = socknl_pending(s);
|
||||
if (have == 0)
|
||||
return -EAGAIN; // never blocks; see sockread
|
||||
for (i = 0; i < m.msg_iovlen; i++) {
|
||||
char *p;
|
||||
int len, rd;
|
||||
if ((len = fetch_iov(&m, i, &p)) < 0)
|
||||
return len;
|
||||
rd = socknl_copy(s, (uint)got, p, len);
|
||||
got += rd;
|
||||
if (rd < len)
|
||||
break;
|
||||
}
|
||||
if (!(flags & MSG_PEEK))
|
||||
socknl_take(s, (uint)got);
|
||||
if ((uint)got < have && (flags & MSG_PEEK))
|
||||
mflags |= MSG_TRUNC;
|
||||
nlret = (flags & MSG_TRUNC) ? (int)have : (int)got;
|
||||
m.msg_iovlen = 0; // the loop below has nothing left
|
||||
}
|
||||
|
||||
for (i = 0; i < m.msg_iovlen; i++) {
|
||||
char *p;
|
||||
int len, rd;
|
||||
|
|
@ -1162,7 +1194,7 @@ SYSCALL_DEFINE(recvmsg)
|
|||
if (sock_passcred(s) && got > 0) {
|
||||
struct { int pid; uint uid, gid; } cr;
|
||||
uint64 off = clen ? CMSG_SPACE(clen - CMSG_LEN(0)) : 0;
|
||||
if (sock_peercred(s, &cr.pid, &cr.uid, &cr.gid) == 0) {
|
||||
if (sock_sendercred(s, &cr.pid, &cr.uid, &cr.gid) == 0) {
|
||||
if (m.msg_control && off + CMSG_LEN(sizeof(cr)) <= m.msg_controllen) {
|
||||
struct cmsghdr c;
|
||||
char tmp[CMSG_SPACE(sizeof(cr))];
|
||||
|
|
@ -1220,5 +1252,5 @@ SYSCALL_DEFINE(recvmsg)
|
|||
if (copy_to_user(umsg + __builtin_offsetof(struct msghdr, msg_flags),
|
||||
&mflags, sizeof(mflags)) < 0)
|
||||
return -EFAULT;
|
||||
return (int)got;
|
||||
return nlret >= 0 ? nlret : (int)got;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -839,6 +839,7 @@ static void *kpi_leds_list[2] = { kpi_leds_list, kpi_leds_list };
|
|||
|
||||
extern int kpi_queue_delayed_work_on(int cpu, void *q, void *dwork, unsigned long delay);
|
||||
extern int kpi_cancel_work_sync(void *work);
|
||||
extern void kpi_kfree_skb(void *skb);
|
||||
|
||||
const struct kpi_sym kpi_base_table[] = {
|
||||
// printing, formatted
|
||||
|
|
@ -945,6 +946,10 @@ const struct kpi_sym kpi_base_table[] = {
|
|||
{ "crc32_le", (void *)kpi_crc32_le },
|
||||
{ "get_random_bytes", (void *)kpi_get_random_bytes },
|
||||
{ "__skb_pad", (void *)kpi_skb_pad },
|
||||
// r8169 frees every transmitted skb this way; a stub leaked one per frame
|
||||
// until the DMA heap ran out. The budget argument changes only which cache
|
||||
// Linux returns it to.
|
||||
{ "napi_consume_skb", (void *)kpi_kfree_skb },
|
||||
|
||||
// data
|
||||
{ "link_mode_params", (void *)kpi_link_mode_params },
|
||||
|
|
|
|||
Loading…
Reference in a new issue