AF_UNIX SEQPACKET/DGRAM, writable /dev overlay, uevent netlink socket; eudev packages

udevd now starts, reads its rules and listens. Still to come: the sysfs
device tree and writable uevent files so a trigger has something to replay.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
Nexoniarz 2026-10-26 09:51:41 +01:00
commit a9f8cb7e0f
7 changed files with 457 additions and 40 deletions

View file

@ -113,6 +113,62 @@ sockinit(void)
"the socket table, with buffered bytes and descriptors in flight");
}
// SOCK_SEQPACKET and SOCK_DGRAM keep message boundaries. They ride in the same
// ring as a stream, each message behind a four-byte length, so a write is one
// message and a read takes exactly one -- the rest of a message that does not
// fit the buffer is dropped, as Linux drops it. A message is never split, so
// one has to fit the ring whole.
static int
is_packet(struct socket *s)
{
if(s->domain == AF_NETLINK)
return s->nlproto == NETLINK_KOBJECT_UEVENT;
return s->domain == AF_UNIX && (s->type == SOCK_DGRAM || s->type == SOCK_SEQPACKET);
}
int
sock_is_packet(struct socket *s)
{
return is_packet(s);
}
// bind(2) on a netlink socket: only the multicast groups mean anything here.
int
sock_netlink_bind(struct socket *s, uint groups)
{
acquire(&stab.lock);
s->nlgroups = groups;
release(&stab.lock);
return 0;
}
// Queue one uevent datagram on every uevent socket listening to any of
// `groups`, except `from`. A full queue drops the message, as Linux does:
// udev recovers with a trigger, and the sender must never wait on a reader.
void
uevent_send(uint groups, const char *msg, int len, struct socket *from)
{
struct socket *s;
if(len <= 0 || len + 4 > SOCKBUF)
return;
acquire(&stab.lock);
for(s = stab.sock; s < stab.sock + NSOCK; s++){
if(s->state == SS_FREE || s == from || s->domain != AF_NETLINK ||
s->nlproto != NETLINK_KOBJECT_UEVENT || !(s->nlgroups & groups))
continue;
if(s->nwrite - s->nread + (uint)len + 4 > SOCKBUF)
continue;
for(int i = 0; i < 4; i++)
s->data[s->nwrite++ % SOCKBUF] = (char)((uint)len >> (8 * i));
for(int i = 0; i < len; i++)
s->data[s->nwrite++ % SOCKBUF] = msg[i];
wakeup(&s->nread);
}
release(&stab.lock);
poll_kick();
}
// Caller holds stab.lock.
static struct socket *
alloc_locked(int type)
@ -415,7 +471,7 @@ sockread_fds(struct socket *s, char *addr, int n, struct file **fout, int *nfout
// so an empty buffer means the caller has already drained this dump rather
// than that more is coming. Blocking here would hang a getifaddrs() that
// asked one question too many.
if(s->domain == AF_NETLINK){
if(s->domain == AF_NETLINK && !is_packet(s)){
acquire(&stab.lock);
uint have = s->nwrite - s->nread;
if(have == 0){
@ -439,6 +495,39 @@ sockread_fds(struct socket *s, char *addr, int n, struct file **fout, int *nfout
sleep(&s->nread, &stab.lock);
}
// One message, whole or truncated; descriptors ride with the header.
if(is_packet(s) && s->nread != s->nwrite){
uint len = 0;
for(i = 0; i < 4; i++)
len |= (uint)(uchar)s->data[(s->nread + i) % SOCKBUF] << (8 * i);
if(s->nscm > 0 && s->scm[0].pos == s->nread){
got = s->scm[0].n;
for(i = 0; i < got; i++)
taken[i] = s->scm[0].f[i];
for(i = 1; i < s->nscm; i++)
s->scm[i - 1] = s->scm[i];
s->nscm--;
}
s->nread += 4;
uint take = len < (uint)n ? len : (uint)n;
for(i = 0; i < (int)take; i++)
addr[i] = s->data[(s->nread + i) % SOCKBUF];
s->nread += len;
wakeup(&s->nwrite);
release(&stab.lock);
poll_kick();
if(fout){
for(int j = 0; j < got; j++)
fout[j] = taken[j];
if(nfout)
*nfout = got;
} else {
for(int j = 0; j < got; j++)
fileclose(taken[j]);
}
return (int)take;
}
// A message carrying descriptors is a boundary. This read either starts
// exactly on one -- and takes them -- or is cut short so that it stops
// before it, leaving the next read to start there.
@ -519,6 +608,11 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf)
// 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
// MSG_DONTWAIT and treats a short read as the end of the dump.
if(s->domain == AF_NETLINK && is_packet(s)){
// udevd announces what it did on group 2; the sender does not hear itself.
uevent_send(UEVENT_GROUP_UDEV, addr, n, s);
return n;
}
if(s->domain == AF_NETLINK){
acquire(&stab.lock);
s->nread = s->nwrite = 0;
@ -548,6 +642,48 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf)
return -ENOBUFS;
}
if(is_packet(s)){
if(n + 4 > SOCKBUF){
release(&stab.lock);
return -EMSGSIZE;
}
for(;;){
dst = s->peer;
if(dst == 0){
release(&stab.lock);
sigsend(myproc()->pid, SIGPIPE);
return -EPIPE;
}
if(dst->nwrite - dst->nread + (uint)n + 4 <= SOCKBUF)
break;
if(myproc()->killed){
release(&stab.lock);
return -EINTR;
}
wakeup(&dst->nread);
sleep(&dst->nwrite, &stab.lock);
}
if(nf > 0){
if(dst->nscm >= NSCM){
release(&stab.lock);
return -ENOBUFS;
}
struct scm_fds *b = &dst->scm[dst->nscm++];
b->pos = dst->nwrite; // the header, where a packet read starts
b->n = nf;
for(int j = 0; j < nf; j++)
b->f[j] = f[j];
}
for(i = 0; i < 4; i++)
dst->data[dst->nwrite++ % SOCKBUF] = (char)((uint)n >> (8 * i));
for(i = 0; i < n; i++)
dst->data[dst->nwrite++ % SOCKBUF] = addr[i];
wakeup(&dst->nread);
release(&stab.lock);
poll_kick();
return n;
}
for(i = 0; i < n; i++){
// Re-read every iteration: the peer can go away while we sleep below.
dst = s->peer;
@ -607,9 +743,11 @@ socknew(int domain, int type, int proto)
if(domain != AF_UNIX && domain != AF_INET && domain != AF_NETLINK)
return 0;
// AF_UNIX is stream-only here; AF_INET has datagrams too, and inet_new is
// AF_UNIX: streams, and message-preserving SEQPACKET and DGRAM (connected
// pairs and listeners; an unconnected DGRAM has no address to send to and
// will be refused when it tries). AF_INET has datagrams too, and inet_new is
// what decides whether it can serve the type asked for.
if(domain == AF_UNIX && type != SOCK_STREAM)
if(domain == AF_UNIX && type != SOCK_STREAM && type != SOCK_DGRAM && type != SOCK_SEQPACKET)
return 0;
// AF_NETLINK carries whole messages and musl asks for SOCK_RAW; SOCK_DGRAM is
// accepted too because Linux does. NETLINK_ROUTE is the only protocol served
@ -618,14 +756,16 @@ socknew(int domain, int type, int proto)
if(domain == AF_NETLINK){
if(type != SOCK_RAW && type != SOCK_DGRAM)
return 0;
if(proto != NETLINK_ROUTE)
if(proto != NETLINK_ROUTE && proto != NETLINK_KOBJECT_UEVENT)
return 0;
}
acquire(&stab.lock);
s = alloc_locked(type);
if(s)
if(s){
s->domain = domain;
s->nlproto = domain == AF_NETLINK ? proto : 0;
}
release(&stab.lock);
if(s == 0)
return 0;
@ -644,16 +784,16 @@ socknew(int domain, int type, int proto)
// A connected pair with no name and no listener, for socketpair(2).
int
sockpair(struct socket **pa, struct socket **pb)
sockpair_type(struct socket **pa, struct socket **pb, int type)
{
struct socket *a, *b;
acquire(&stab.lock);
if((a = alloc_locked(SOCK_STREAM)) == 0){
if((a = alloc_locked(type)) == 0){
release(&stab.lock);
return -ENFILE;
}
if((b = alloc_locked(SOCK_STREAM)) == 0){
if((b = alloc_locked(type)) == 0){
a->state = SS_FREE;
a->ref = 0;
release(&stab.lock);
@ -674,6 +814,12 @@ sockpair(struct socket **pa, struct socket **pb)
return 0;
}
int
sockpair(struct socket **pa, struct socket **pb)
{
return sockpair_type(pa, pb, SOCK_STREAM);
}
// bind(2) for AF_UNIX. The caller has already created the filesystem node and
// hands over its identity; see sys_bind() in kernel/sys/sys_net.c for why the
// creation lives there and not here.