/* libink — listening socket, accept, peer-credential capture * * Copyright (c) 2026 Joachim Wiberg * SPDX-License-Identifier: MIT */ #include #include #include #include #include #include #include #include #include "internal.h" static void close_save_errno(int fd) { int saved = errno; close(fd); errno = saved; } int link_server_new(link_server_t **out, const char *path, mode_t mode) { struct sockaddr_un sun = { .sun_family = AF_UNIX }; link_server_t *srv; size_t plen; int fd; if (!out || !path || !*path) { errno = EINVAL; return -1; } plen = strlen(path); if (plen >= sizeof(sun.sun_path) || plen >= LINK_PATH_MAX) { errno = ENAMETOOLONG; return -1; } srv = calloc(1, sizeof(*srv)); if (!srv) return -1; TAILQ_INIT(&srv->objects); fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (fd < 0) goto err_free; memcpy(sun.sun_path, path, plen + 1); (void)unlink(path); /* fchmod() on a Unix-domain socket fd is a silent no-op on Linux: * the file mode is fixed at bind() time as (0777 & ~umask). Set * umask around the bind() so the socket appears with the mode the * caller asked for atomically, with no window where it is more * permissive. Who may connect is the caller's policy to set; * per-method authorization happens later in dispatch via * SO_PEERCRED. */ __dbg("binding %s, mode %04o", path, (unsigned)mode); { mode_t oldmask = umask(0777 & ~mode); int rc = bind(fd, (struct sockaddr *)&sun, sizeof(sun)); int saved = errno; umask(oldmask); if (rc < 0) { errno = saved; goto err_close; } } if (listen(fd, 16) < 0) goto err_unlink; srv->fd = fd; memcpy(srv->path, path, plen + 1); *out = srv; return 0; err_unlink: (void)unlink(path); err_close: close_save_errno(fd); err_free: free(srv); return -1; } void link_server_free(link_server_t *srv) { struct link_object *o; if (!srv) return; o = TAILQ_FIRST(&srv->objects); while (o) { struct link_object *next_o = TAILQ_NEXT(o, link); struct link_vtable_entry *e = TAILQ_FIRST(&o->vtables); while (e) { struct link_vtable_entry *next_e = TAILQ_NEXT(e, link); free(e); e = next_e; } free(o); o = next_o; } if (srv->fd >= 0) close(srv->fd); if (srv->path[0]) (void)unlink(srv->path); free(srv); } int link_server_get_fd(const link_server_t *srv) { if (!srv) return -1; return srv->fd; } int link_server_accept(link_server_t *srv, link_connection_t **out) { struct ucred cred = { 0 }; socklen_t credlen = sizeof(cred); link_connection_t *conn; int cfd; if (!srv || !out) { errno = EINVAL; return -1; } cfd = accept4(srv->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); if (cfd < 0) return -1; conn = calloc(1, sizeof(*conn)); if (!conn) { close_save_errno(cfd); return -1; } conn->fd = cfd; conn->auth = LINK_AUTH_NUL; conn->server = srv; if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &credlen) == 0) { conn->peer_uid = cred.uid; /* Capture the peer's group set from the kernel so the * authorizer never has to ask NSS (which can block PID 1). * Primary gid first, then the supplementary groups. * * SO_PEERGROUPS is all-or-nothing: too small a buffer gets * ERANGE and fills nothing, so a peer with more than fits * is captured as the primary gid alone. A member of the * owning group through a supplementary slot beyond the cap * is then denied -- it fails closed, never open. Needs * Linux 4.13; on older kernels the primary gid stands. */ conn->peer_groups[0] = cred.gid; conn->peer_ngroups = 1; #ifdef SO_PEERGROUPS { gid_t sup[LINK_PEER_GROUPS_MAX - 1]; socklen_t len = sizeof(sup); if (getsockopt(cfd, SOL_SOCKET, SO_PEERGROUPS, sup, &len) == 0) { int n = (int)(len / sizeof(sup[0])); for (int i = 0; i < n; i++) conn->peer_groups[conn->peer_ngroups++] = sup[i]; } } #endif } else { conn->peer_uid = (uid_t)-1; } __auth_generate_guid(conn->guid); __dbg("new peer on fd %d, uid %d", cfd, (int)conn->peer_uid); *out = conn; return 0; } void link_server_set_uid_resolver(link_server_t *srv, link_uid_resolver_t cb, void *userdata) { if (!srv) return; srv->uid_resolver = cb; srv->uid_userdata = userdata; } void link_server_set_authorizer(link_server_t *srv, link_authorizer_t cb, void *userdata) { if (!srv) return; srv->authorizer = cb; srv->authz_userdata = userdata; } link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid, unsigned int attach_flags) { link_connection_t *conn; int flags; /* On entry we always own `fd` -- close it on every failure path * so callers don't have to track whether we touched fcntl state. */ if (!srv || fd < 0) { if (fd >= 0) close_save_errno(fd); errno = EINVAL; return NULL; } /* Match server_accept's fd setup: CLOEXEC first (so a fork-and- * exec between the two calls cannot leak the fd), then NONBLOCK * so process_binary's read loop can drain without hanging. */ flags = fcntl(fd, F_GETFD, 0); if (flags < 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) goto err_close; flags = fcntl(fd, F_GETFL, 0); if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) goto err_close; conn = calloc(1, sizeof(*conn)); if (!conn) goto err_close; conn->fd = fd; conn->auth = LINK_AUTH_DONE; /* caller already handshook */ conn->server = srv; conn->peer_uid = peer_uid; conn->broker = !!(attach_flags & LINK_ATTACH_BROKER); __auth_generate_guid(conn->guid); __dbg("attached %s peer on fd %d, uid %d", conn->broker ? "broker" : "external", fd, (int)peer_uid); return conn; err_close: close_save_errno(fd); return NULL; }