Files
DroidDeck/tools/linuxfs/preload/udevmon.c
Kurt Himebauchanduubooster-team 6a5722c6d1 fix(preload): answer NETLINK_ROUTE queries inside a session (#2141)
* fix(preload): answer NETLINK_ROUTE queries inside a session

ip(8) could not run in a session: the app's SELinux domain has no
netlink_route_socket bind permission, so iproute2 failed at bind and never saw
the link that getifaddrs() and /proc/net already expose.

Serve NETLINK_ROUTE from the same etc/droiddeck-net snapshot: an AF_UNIX
socketpair backs the netlink fd, link/addr/route dumps and route get are
synthesised, writes return EPERM, and multicast subscriptions keep failing
with EACCES so consumers that wait for link events keep their fallback.

Replies carry the port id reported by getsockname() and recvmsg() rewrites
the source address to AF_NETLINK/pid 0. iproute2's dump loop requires both
before it accepts a message or NLMSG_DONE; when either is missing every
message is skipped silently and the process blocks in recvmsg().

* fix(preload): keep socket() and send() semantics for emulated netlink

* fix(preload): deliver netlink replies with the caller's blocking mode

* fix(preload): preserve NETLINK_ROUTE protocol and fd semantics

- Send ACKs for successful queries and honor NETLINK_CAP_ACK
- Reject unsupported subscriptions and apply socket timeouts
- Track socket identity and shared references across dup and close
- Support kernel connect and rewrite recvfrom source addresses
- Resolve local and connected routes before default gateways
- Add netlink and iproute2 regression tests

* fix(preload): handle netlink request limits and socket lifecycle

- Collect large sendmsg requests without falling back to UNIX sockets
- Reject oversized replies explicitly instead of returning partial results
- Allocate unique port IDs and reject duplicate bindings
- Honor socket flags and restore inherited sockets across exec
- Validate socket types and report netlink socket identity
- Replace the fixed socket table with a dynamic registry
- Add regression tests for batching, port conflicts and exec inheritance

* fix(preload): optimize netlink tracking and support link name queries

- Add lock-free fd tracking to bypass netlink locks on ordinary sockets
- Move descriptor restoration to initialization and explicit discovery paths
- Support RTM_GETLINK lookups via IFLA_IFNAME with correct error codes
- Add regression coverage for hot-path overhead and name-based queries

* fix(preload): accept unterminated netlink interface names

- Copy IFLA_IFNAME into a bounded, NUL-terminated buffer before lookup
- Match kernel string length validation and ERANGE errors
- Add regression coverage for unterminated names and boundary cases

* fix(preload): pin peers for netlink replies

* fix(preload): make autobind idempotent

* fix(preload): isolate forked netlink cleanup

* fix(preload): avoid interposed fd scans

* perf(preload): skip PID checks for untracked fds

* fix(preload): verify peer before replay

---------

Co-authored-by: uubooster-team <uu.booster@service.netease.com>
2026-11-05 18:27:08 -05:00

311 lines
10 KiB
C

/*
* Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/netlink.h>
#include <pthread.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <endian.h>
#include <stdio.h>
#include <unistd.h>
struct stand_in {
int fd;
int peer;
ino_t inode;
};
static struct stand_in stand_ins[16];
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static void lock_before_fork(void) { pthread_mutex_lock(&lock); }
static void unlock_after_fork(void) { pthread_mutex_unlock(&lock); }
static void reset_after_fork(void) { pthread_mutex_init(&lock, NULL); }
__attribute__((constructor)) static void init_udevmon(void) {
for (size_t i = 0; i < sizeof(stand_ins) / sizeof(stand_ins[0]); i++)
stand_ins[i].fd = -1;
pthread_atfork(lock_before_fork, unlock_after_fork, reset_after_fork);
}
static ino_t socket_inode(int fd) {
struct stat st;
return fstat(fd, &st) == 0 ? st.st_ino : 0;
}
static int is_stand_in(int fd) {
int found = 0;
if (fd < 0)
return 0;
pthread_mutex_lock(&lock);
for (size_t i = 0; i < sizeof(stand_ins) / sizeof(stand_ins[0]); i++) {
if (stand_ins[i].fd != fd)
continue;
if (stand_ins[i].inode == socket_inode(fd)) {
found = 1;
} else {
close(stand_ins[i].peer);
stand_ins[i].fd = -1;
}
break;
}
pthread_mutex_unlock(&lock);
return found;
}
static int is_wine_process(void) {
static int known;
if (known == 0) {
char exe[PATH_MAX];
ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
const char *name = exe;
if (n > 0) {
exe[n] = '\0';
name = strrchr(exe, '/');
name = name != NULL ? name + 1 : exe;
}
known = n > 0 && strncmp(name, "wine", 4) == 0 ? 1 : -1;
}
return known == 1;
}
static int make_stand_in(int type) {
int sv[2];
int saved = errno;
int flags = SOCK_DGRAM | (type & (SOCK_CLOEXEC | SOCK_NONBLOCK));
if (socketpair(AF_UNIX, flags, 0, sv) != 0) {
errno = saved;
return -1;
}
fcntl(sv[1], F_SETFD, FD_CLOEXEC);
pthread_mutex_lock(&lock);
for (size_t i = 0; i < sizeof(stand_ins) / sizeof(stand_ins[0]); i++) {
if (stand_ins[i].fd == -1) {
stand_ins[i].fd = sv[0];
stand_ins[i].peer = sv[1];
stand_ins[i].inode = socket_inode(sv[0]);
pthread_mutex_unlock(&lock);
return sv[0];
}
}
pthread_mutex_unlock(&lock);
close(sv[0]);
close(sv[1]);
errno = saved;
return -1;
}
__attribute__((visibility("hidden"))) int bl_nl_socket(int type, int *fd);
__attribute__((visibility("hidden"))) int bl_nl_owns(int fd);
__attribute__((visibility("hidden"))) int bl_nl_getsockname(int fd, struct sockaddr *addr,
socklen_t *len, int *rc);
__attribute__((visibility("hidden"))) int bl_nl_getpeername(int fd, struct sockaddr *addr,
socklen_t *len, int *rc);
__attribute__((visibility("hidden"))) int bl_nl_setsockopt(int fd, int level, int option,
const void *value, socklen_t len,
int (*real)(int, int, int, const void *, socklen_t),
int *rc);
__attribute__((visibility("hidden"))) int bl_nl_getsockopt(int fd, int level, int option,
void *value, socklen_t *len,
int (*real)(int, int, int, void *, socklen_t *),
int *rc);
int socket(int domain, int type, int protocol) {
static int (*real_socket)(int, int, int);
int fd;
if (real_socket == NULL) real_socket = dlsym(RTLD_NEXT, "socket");
if (real_socket == NULL) {
errno = ENOSYS;
return -1;
}
if (domain == AF_NETLINK && protocol == NETLINK_ROUTE) {
int route_fd;
bl_nl_socket(type, &route_fd);
return route_fd;
}
fd = real_socket(domain, type, protocol);
if (fd >= 0 || domain != AF_NETLINK || protocol != NETLINK_KOBJECT_UEVENT)
return fd;
int refused = errno;
if (is_wine_process()) {
errno = refused;
return -1;
}
return make_stand_in(type);
}
__attribute__((visibility("hidden"))) int bl_udevmon_stand_in(int fd) {
return is_stand_in(fd);
}
int getsockname(int fd, struct sockaddr *addr, socklen_t *len) {
static int (*real_getsockname)(int, struct sockaddr *, socklen_t *);
struct sockaddr_nl nl;
int route_rc;
if (bl_nl_getsockname(fd, addr, len, &route_rc)) return route_rc;
if (is_stand_in(fd)) {
if (addr == NULL || len == NULL) {
errno = EFAULT;
return -1;
}
memset(&nl, 0, sizeof(nl));
nl.nl_family = AF_NETLINK;
nl.nl_pid = (unsigned)getpid();
memcpy(addr, &nl, *len < sizeof(nl) ? *len : sizeof(nl));
*len = sizeof(nl);
return 0;
}
if (real_getsockname == NULL) real_getsockname = dlsym(RTLD_NEXT, "getsockname");
if (real_getsockname == NULL) {
errno = ENOSYS;
return -1;
}
return real_getsockname(fd, addr, len);
}
int getpeername(int fd, struct sockaddr *addr, socklen_t *len) {
static int (*real)(int, struct sockaddr *, socklen_t *);
int route_rc;
if (bl_nl_getpeername(fd, addr, len, &route_rc)) return route_rc;
if (real == NULL) real = dlsym(RTLD_NEXT, "getpeername");
return real(fd, addr, len);
}
int setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen) {
static int (*real_setsockopt)(int, int, int, const void *, socklen_t);
int route_rc;
if (real_setsockopt == NULL) real_setsockopt = dlsym(RTLD_NEXT, "setsockopt");
if (real_setsockopt == NULL) {
errno = ENOSYS;
return -1;
}
if (bl_nl_setsockopt(fd, level, optname, optval, optlen, real_setsockopt, &route_rc)) return route_rc;
if (is_stand_in(fd)) {
if (level == SOL_NETLINK)
return 0;
if (level == SOL_SOCKET && (optname == SO_ATTACH_FILTER || optname == SO_DETACH_FILTER))
return 0;
}
return real_setsockopt(fd, level, optname, optval, optlen);
}
int getsockopt(int fd, int level, int option, void *value, socklen_t *len) {
static int (*real)(int, int, int, void *, socklen_t *);
int route_rc;
if (real == NULL) real = dlsym(RTLD_NEXT, "getsockopt");
if (real == NULL) {
errno = ENOSYS;
return -1;
}
if (bl_nl_getsockopt(fd, level, option, value, len, real, &route_rc)) return route_rc;
return real(fd, level, option, value, len);
}
struct monitor_netlink_header {
char prefix[8];
unsigned magic;
unsigned header_size;
unsigned properties_off;
unsigned properties_len;
unsigned filter_subsystem_hash;
unsigned filter_devtype_hash;
unsigned filter_tag_bloom_hi;
unsigned filter_tag_bloom_lo;
};
static unsigned murmur_hash2(const char *key, size_t len) {
const unsigned m = 0x5bd1e995;
unsigned h = (unsigned)len;
const unsigned char *data = (const unsigned char *)key;
while (len >= 4) {
unsigned k;
memcpy(&k, data, 4);
k *= m;
k ^= k >> 24;
k *= m;
h *= m;
h ^= k;
data += 4;
len -= 4;
}
switch (len) {
case 3: h ^= data[2] << 16; __attribute__((fallthrough));
case 2: h ^= data[1] << 8; __attribute__((fallthrough));
case 1: h ^= data[0]; h *= m;
}
h ^= h >> 13;
h *= m;
h ^= h >> 15;
return h;
}
__attribute__((visibility("default"))) int bl_udevmon_inject(const char *action, const char *devpath,
const char *subsystem, const char *devname,
unsigned major, unsigned minor) {
static unsigned long long seqnum = 900000;
char props[1024];
int used = snprintf(props, sizeof(props), "ACTION=%s%cDEVPATH=%s%cSUBSYSTEM=%s%cDEVNAME=%s%cMAJOR=%u%cMINOR=%u%cSEQNUM=%llu",
action, 0, devpath, 0, subsystem, 0, devname, 0, major, 0, minor, 0,
__atomic_add_fetch(&seqnum, 1, __ATOMIC_RELAXED));
if (used <= 0 || (size_t)used >= sizeof(props)) return -1;
used++;
struct monitor_netlink_header header;
memset(&header, 0, sizeof(header));
memcpy(header.prefix, "libudev", 8);
header.magic = htobe32(0xfeedcafe);
header.header_size = sizeof(header);
header.properties_off = sizeof(header);
header.properties_len = (unsigned)used;
header.filter_subsystem_hash = htobe32(murmur_hash2(subsystem, strlen(subsystem)));
char message[sizeof(header) + sizeof(props)];
memcpy(message, &header, sizeof(header));
memcpy(message + sizeof(header), props, (size_t)used);
int sent = 0;
pthread_mutex_lock(&lock);
for (size_t i = 0; i < sizeof(stand_ins) / sizeof(stand_ins[0]); i++) {
if (stand_ins[i].fd == -1) continue;
if (send(stand_ins[i].peer, message, sizeof(header) + (size_t)used, MSG_DONTWAIT | MSG_NOSIGNAL) > 0) sent++;
}
pthread_mutex_unlock(&lock);
return sent;
}
__attribute__((visibility("hidden"))) int bl_udevmon_recvmsg(int fd, struct msghdr *msg, int flags,
ssize_t (*real_recvmsg)(int, struct msghdr *, int),
ssize_t *result) {
if (!is_stand_in(fd)) return 0;
size_t control_room = msg->msg_controllen;
socklen_t name_room = msg->msg_namelen;
ssize_t n = real_recvmsg(fd, msg, flags);
*result = n;
if (n < 0) return 1;
if (msg->msg_name != NULL && name_room >= sizeof(struct sockaddr_nl)) {
struct sockaddr_nl nl;
memset(&nl, 0, sizeof(nl));
nl.nl_family = AF_NETLINK;
nl.nl_groups = 2;
memcpy(msg->msg_name, &nl, sizeof(nl));
msg->msg_namelen = sizeof(nl);
}
if (msg->msg_control != NULL && control_room >= CMSG_SPACE(sizeof(struct ucred))) {
struct cmsghdr *cmsg = (struct cmsghdr *)msg->msg_control;
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_CREDENTIALS;
cmsg->cmsg_len = CMSG_LEN(sizeof(struct ucred));
struct ucred cred = {.pid = 1, .uid = 0, .gid = 0};
memcpy(CMSG_DATA(cmsg), &cred, sizeof(cred));
msg->msg_controllen = CMSG_SPACE(sizeof(struct ucred));
}
return 1;
}