mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
* 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>
161 lines
6.7 KiB
Python
161 lines
6.7 KiB
Python
import errno
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
|
|
|
|
PROGRAM = r"""
|
|
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <spawn.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
extern char **environ;
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc < 3) return 2;
|
|
const char *how = argv[1], *file = argv[2];
|
|
char **args = argv + 2;
|
|
if (!strcmp(how, "access")) {
|
|
int result = access(file, X_OK);
|
|
printf("access %d %d\n", result, result ? errno : 0);
|
|
return 0;
|
|
}
|
|
if (!strcmp(how, "spawn") || !strcmp(how, "spawnp")) {
|
|
pid_t pid;
|
|
int error = how[5] ? posix_spawnp(&pid, file, NULL, NULL, args, environ) : posix_spawn(&pid, file, NULL, NULL, args, environ);
|
|
if (error) { printf("errno %d\n", error); return 3; }
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
printf("exit %d\n", WEXITSTATUS(status));
|
|
return 0;
|
|
}
|
|
if (!strcmp(how, "execve")) execve(file, args, environ);
|
|
else if (!strcmp(how, "execv")) execv(file, args);
|
|
else if (!strcmp(how, "execvp")) execvp(file, args);
|
|
else if (!strcmp(how, "execvpe")) execvpe(file, args, environ);
|
|
printf("errno %d\n", errno);
|
|
return 3;
|
|
}
|
|
"""
|
|
|
|
HANDLER = "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$HANDLER_LOG\"\nexit 7\n"
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("gcc") and os.uname().machine == "x86_64", "needs gcc on an x86-64 host")
|
|
class BinfmtPreloadTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.tmp = Path(tempfile.mkdtemp())
|
|
cls.handler = cls.tmp / "handler"
|
|
cls.handler.write_text(HANDLER)
|
|
cls.handler.chmod(0o755)
|
|
cls.library = cls.tmp / "libbinfmt.so"
|
|
subprocess.run(["gcc", "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-Werror", "-DBINFMT_TEST",
|
|
'-DBINFMT_HANDLER="%s"' % cls.handler, "-o", str(cls.library),
|
|
"-pthread"] + [str(PRELOAD / name) for name in ("binfmt.c", "net.c", "udevmon.c", "pathcache.c", "perms.c", "webhelper.c")]
|
|
+ [str(Path(__file__).with_name("netlink_stubs.c")), "-ldl"],
|
|
check=True)
|
|
source = cls.tmp / "program.c"
|
|
source.write_text(PROGRAM)
|
|
cls.program = cls.tmp / "program"
|
|
subprocess.run(["gcc", "-O2", "-o", str(cls.program), str(source)], check=True)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
shutil.rmtree(cls.tmp, True)
|
|
|
|
def setUp(self):
|
|
self.dir = Path(tempfile.mkdtemp(dir=self.tmp))
|
|
self.log = self.dir / "handler.log"
|
|
|
|
def file(self, name, data, mode=0o755):
|
|
path = self.dir / name
|
|
path.write_bytes(data)
|
|
path.chmod(mode)
|
|
return path
|
|
|
|
def x86(self, name="tool", mode=0o755):
|
|
path = self.dir / name
|
|
shutil.copyfile(shutil.which("true"), path)
|
|
path.chmod(mode)
|
|
return path
|
|
|
|
def run_program(self, how, target, *args, desktop=True):
|
|
env = {"PATH": "%s:/usr/bin:/bin" % self.dir, "LD_PRELOAD": str(self.library), "HANDLER_LOG": str(self.log)}
|
|
if desktop:
|
|
env["BL_DESKTOP"] = "1"
|
|
done = subprocess.run([str(self.program), how, str(target)] + list(args), env=env,
|
|
stdout=subprocess.PIPE, text=True, timeout=30)
|
|
handed = self.log.read_text().splitlines() if self.log.exists() else None
|
|
return done.returncode, done.stdout.strip(), handed
|
|
|
|
def test_an_x86_program_goes_to_the_handler_before_it_is_executed(self):
|
|
tool = self.x86()
|
|
for how in ("execve", "execv"):
|
|
self.log.unlink(missing_ok=True)
|
|
self.assertEqual(self.run_program(how, tool, "a", "b c"), (7, "", ["--exec", str(tool), "a", "b c"]))
|
|
|
|
def test_outside_the_desktop_nothing_changes(self):
|
|
self.assertEqual(self.run_program("execve", self.x86(), desktop=False), (0, "", None))
|
|
|
|
def test_a_program_without_execute_permission_stays_refused(self):
|
|
tool = self.x86(mode=0o644)
|
|
self.assertEqual(self.run_program("execve", tool), (3, "errno %d" % errno.EACCES, None))
|
|
|
|
def test_a_native_program_the_kernel_cannot_run_keeps_its_error(self):
|
|
arm64 = bytearray(64)
|
|
arm64[0:4], arm64[4], arm64[5], arm64[18] = b"\x7fELF", 2, 1, 183
|
|
program = self.file("arm64", bytes(arm64))
|
|
self.assertEqual(self.run_program("execve", program), (3, "errno %d" % errno.ENOEXEC, None))
|
|
|
|
def test_a_script_runs_as_itself(self):
|
|
script = self.file("script.sh", b"#!/bin/sh\nexit 5\n")
|
|
self.assertEqual(self.run_program("execve", script), (5, "", None))
|
|
|
|
def test_a_windows_program_found_on_path_goes_to_the_handler(self):
|
|
program = self.file("setup.exe", b"MZ" + bytes(62))
|
|
for how in ("execvp", "execvpe"):
|
|
self.log.unlink(missing_ok=True)
|
|
self.assertEqual(self.run_program(how, "setup.exe", "/S"), (7, "", ["--exec", str(program), "/S"]))
|
|
|
|
def test_a_spawned_x86_program_goes_to_the_handler(self):
|
|
tool = self.x86()
|
|
self.assertEqual(self.run_program("spawn", tool, "x"), (0, "exit 7", ["--exec", str(tool), "x"]))
|
|
self.log.unlink()
|
|
self.assertEqual(self.run_program("spawnp", "tool", "y"), (0, "exit 7", ["--exec", str(tool), "y"]))
|
|
|
|
def test_access_keeps_the_kernels_answer_off_noexec_storage(self):
|
|
self.assertEqual(self.run_program("access", self.x86(mode=0o644)), (0, "access -1 %d" % errno.EACCES, None))
|
|
self.assertEqual(self.run_program("access", self.x86()), (0, "access 0 0", None))
|
|
|
|
def test_a_program_on_noexec_storage_passes_access(self):
|
|
storage = next((d for d in (os.environ.get("XDG_RUNTIME_DIR"), "/dev/shm", "/run/user/%d" % os.getuid())
|
|
if d and os.path.isdir(d) and os.access(d, os.W_OK) and os.statvfs(d).f_flag & os.ST_NOEXEC), None)
|
|
if storage is None:
|
|
self.skipTest("no writable noexec mount here")
|
|
folder = Path(tempfile.mkdtemp(dir=storage))
|
|
self.addCleanup(shutil.rmtree, folder, True)
|
|
tool = folder / "tool"
|
|
shutil.copyfile(shutil.which("true"), tool)
|
|
notes = folder / "notes.txt"
|
|
notes.write_text("hello\n")
|
|
self.assertEqual(self.run_program("access", tool), (0, "access 0 0", None))
|
|
self.assertEqual(self.run_program("access", notes), (0, "access -1 %d" % errno.EACCES, None))
|
|
self.assertEqual(self.run_program("access", tool, desktop=False), (0, "access -1 %d" % errno.EACCES, None))
|
|
|
|
def test_a_spawned_native_script_is_left_alone(self):
|
|
self.file("script.sh", b"#!/bin/sh\nexit 5\n")
|
|
self.assertEqual(self.run_program("spawnp", "script.sh"), (0, "exit 5", None))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|