Files
DroidDeck/tools/linuxfs/preload/tracer.c
Kurt Himebauch 2848484a99 feat: fuck proot and proots mama (#2300)
* feat: experimental native userland that runs the Linux side without proot

* fix(native): keep the visible name of link2symlink binaries

* fix(ci): run the native hook build through bash

* fix(native): address review findings in the hook and the ctl fallback

* fix(ctl): use the broadcast fallback only when content is missing

* fix(native): survive Android's seccomp filter in ld.so, libc and posix_spawn children

* docs(native): how to test under Android's seccomp filter

* fix(session): hide an unlistable /sys/bus/pci/devices so libpci does not abort Chromium's GPU init

* fix(native): nested syscall() redirects no longer loop with libblsession's openat2

* fix(native): controllers reach Steam and the install-script evaluator no longer blocks game moves

* fix(native): write the hook's NUL character literals as escapes

* fix(native): proot fast path turns off and greys out while running without proot

* fix(native): D-Bus logins work and Xwayland still admits root once peers read as root

* fix(native): scripts on session mounts outside the rootfs run with the rootfs shell

* fix(native): games on the SD card library start in their own folder

* fix(native): Wine drive links proot made point at their real places

* fix(native): at-calls, uid lookups and relative programs follow the session's mounts

* fix(native): close-from in spawned programs covers what is open at the spawn

* fix(native): service names resolve and the DNS fallback answers what was asked

* build(native): the hook is built with its Go support and tracked by the preload recipe

* fix(native): drive links into identity mounts stay, and relink and seccomp patch failures are logged

* fix(native): seccomp-sim's filter holds every allowed call

* fix(ctl): the broadcast fallback only stands in for a missing content command

* fix(native): pre-2.33 glibc stat calls present owners as root too

* fix(native): res_init and res_ninit call glibc, so Steam's store resolves names

* fix(ci): a preload lock older than a new output rebuilds that group, and generated headers are not hashed

* fix(native): a second seccomp patch run leaves other system calls alone

* fix(native): the terminal and bottom desktop keep proot's environment in a native session

* fix(native): any number of session mounts, and openat2 relative to a mounted folder

* fix(agent): a native command's stdin is written beside its timeout

* fix(ctl): native rejects --env without = and timeouts outside 0-600 s

* fix(native): the resolver fallback has the network's IPv6 servers, and lookups keep the fallback's validation errors

* fix(native): a spawn whose dup2 source close-from closed fails with EBADF

* fix(native): __getcwd_chk checks its buffer, and seccomp-sim -a takes only call numbers

* feat(native): guests see the session hostname from uname and gethostname

* feat(native): the terminal and bottom desktop run without proot

* fix(components): an origin APK without a newer preload output still proves the rest

* fix(native): seccomp-sim -a rejects empty fields

* fix(native): gethostbyname no longer answers failed lookups with Ubuntu and Debian addresses

* feat(native): store installers, root helpers, Comet and Xodus run without proot

* fix(native): the hook finds libraries, preloads and itself through a sandbox's mounts, and ldconfig -C makes its cache

* feat(native): Flatpak sandboxes run without proot, and apply_extra runs without a runtime

* fix(native): remove, glob and nftw take guest paths, and links in a mount are followed as the guest sees them

* fix(native): Flatpak sandboxes get the session's mounts, their libraries' RUNPATHs, and no rootfs-only settings

* fix(native): gethostbyname asks the network's DNS servers, nftw of / gives single slashes, and the library path fits mapped directories

* fix(native): a native process starts in its working directory's real place

* fix(native): the library path given to ld.so holds whole directories only

* fix(native): Wine drive links keep their guest paths, so proot finds the library after a native session

* fix(native): droiddeckctl native prepares the rootfs with the setting off

* fix(native): a SysV segment stays while any process has it attached

* fix(native): DNS lookups ask the network's own servers before public ones

* fix(native): a program's own seccomp traps reach its own SIGSYS handler

* feat(native): run native sessions under a device's own app seccomp filter for testing

* fix(native): SysV attaches and collection take a lock, a failed mark fails the attach, and collection closes its directory

* fix(native): shmat fails when it cannot take the segment lock

* fix(native): the SysV segment lock is retried when a signal interrupts it

* fix(native): process tools see the session's processes as root's

* fix(native): native processes are named after their program, not ld.so

* fix(native): a session's helpers keep the mode the session started in

* fix(native): a keyed SysV segment stays until IPC_RMID

* fix(native): a rootfs link to an absolute guest path is followed in the guest, so Wine finds its z: drive

* fix(native): a sandbox whose loader can't be patched runs under proot

* fix(native): creating a private SysV segment also collects stale ones

* fix(native): a session stopped during startup forgets its mode

* fix(xbox): a native backend is authorized by its process ancestry

* fix(native): Wine's drive links are followed in the guest from any prefix

* fix(native): only the startup that owns the running session publishes its mode

* fix(native): a SysV segment stays until IPC_RMID even after its maker ends

* fix(native): a sandbox mount nested in another mount gets its own place

* fix(native): a sandbox whose mount places can't be made runs under proot
2026-11-11 09:13:17 -05:00

119 lines
3.7 KiB
C

/*
* Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0.
*/
#define _GNU_SOURCE
#include <ctype.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#define STATUS_MAX 16384
#define TRACER_FIELD "\nTracerPid:\t"
int bl_from_ntdll(const void *caller) __attribute__((visibility("hidden")));
static const char *skip_digits(const char *s) {
const char *end = s;
while (isdigit((unsigned char)*end)) end++;
return end == s ? NULL : end;
}
static long status_pid(const char *path) {
const char *rest;
long pid = 0;
if (path == NULL || strncmp(path, "/proc/", 6) != 0) return -1;
path += 6;
if (strncmp(path, "self/", 5) == 0) rest = path + 4;
else if (strncmp(path, "thread-self/", 12) == 0) rest = path + 11;
else if ((rest = skip_digits(path)) == NULL) return -1;
else pid = strtol(path, NULL, 10);
if (strncmp(rest, "/task/", 6) == 0 && (rest = skip_digits(rest + 6)) == NULL) return -1;
return strcmp(rest, "/status") == 0 ? pid : -1;
}
static int is_web_helper(const void *caller) {
if (strcmp(program_invocation_short_name, "steamwebhelper") != 0 &&
strcmp(program_invocation_short_name, "vrwebhelper") != 0)
return 0;
const char *hide = getenv("BL_WEBHELPER_HIDE_TRACER");
Dl_info info;
if (hide && hide[0] == '1' && caller && dladdr(caller, &info) && info.dli_fname
&& strstr(info.dli_fname, "libtier0_s.so"))
return 0;
return 1;
}
static int write_all(int fd, const char *data, size_t len) {
while (len > 0) {
ssize_t n = write(fd, data, len);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) return -1;
data += n;
len -= (size_t)n;
}
return 0;
}
static int read_only_copy(const char *data, size_t len, int flags) {
char path[] = "/tmp/.droiddeck-status-XXXXXX";
int rw = mkostemp(path, O_CLOEXEC);
if (rw < 0) return -1;
int ro = -1;
if (write_all(rw, data, len) == 0)
ro = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | (flags & O_CLOEXEC));
unlink(path);
close(rw);
return ro;
}
__attribute__((visibility("hidden"))) int bl_status_without_tracer(const char *path, int flags, const void *caller) {
char text[STATUS_MAX];
size_t len = 0;
int saved = errno;
int out = -1;
long pid;
if ((flags & O_ACCMODE) != O_RDONLY || (pid = status_pid(path)) < 0 || is_web_helper(caller) || bl_from_ntdll(caller))
return -1;
// through the next open(): the native userland's hook gives the app's ids as root's there
static int (*next_open)(const char *, int, ...) = NULL;
if (!next_open) next_open = (int (*)(const char *, int, ...))dlsym(RTLD_NEXT, "open");
int in = next_open ? next_open(path, O_RDONLY | O_CLOEXEC) : (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC);
if (in < 0) goto done;
for (;;) {
ssize_t n = read(in, text + len, sizeof(text) - 1 - len);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) break;
len += (size_t)n;
}
close(in);
text[len] = '\0';
char *field = strstr(text, TRACER_FIELD);
if (field == NULL) goto done;
char *value = field + strlen(TRACER_FIELD);
char *line_end = strchr(value, '\n');
if (line_end == NULL || line_end == value) goto done;
memmove(value + 1, line_end, len - (size_t)(line_end - text) + 1);
*value = '0';
len = (size_t)(value + 1 - text) + strlen(value + 1);
if (pid != 0 && pid != getpid() && (out = read_only_copy(text, len, flags)) >= 0) goto done;
out = memfd_create("status", (flags & O_CLOEXEC) ? MFD_CLOEXEC : 0);
if (out < 0) goto done;
if (write_all(out, text, len) != 0 || lseek(out, 0, SEEK_SET) != 0) {
close(out);
out = -1;
}
done:
errno = saved;
return out;
}