-
Notifications
You must be signed in to change notification settings - Fork 26
Let kill reach a relative that is not a child #388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2555e69
e0012bb
464b77a
2cc9ea2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,7 @@ ELFUSE_HOST_NOFILE_MIN ?= $(shell bash "$(CURDIR)/tests/test-config.sh" --host-n | |
| test-sysroot-dotdot test-sysroot-openat2-walk \ | ||
| test-sysroot-inotify-names test-sysroot-exec-names \ | ||
| test-sysroot-interp-fallback test-sysroot-interp-cased \ | ||
| test-sysroot-absock-names test-absock-cleanup \ | ||
| test-sysroot-absock-names test-absock-cleanup test-registry-stale-pid \ | ||
| test-linkat-symlink-fallback test-casefold-host \ | ||
| test-casefold-walk-host test-absock-names-host \ | ||
| test-wakeup-pipe-host test-guest-env-host \ | ||
|
|
@@ -340,6 +340,7 @@ check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage check-eintr-contract ch | |
| $(call run-lane,test-sysroot-interp-cased,PT_INTERP through an escaped path) | ||
| $(call run-lane,test-sysroot-absock-names,pathname sockets across the escape boundary) | ||
| $(call run-lane,test-absock-cleanup,absock namespace lifecycle) | ||
| $(call run-lane,test-registry-stale-pid,stale registry record on a reused host pid) | ||
| $(call run-lane,test-sysroot-root,sysroot mounted at /) | ||
| $(call run-lane,test-nosysroot-literal-names,literal names without a sysroot) | ||
| $(call run-lane,test-sysroot-outside-names,literal names outside the sysroot) | ||
|
|
@@ -726,6 +727,39 @@ test-absock-cleanup: $(ELFUSE_BIN) $(BUILD_DIR)/test-absock-cleanup | |
| fi; \ | ||
| $(ASSERT_NO_ABSOCK_LEAK) | ||
|
|
||
| # An exited member's registry record outlives it, and macOS can hand its host | ||
| # pid to another elfuse process. The recipe plants such a record, host pid of | ||
| # a live unrelated elfuse run with a start time it does not have, and the | ||
| # family's kill(99, 0) must still fail with ESRCH. | ||
| ## registry ignores a record whose host pid was reused | ||
| test-registry-stale-pid: $(ELFUSE_BIN) $(BUILD_DIR)/test-registry-stale-pid | ||
| @tmp=$$(mktemp -d); xpid=; fpid=; \ | ||
| trap 'kill $$xpid $$fpid 2>/dev/null; rm -rf "$$tmp"' EXIT; \ | ||
| fail() { printf "FAIL: %s\n" "$$1"; exit 1; }; \ | ||
| printf " %-30s " "stale record on reused pid"; \ | ||
| mkfifo "$$tmp/go"; \ | ||
| $(ELFUSE_BIN) $(BUILD_DIR)/test-registry-stale-pid hold & \ | ||
| xpid=$$!; \ | ||
| $(ELFUSE_BIN) $(BUILD_DIR)/test-registry-stale-pid \ | ||
| < "$$tmp/go" > "$$tmp/out" & \ | ||
| fpid=$$!; \ | ||
| exec 4> "$$tmp/go"; \ | ||
| for i in $$(seq 1 50); do \ | ||
| grep -q READY "$$tmp/out" && break; \ | ||
| sleep 0.1; \ | ||
| done; \ | ||
| grep -q READY "$$tmp/out" || fail "family never reported READY"; \ | ||
| reg="$$(getconf DARWIN_USER_TEMP_DIR)elfuse-procs-$$fpid"; \ | ||
| [ -f "$$reg" ] || fail "no registry at $$reg"; \ | ||
| printf '%s 99 1 1\n' "$$xpid" >> "$$reg" || fail "cannot append to $$reg"; \ | ||
| echo go >&4; \ | ||
| exec 4>&-; \ | ||
| wait $$fpid; \ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| kill -0 "$$xpid" 2>/dev/null || fail "holder exited before the lookup"; \ | ||
| verdict=$$(sed -n 's/^STALE=//p' "$$tmp/out"); \ | ||
| [ "$$verdict" = esrch ] || fail "kill(99, 0) $${verdict:-unreported}"; \ | ||
| printf "OK\n" | ||
|
|
||
| # PT_INTERP names the loader by the guest's spelling, and a rootfs may ship | ||
| # it somewhere other than where the binary asks (store-style paths). The | ||
| # interp resolver falls back to /lib/<basename> when the asked-for path does | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1157,11 +1157,14 @@ static void proc_registry_reset_if_owner(const char *path) | |||||||||||||||||
| unlink(path); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /* One live member of a process group registry. */ | ||||||||||||||||||
| /* One live member of a process group registry. start_us is the host process's | ||||||||||||||||||
| * start time: a host pid alone matches whatever process macOS hands it to next. | ||||||||||||||||||
| */ | ||||||||||||||||||
| typedef struct { | ||||||||||||||||||
| pid_t host_pid; | ||||||||||||||||||
| int64_t guest_pid; | ||||||||||||||||||
| int64_t pgid; | ||||||||||||||||||
| uint64_t start_us; | ||||||||||||||||||
| } registry_entry_t; | ||||||||||||||||||
|
|
||||||||||||||||||
| #define REGISTRY_MAX_ENTRIES 4096 | ||||||||||||||||||
|
|
@@ -1178,10 +1181,10 @@ static int flock_retry(int fd, int op) | |||||||||||||||||
|
|
||||||||||||||||||
| /* Read @fd from its current offset and invoke @cb once per newline-terminated | ||||||||||||||||||
| * record, passing a NUL-terminated copy. Records must fit in 159 bytes; both | ||||||||||||||||||
| * the registry ("hostpid guestpid pgid") and signal/control transport records | ||||||||||||||||||
| * use bounded numeric lines. Overlong records and an unterminated trailing | ||||||||||||||||||
| * token are dropped -- every writer appends a whole record under an exclusive | ||||||||||||||||||
| * lock, so a partial line only appears after a crash mid-write. | ||||||||||||||||||
| * the registry ("hostpid guestpid pgid startus") and signal/control transport | ||||||||||||||||||
| * records use bounded numeric lines. Overlong records and an unterminated | ||||||||||||||||||
| * trailing token are dropped -- every writer appends a whole record under an | ||||||||||||||||||
| * exclusive lock, so a partial line only appears after a crash mid-write. | ||||||||||||||||||
| */ | ||||||||||||||||||
| static void for_each_record(int fd, void (*cb)(char *rec, void *ctx), void *ctx) | ||||||||||||||||||
| { | ||||||||||||||||||
|
|
@@ -1217,19 +1220,37 @@ typedef struct { | |||||||||||||||||
| bool truncated; | ||||||||||||||||||
| } registry_parse_ctx_t; | ||||||||||||||||||
|
|
||||||||||||||||||
| /* Upsert one "hostpid guestpid pgid" record, keeping the latest guest_pid/pgid | ||||||||||||||||||
| * per LIVE host pid. Dead, malformed, and out-of-range records are dropped. | ||||||||||||||||||
| /* Start time of host process @pid in microseconds. | ||||||||||||||||||
| * | ||||||||||||||||||
| * Returns false once @pid has exited. | ||||||||||||||||||
| */ | ||||||||||||||||||
| static bool host_start_us(pid_t pid, uint64_t *out) | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing on the reverse lookup uses this. |
||||||||||||||||||
| { | ||||||||||||||||||
| struct proc_bsdinfo info; | ||||||||||||||||||
| if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, sizeof(info)) != | ||||||||||||||||||
| (int) sizeof(info)) | ||||||||||||||||||
| return false; | ||||||||||||||||||
| *out = info.pbi_start_tvsec * 1000000ULL + info.pbi_start_tvusec; | ||||||||||||||||||
| return true; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /* Upsert one "hostpid guestpid pgid startus" record, keeping the latest | ||||||||||||||||||
| * guest_pid/pgid per LIVE host pid. A record whose start time differs from the | ||||||||||||||||||
| * running process's names an exited member whose host pid was reused, so it is | ||||||||||||||||||
| * dropped along with dead, malformed, and out-of-range records. | ||||||||||||||||||
| */ | ||||||||||||||||||
| static void registry_parse_cb(char *rec, void *vctx) | ||||||||||||||||||
| { | ||||||||||||||||||
| registry_parse_ctx_t *c = vctx; | ||||||||||||||||||
| long hp; | ||||||||||||||||||
| long long gp, pg; | ||||||||||||||||||
| if (sscanf(rec, "%ld %lld %lld", &hp, &gp, &pg) != 3) | ||||||||||||||||||
| unsigned long long st; | ||||||||||||||||||
| if (sscanf(rec, "%ld %lld %lld %llu", &hp, &gp, &pg, &st) != 4) | ||||||||||||||||||
| return; | ||||||||||||||||||
| if (hp <= 0 || hp > INT_MAX || pg < 0 || pg > INT_MAX) | ||||||||||||||||||
| return; | ||||||||||||||||||
| if (kill((pid_t) hp, 0) != 0) | ||||||||||||||||||
| uint64_t live_us; | ||||||||||||||||||
| if (!host_start_us((pid_t) hp, &live_us) || live_us != (uint64_t) st) | ||||||||||||||||||
| return; | ||||||||||||||||||
| int idx = -1; | ||||||||||||||||||
| for (int k = 0; k < c->n; k++) | ||||||||||||||||||
|
|
@@ -1244,6 +1265,7 @@ static void registry_parse_cb(char *rec, void *vctx) | |||||||||||||||||
| } | ||||||||||||||||||
| idx = c->n++; | ||||||||||||||||||
| c->entries[idx].host_pid = (pid_t) hp; | ||||||||||||||||||
| c->entries[idx].start_us = live_us; | ||||||||||||||||||
| } | ||||||||||||||||||
| c->entries[idx].guest_pid = (int64_t) gp; | ||||||||||||||||||
| c->entries[idx].pgid = (int64_t) pg; | ||||||||||||||||||
|
|
@@ -1328,7 +1350,8 @@ static void proc_registry_publish(pid_t host_pid, | |||||||||||||||||
| idx = i; | ||||||||||||||||||
| break; | ||||||||||||||||||
| } | ||||||||||||||||||
| if (idx < 0) { | ||||||||||||||||||
| uint64_t start_us; | ||||||||||||||||||
| if (idx < 0 && host_start_us(host_pid, &start_us)) { | ||||||||||||||||||
| if (n == REGISTRY_MAX_ENTRIES) | ||||||||||||||||||
|
|
||||||||||||||||||
| /* No slot for a new live member: group signals (kill(-1), | ||||||||||||||||||
|
|
@@ -1342,6 +1365,7 @@ static void proc_registry_publish(pid_t host_pid, | |||||||||||||||||
| else { | ||||||||||||||||||
| idx = n++; | ||||||||||||||||||
| entries[idx].host_pid = host_pid; | ||||||||||||||||||
| entries[idx].start_us = start_us; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| if (idx >= 0) { | ||||||||||||||||||
|
|
@@ -1351,11 +1375,12 @@ static void proc_registry_publish(pid_t host_pid, | |||||||||||||||||
|
|
||||||||||||||||||
| if (ftruncate(fd, 0) == 0 && lseek(fd, 0, SEEK_SET) == 0) { | ||||||||||||||||||
| for (int i = 0; i < n; i++) { | ||||||||||||||||||
| char lineb[64]; | ||||||||||||||||||
| int len = snprintf(lineb, sizeof(lineb), "%ld %lld %lld\n", | ||||||||||||||||||
| char lineb[96]; | ||||||||||||||||||
| int len = snprintf(lineb, sizeof(lineb), "%ld %lld %lld %llu\n", | ||||||||||||||||||
| (long) entries[i].host_pid, | ||||||||||||||||||
| (long long) entries[i].guest_pid, | ||||||||||||||||||
| (long long) entries[i].pgid); | ||||||||||||||||||
| (long long) entries[i].pgid, | ||||||||||||||||||
| (unsigned long long) entries[i].start_us); | ||||||||||||||||||
| if (len > 0 && (size_t) len < sizeof(lineb) && | ||||||||||||||||||
| write_all(fd, lineb, (size_t) len) < 0) | ||||||||||||||||||
| break; | ||||||||||||||||||
|
|
@@ -1708,9 +1733,14 @@ int proc_set_child_pgid(int64_t guest_pid_val, int64_t pgid) | |||||||||||||||||
| return ret; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| int proc_get_namespace_targets(proc_signal_target_t *out, | ||||||||||||||||||
| int max, | ||||||||||||||||||
| int64_t pgid_filter) | ||||||||||||||||||
| /* Shared body for the group/broadcast collector and the single-pid lookup. | ||||||||||||||||||
| * guest_filter of 0 accepts every member; a positive value stops at the one | ||||||||||||||||||
| * member carrying that guest pid. | ||||||||||||||||||
| */ | ||||||||||||||||||
| static int registry_collect(proc_signal_target_t *out, | ||||||||||||||||||
| int max, | ||||||||||||||||||
| int64_t pgid_filter, | ||||||||||||||||||
| int64_t guest_filter) | ||||||||||||||||||
| { | ||||||||||||||||||
| /* No republish here: every group change already publishes (fork, setpgid, | ||||||||||||||||||
| * setsid), and this reader excludes its own entry anyway. | ||||||||||||||||||
|
|
@@ -1749,6 +1779,8 @@ int proc_get_namespace_targets(proc_signal_target_t *out, | |||||||||||||||||
| continue; | ||||||||||||||||||
| if (pgid_filter != PROC_PGID_ANY && entries[i].pgid != pgid_filter) | ||||||||||||||||||
| continue; | ||||||||||||||||||
| if (guest_filter > 0 && entries[i].guest_pid != guest_filter) | ||||||||||||||||||
| continue; | ||||||||||||||||||
| char ppath[PROC_PIDPATHINFO_MAXSIZE]; | ||||||||||||||||||
| int plen = proc_pidpath(entries[i].host_pid, ppath, sizeof(ppath)); | ||||||||||||||||||
| if (plen != our_len || memcmp(ppath, our_path, (size_t) our_len)) | ||||||||||||||||||
|
|
@@ -1760,6 +1792,21 @@ int proc_get_namespace_targets(proc_signal_target_t *out, | |||||||||||||||||
| return count; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| int proc_get_namespace_targets(proc_signal_target_t *out, | ||||||||||||||||||
| int max, | ||||||||||||||||||
| int64_t pgid_filter) | ||||||||||||||||||
| { | ||||||||||||||||||
| return registry_collect(out, max, pgid_filter, 0); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| pid_t proc_namespace_host_pid(int64_t guest_pid) | ||||||||||||||||||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||||||||||||||||||
| { | ||||||||||||||||||
| proc_signal_target_t target; | ||||||||||||||||||
|
Comment on lines
+1802
to
+1804
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two inputs break the contract
Suggested change
|
||||||||||||||||||
| return registry_collect(&target, 1, PROC_PGID_ANY, guest_pid) > 0 | ||||||||||||||||||
| ? target.host_pid | ||||||||||||||||||
| : -1; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| int64_t proc_host_to_guest_pid(pid_t host_pid) | ||||||||||||||||||
| { | ||||||||||||||||||
| pthread_mutex_lock(&pid_lock); | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -495,6 +495,16 @@ int proc_get_namespace_targets(proc_signal_target_t *out, | |
| int max, | ||
| int64_t pgid_filter); | ||
|
|
||
| /* Resolve one guest pid to its host pid through the same fork-family registry | ||
| * proc_get_namespace_targets reads. The child table only holds descendants, so | ||
| * this is what lets a process signal a relative that is not its own child (its | ||
| * parent, most commonly). | ||
| * | ||
| * Returns the host pid, or -1 when the registry holds no live member with that | ||
| * guest pid. | ||
| */ | ||
| pid_t proc_namespace_host_pid(int64_t guest_pid); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| /* Publish the caller's current guest pid/pgid to the fork-family registry. */ | ||
| void proc_registry_publish_self(void); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1034,6 +1034,17 @@ static int kill_deliver_targets(const proc_signal_target_t *targets, | |
| return delivered; | ||
| } | ||
|
|
||
| /* Resolve a guest pid for kill(2). The child table answers for descendants. | ||
| * Every other member of the fork family, the caller's own parent above all, | ||
| * exists only in the namespace registry, the same source the group and | ||
| * broadcast forms already read. | ||
| */ | ||
| static pid_t kill_resolve_host_pid(int64_t gpid) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This puts pid-directed |
||
| { | ||
| pid_t hpid = proc_guest_to_host_pid(gpid); | ||
| return hpid > 0 ? hpid : proc_namespace_host_pid(gpid); | ||
| } | ||
|
|
||
| static int64_t sc_kill(guest_t *g, | ||
| uint64_t x0, | ||
| uint64_t x1, | ||
|
|
@@ -1083,7 +1094,7 @@ static int64_t sc_kill(guest_t *g, | |
| } | ||
| int64_t r = (pid == (int) our_pid) ? 0 : -LINUX_ESRCH; | ||
| if (r == -LINUX_ESRCH) { | ||
| pid_t hpid = proc_guest_to_host_pid((int64_t) pid); | ||
| pid_t hpid = kill_resolve_host_pid((int64_t) pid); | ||
| if (hpid > 0) | ||
| r = (kill(hpid, 0) == 0) ? 0 : -LINUX_ESRCH; | ||
| } | ||
|
|
@@ -1148,7 +1159,7 @@ static int64_t sc_kill(guest_t *g, | |
| signal_queue(sig); | ||
| return 0; | ||
| } | ||
| pid_t hpid = proc_guest_to_host_pid((int64_t) pid); | ||
| pid_t hpid = kill_resolve_host_pid((int64_t) pid); | ||
| if (hpid > 0) | ||
| return (proc_send_guest_signal(hpid, (int64_t) pid, sig) == 0) | ||
| ? 0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* | ||
| * Test kill(pid, sig) aimed at the caller's parent | ||
| * | ||
| * Copyright 2026 elfuse contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * A forked child probes its parent with kill(getppid(), 0) and then signals it | ||
| * with SIGUSR1. The parent is not in the child's own descendant table, so both | ||
| * calls exercise the single-pid lookup through the fork-family registry. | ||
| */ | ||
|
|
||
| #include <signal.h> | ||
| #include <stdbool.h> | ||
| #include <stdio.h> | ||
| #include <string.h> | ||
| #include <sys/wait.h> | ||
| #include <time.h> | ||
| #include <unistd.h> | ||
|
|
||
| static volatile sig_atomic_t got_usr1 = 0; | ||
|
|
||
| static void usr1_handler(int sig) | ||
| { | ||
| (void) sig; | ||
| got_usr1 = 1; | ||
| } | ||
|
|
||
| static bool wait_flag(int max_ms) | ||
| { | ||
| for (int i = 0; i < max_ms && !got_usr1; i++) { | ||
| struct timespec ts = {0, 1000000}; /* 1 ms */ | ||
| nanosleep(&ts, NULL); | ||
| } | ||
| return got_usr1 != 0; | ||
| } | ||
|
|
||
| int main(void) | ||
| { | ||
| int failed = 0; | ||
| struct sigaction sa; | ||
| memset(&sa, 0, sizeof(sa)); | ||
| sa.sa_handler = usr1_handler; | ||
| sigaction(SIGUSR1, &sa, NULL); | ||
|
|
||
| pid_t pid = fork(); | ||
| if (pid < 0) | ||
| return 1; | ||
| if (pid == 0) { | ||
| if (kill(getppid(), 0) != 0) | ||
| _exit(1); | ||
| if (kill(getppid(), SIGUSR1) != 0) | ||
| _exit(2); | ||
| _exit(0); | ||
| } | ||
|
|
||
| if (!wait_flag(2000)) { | ||
| fprintf(stderr, "FAIL: child kill(getppid(), SIGUSR1) not delivered\n"); | ||
| failed++; | ||
| } | ||
| int status = 0; | ||
| if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) { | ||
| fprintf(stderr, "FAIL: child did not exit cleanly\n"); | ||
| failed++; | ||
| } else if (WEXITSTATUS(status) == 1) { | ||
| fprintf(stderr, "FAIL: kill(getppid(), 0) failed in child\n"); | ||
| failed++; | ||
| } else if (WEXITSTATUS(status) == 2) { | ||
| fprintf(stderr, "FAIL: kill(getppid(), SIGUSR1) failed in child\n"); | ||
| failed++; | ||
| } | ||
|
|
||
| printf("%s: %d failed\n", failed == 0 ? "PASS" : "FAIL", failed); | ||
| return failed == 0 ? 0 : 1; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.