From 41fa2d2f2bea9d6ceef2ddd1f412bbf6e4c820a5 Mon Sep 17 00:00:00 2001 From: Trung Date: Mon, 20 Jul 2026 13:25:55 +0700 Subject: [PATCH] Accept live thread TIDs in getpriority Linux treats thread IDs as valid PRIO_PROCESS targets because each thread is a task. elfuse only accepted who 0 and the guest process ID, so getpriority(PRIO_PROCESS, tid) returned ESRCH for live guest threads. Add a shared process/TID liveness helper and use it from both scheduler queries and getpriority. Keep setpriority self-only while elfuse stores nice as one process-wide value. This avoids silently accepting a live TID and mutating the nice value reported for every task. Add regression coverage for live thread getpriority, non-self setpriority rejection, and dead thread ESRCH behavior. Fix #231 --- src/syscall/proc-identity.c | 13 ++- src/syscall/proc.h | 8 +- src/syscall/sys.c | 31 ++---- tests/test-credentials.c | 140 ++++++++++++++++++++++++++++ tests/test-identity-override-host.c | 7 ++ 5 files changed, 173 insertions(+), 26 deletions(-) diff --git a/src/syscall/proc-identity.c b/src/syscall/proc-identity.c index 31e34af6..6aaf1ff2 100644 --- a/src/syscall/proc-identity.c +++ b/src/syscall/proc-identity.c @@ -13,6 +13,7 @@ #include "syscall/abi.h" #include "core/shim-globals.h" +#include "runtime/thread.h" #include "syscall/proc-identity.h" #include "syscall/proc.h" @@ -223,10 +224,20 @@ void proc_set_nice(int32_t val) emu_nice = val; } +bool proc_pid_alive(int pid) +{ + if (pid == 0 || pid == (int) guest_pid) + return true; + return thread_tid_alive((int64_t) pid) != 0; +} + int64_t proc_sys_setpriority(int which, int who, int prio) { if (which != 0) return -LINUX_EINVAL; + /* emu_nice is process-global. Reject non-self task IDs until nice is + * tracked per task rather than silently changing every thread's value. + */ if (who != 0 && who != (int) guest_pid) return -LINUX_ESRCH; if (prio < -20) @@ -241,7 +252,7 @@ int64_t proc_sys_getpriority(int which, int who) { if (which != 0) return -LINUX_EINVAL; - if (who != 0 && who != (int) guest_pid) + if (!proc_pid_alive(who)) return -LINUX_ESRCH; return 20 - emu_nice; } diff --git a/src/syscall/proc.h b/src/syscall/proc.h index 03830413..fbee8de4 100644 --- a/src/syscall/proc.h +++ b/src/syscall/proc.h @@ -204,9 +204,13 @@ void proc_set_ids(uint32_t uid, /* Emulated nice value for setpriority/getpriority coherence. */ int32_t proc_get_nice(void); void proc_set_nice(int32_t val); +bool proc_pid_alive(int pid); -/* setpriority/getpriority: only PRIO_PROCESS for self. setpriority clamps prio - * to [-20, 19]. getpriority returns 20-nice (always > 0 per Linux convention). +/* setpriority/getpriority: only PRIO_PROCESS is modeled. getpriority accepts + * any live task ID but reports the process-global emulated nice value. + * setpriority is kept self-only until elfuse tracks per-task nice state. + * setpriority clamps prio to [-20, 19]. getpriority returns 20-nice (always + * > 0 per Linux convention). */ int64_t proc_sys_setpriority(int which, int who, int prio); int64_t proc_sys_getpriority(int which, int who); diff --git a/src/syscall/sys.c b/src/syscall/sys.c index 4bc9a33e..1f675734 100644 --- a/src/syscall/sys.c +++ b/src/syscall/sys.c @@ -81,12 +81,6 @@ _Static_assert(offsetof(struct rusage, ru_maxrss) == offsetof(linux_rusage_t, ru_maxrss), "ru_maxrss offset must stay aligned for fast translation"); -/* Defined below in the scheduler-policy section; forward-declared so - * sys_sched_getaffinity (which sits above the policy stubs) can share the same - * per-thread TID gate. - */ -static bool sched_pid_alive(int pid); - static void groups_init_cached_linux_groups(void) { gid_t groups[64]; @@ -281,7 +275,7 @@ int64_t sys_sched_getaffinity(guest_t *g, */ if (pid < 0) return -LINUX_EINVAL; - if (!sched_pid_alive(pid)) + if (!proc_pid_alive(pid)) return -LINUX_ESRCH; if (size < 8) return -LINUX_EINVAL; @@ -321,8 +315,8 @@ int64_t sys_sched_getaffinity(guest_t *g, * elfuse models a single SCHED_OTHER thread group. Linux scheduler syscalls are * per-thread: the pid argument is actually a TID, and a worker calling * sched_getscheduler(gettid()) must reach its own thread entry, not just the - * thread-group leader. Live TIDs are matched via thread_tid_alive(); pid 0 - * means "the calling thread" and is always accepted. + * thread-group leader. Live TIDs are matched via proc_pid_alive(); pid 0 means + * "the calling thread" and is always accepted. * * Any policy transition away from SCHED_OTHER is rejected unless the stub can * model it faithfully. Callers branch on the apparent policy, and silently @@ -339,15 +333,6 @@ int64_t sys_sched_getaffinity(guest_t *g, * Getters that only write back leave EFAULT for the final copy_to_user step, * matching the kernel's order. */ -static bool sched_pid_alive(int pid) -{ - if (pid == 0) - return true; - if (pid == (int) proc_get_pid()) - return true; - return thread_tid_alive((int64_t) pid) != 0; -} - /* Validate a sched_param for the named policy. * * Returns 0 if accepted by the stub, or a negative Linux errno. EPERM is @@ -390,7 +375,7 @@ int64_t sys_sched_getscheduler(int pid) { if (pid < 0) return -LINUX_EINVAL; - if (!sched_pid_alive(pid)) + if (!proc_pid_alive(pid)) return -LINUX_ESRCH; return LINUX_SCHED_NORMAL; } @@ -399,7 +384,7 @@ int64_t sys_sched_getparam(guest_t *g, int pid, uint64_t param_gva) { if (pid < 0 || param_gva == 0) return -LINUX_EINVAL; - if (!sched_pid_alive(pid)) + if (!proc_pid_alive(pid)) return -LINUX_ESRCH; linux_sched_param_t param = {0}; if (guest_write_small(g, param_gva, ¶m, sizeof(param)) < 0) @@ -417,7 +402,7 @@ int64_t sys_sched_setscheduler(guest_t *g, linux_sched_param_t param; if (guest_read_small(g, param_gva, ¶m, sizeof(param)) < 0) return -LINUX_EFAULT; - if (!sched_pid_alive(pid)) + if (!proc_pid_alive(pid)) return -LINUX_ESRCH; return sched_check_policy_param(policy, param.sched_priority); } @@ -429,7 +414,7 @@ int64_t sys_sched_setparam(guest_t *g, int pid, uint64_t param_gva) linux_sched_param_t param; if (guest_read_small(g, param_gva, ¶m, sizeof(param)) < 0) return -LINUX_EFAULT; - if (!sched_pid_alive(pid)) + if (!proc_pid_alive(pid)) return -LINUX_ESRCH; /* Current policy is SCHED_OTHER, so only priority 0 is valid. Any other * value mirrors the kernel's EINVAL for non-RT priority changes. @@ -475,7 +460,7 @@ int64_t sys_sched_rr_get_interval(guest_t *g, int pid, uint64_t ts_gva) { if (pid < 0) return -LINUX_EINVAL; - if (!sched_pid_alive(pid)) + if (!proc_pid_alive(pid)) return -LINUX_ESRCH; if (ts_gva == 0) return -LINUX_EFAULT; diff --git a/tests/test-credentials.c b/tests/test-credentials.c index 8de9c7a7..762b09eb 100644 --- a/tests/test-credentials.c +++ b/tests/test-credentials.c @@ -34,6 +34,97 @@ #define __NR_geteuid 175 #define __NR_getgid 176 #define __NR_setgroups 159 +#define __NR_sched_yield 124 +#define __NR_clock_gettime 113 + +#define CLOCK_MONOTONIC 1 + +#define CLONE_VM 0x00000100UL +#define CLONE_FS 0x00000200UL +#define CLONE_FILES 0x00000400UL +#define CLONE_SIGHAND 0x00000800UL +#define CLONE_THREAD 0x00010000UL +#define CLONE_SYSVSEM 0x00040000UL +#define CLONE_PARENT_SETTID 0x00100000UL +#define CLONE_CHILD_CLEARTID 0x00200000UL +#define CLONE_DETACHED 0x00400000UL + +typedef struct { + long tv_sec; + long tv_nsec; +} test_timespec_t; + +static volatile int prio_child_tid = 0; +static volatile int prio_child_ready = 0; +static volatile int prio_child_release = 0; +static int prio_dead_tid = 0; +static char prio_child_stack[8192] __attribute__((aligned(16))); + +static long monotonic_ms(void) +{ + test_timespec_t ts = {0}; + long rc = raw_syscall2(__NR_clock_gettime, CLOCK_MONOTONIC, (long) &ts); + if (rc != 0) + return -1; + return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static void priority_child_work(void) +{ + prio_child_ready = 1; + raw_futex_wake((int *) &prio_child_ready, 1); + + while (prio_child_release == 0) + raw_futex_wait((int *) &prio_child_release, 0); + + raw_exit(0); +} + +static long spawn_priority_child(void) +{ + prio_child_tid = 0; + prio_child_ready = 0; + prio_child_release = 0; + + unsigned long flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | + CLONE_THREAD | CLONE_SYSVSEM | CLONE_PARENT_SETTID | + CLONE_CHILD_CLEARTID | CLONE_DETACHED; + void *stack_top = prio_child_stack + sizeof(prio_child_stack); + long ret = + raw_clone(flags, stack_top, (int *) &prio_child_tid, /* parent_tid */ + 0, /* tls */ + (int *) &prio_child_tid); /* child_tid */ + if (ret == 0) { + priority_child_work(); + __builtin_unreachable(); + } + return ret; +} + +static void release_priority_child(void) +{ + prio_child_release = 1; + raw_futex_wake((int *) &prio_child_release, 1); + while (prio_child_tid != 0) + raw_futex_wait_cleartid((int *) &prio_child_tid, prio_child_tid); +} + +static long wait_for_dead_priority_tid(int tid) +{ + long start_ms = monotonic_ms(); + long prio = 0; + + for (;;) { + prio = raw_syscall2(__NR_getpriority, 0, tid); + if (prio == -3) + return prio; + raw_syscall0(__NR_sched_yield); + + long now_ms = monotonic_ms(); + if (start_ms >= 0 && now_ms >= 0 && now_ms - start_ms > 2000) + return prio; + } +} int main(void) { @@ -284,6 +375,55 @@ int main(void) raw_syscall3(__NR_setpriority, 0, 0, 0); } + TEST("getpriority live thread TID"); + { + long ret = spawn_priority_child(); + if (ret < 0) { + FAIL("clone failed"); + } else { + while (prio_child_ready == 0) + raw_futex_wait((int *) &prio_child_ready, 0); + + long prio = raw_syscall2(__NR_getpriority, 0, prio_child_tid); + EXPECT_TRUE(prio == 20, "getpriority(live tid) mismatch"); + release_priority_child(); + } + } + + TEST("setpriority live thread TID rejected"); + { + long ret = spawn_priority_child(); + if (ret < 0) { + FAIL("clone failed"); + } else { + while (prio_child_ready == 0) + raw_futex_wait((int *) &prio_child_ready, 0); + + raw_syscall3(__NR_setpriority, 0, 0, 0); + long baseline_prio = raw_syscall2(__NR_getpriority, 0, 0); + int tid = prio_child_tid; + long rc = raw_syscall3(__NR_setpriority, 0, tid, 5); + long self_prio = raw_syscall2(__NR_getpriority, 0, 0); + long tid_prio = raw_syscall2(__NR_getpriority, 0, tid); + EXPECT_TRUE(rc == -3 && self_prio == baseline_prio && + tid_prio == baseline_prio, + "setpriority(live tid) changed global nice"); + release_priority_child(); + prio_dead_tid = tid; + } + raw_syscall3(__NR_setpriority, 0, 0, 0); + } + + TEST("getpriority dead thread TID"); + { + if (prio_dead_tid == 0) { + FAIL("no dead tid from setpriority test"); + } else { + long dead_prio = wait_for_dead_priority_tid(prio_dead_tid); + EXPECT_TRUE(dead_prio == -3, "getpriority(dead tid) succeeded"); + } + } + /* sched_setaffinity: mask with CPU 0 should succeed */ TEST("sched_setaffinity(CPU0)"); { diff --git a/tests/test-identity-override-host.c b/tests/test-identity-override-host.c index c94aa3a5..0f6bfab3 100644 --- a/tests/test-identity-override-host.c +++ b/tests/test-identity-override-host.c @@ -25,6 +25,13 @@ void shim_globals_publish_pgsid(guest_t *g, int64_t pgid, int64_t sid) (void) sid; } +int thread_tid_alive(int64_t tid); +int thread_tid_alive(int64_t tid) +{ + (void) tid; + return 0; +} + int main(void) { /* Test 1: Fallback case (fakeroot disabled) */