sched_setscheduler returns success (0) for invalid policies β POSIX-required EINVAL missing; silent no-op leaves processes at real-time priority
| Field | Value |
|---|---|
| ID | DF-2937 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:N |
| CWE | CWE-20 |
| File | sys/kern/kern_p1003_1b.c |
| Lines | 265-267 (sink: kern_sched.c:166-197, no default case) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sys_sched_setscheduler forwards the unvalidated user policy int into ksched_setscheduler, whose switch handles only SCHED_FIFO/SCHED_RR/ SCHED_OTHER with no default case, so any other policy value returns 0 (success) while doing nothing. Live on the stock guest: policies 0, 4711, β1, INT_MAX all rc=0 as root; a process left at SCHED_RR stays real-time across a 'successful' bogus-policy call. Root-gated (CAN_AFFECT == cr_uid==0 even for self) β API-contract/robustness defect with a demonstrated operational hazard for privileged RT-tuning tools. Fix validated by full in-guest rebuild: default: e = EINVAL, valid paths unchanged.
Timeline
- 2026-09-02 Discovered during pass-2 audit of kern_p1003_1b.c (GLM 5.3); reproduced root-gated + fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2937 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| sched_probe.c | β | 2.9 KB | view raw | |
| build.sh | β | 391 B | view raw | |
| run.sh | β | 209 B | view raw | |
| build.log | β | 5.2 MB | β download | |
| run.unpriv.log | β | 1.8 KB | view raw | |
| run.root.log | β | 1.5 KB | view raw | |
| run.patched.log | β | 1.7 KB | view raw | |
| env.txt | β | 262 B | view raw | |
| fix.diff | β | 171 B | view raw | |
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 5.5 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-2937 β sched_setscheduler() returns success (0) for invalid policies; POSIX-required EINVAL missing
What
sys_sched_setscheduler() (sys/kern/kern_p1003_1b.c:250-276) passes the
user-supplied policy integer to ksched_setscheduler()
(sys/kern/kern_sched.c:160-198) without validating it. The callee's
switch (policy) handles only SCHED_RR, SCHED_FIFO, SCHED_OTHER and has
no default: case, so any other policy value (0, 4711, -1, INT_MAX, β¦)
falls through the switch and the function returns e == 0 β success β
while doing nothing.
POSIX.1b (sched_setscheduler, _POSIX_PRIORITY_SCHEDULING) requires [EINVAL]
for a policy value other than SCHED_FIFO, SCHED_RR, SCHED_OTHER.
Reachability / privilege gate
- Reaching the callee requires passing
p31b_proc()(sys/kern/kern_p1003_1b.c:136-167), whoseCAN_AFFECTgate (sys/kern/kern_p1003_1b.c:80) is((cr)->cr_uid == 0)β root-only, including for pid==0 (self). Verified empirically: as uid 1001 every pid-taking sched(2) call returns EPERM, evensched_getscheduler(0)(seerun.unpriv.log). - Therefore the defect is only triggerable by an effective-uid-0 caller β an API-contract / robustness defect (silent no-op success), not an unprivileged exploit. Severity: Info.
Why it still matters
A privileged tool (RT tuner, daemon supervisor) that requests a policy
change with a bad constant gets rc=0 and believes the change happened.
The probe demonstrates the concrete hazard: after a successful
sched_setscheduler(0, SCHED_RR, {0}) the calling process is real-time;
a subsequent sched_setscheduler(0, <garbage>, {β¦}) "succeeds" and the
process stays at real-time priority with the caller believing it
reverted to normal (see run.root.log: sched_getscheduler(self) still
returns SCHED_RR=3 after the bogus-policy "success").
Linux-compiled static binaries are also affected by the constant-space
collision (Linux SCHED_OTHER==0 β silent no-op on DragonFly).
Reproduce
scp findings/poc/DF-2937/sched_probe.c guest:/root/
ssh guest-root 'cc -o /root/sched_probe /root/sched_probe.c'
/root/sched_probe # as root: 4 bogus policies -> rc=0 (BUG)
# as user: everything pid-taking -> EPERM
Expected (unpatched, root): the last four lines all print rc=0 errno=0.
Expected (patched, root): the last four lines all print rc=-1 errno=22.
Fix
default: e = EINVAL; in the policy switch β fix.diff in this directory.
Validated by in-guest kernel rebuild: run.patched.log.
DF-2937 VERDICT
Finding: sys_sched_setscheduler() / ksched_setscheduler() return
success (0) for invalid policy values; POSIX-required EINVAL missing.
File: sys/kern/kern_p1003_1b.c (syscall layer) +
sys/kern/kern_sched.c (callee).
Severity: Info β root-gated (verified), pure API-contract defect, no
memory-safety impact.
Reproduced? β YES (as designed for a conformance bug)
Guest: DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC (stock INVARIANTS
kernel), QEMU/KVM. Source tree MD5-identical to the audit tree
(kern_sched.c c4ff911406ad45ba964248faae8f9283,
kern_p1003_1b.c c15d8af3399595dd2f17e12260aff2d7 before patching).
Baseline (unpatched)
run.root.log (uid 0):
sched_setscheduler(0, 0 /*Linux SCHED_OTHER*/, {0}) rc=0 errno=0
sched_setscheduler(0, 4711, {0}) rc=0 errno=0
sched_setscheduler(0, -1, {0}) rc=0 errno=0
sched_setscheduler(0, INT_MAX, {0}) rc=0 errno=0
All four bogus policies return success while doing nothing. The hazard is
visible in the same log: after the earlier successful
sched_setscheduler(0, SCHED_RR, {0}), the process remains SCHED_RR
(sched_getscheduler(self) = 3) across the "successful" bogus-policy call.
Root cause chain, line-accurate:
sys/kern/kern_p1003_1b.c:265-267βuap->policyforwarded toksched_setscheduler()without validation.sys/kern/kern_sched.c:166-195βswitch (policy)has cases only for SCHED_RR/SCHED_FIFO/SCHED_OTHER and nodefault:;estays 0 andreturn e(line 197) reports success. POSIX.1b sched_setscheduler(3) requires[EINVAL] The value of the policy parameter is invalid.
Privilege gate (bounds severity to Info) β verified
p31b_proc() (sys/kern/kern_p1003_1b.c:136-167) applies
CAN_AFFECT(p, p->p_ucred, other_proc) = ((cr)->cr_uid == 0)
(kern_p1003_1b.c:80) β including when pid==0 (self). run.unpriv.log
(uid 1001) shows every pid-taking sched(2) call β even
sched_getscheduler(0) β returning EPERM, and nonexistent/negative pids
returning ESRCH. The complete unprivileged reachable surface of this file
is therefore sched_yield() (rc=0) and sched_get_priority_max/min()
(validated switch, EINVAL for bogus policy, kern_sched.c:225-266) β
both memory-safe. No unprivileged route to the defect exists.
Fix validated? β YES
fix.diff: add default: e = EINVAL; break; to the policy switch.
Procedure: applied in guest /usr/src (patch -p1), make nativekernel
+ make installkernel, rebooted into the patched kernel (uname in
run.patched.log header differs: built by root on the same guest).
BUILD_DONE_OK in the in-guest /root/build.log; full tail saved as
build.tail.log.
Result (run.patched.log, uid 0):
sched_setscheduler(0, 0 /*Linux SCHED_OTHER*/, {0}) rc=-1 errno=22 (Invalid argument)
sched_setscheduler(0, 4711, {0}) rc=-1 errno=22 (Invalid argument)
sched_setscheduler(0, -1, {0}) rc=-1 errno=22 (Invalid argument)
sched_setscheduler(0, INT_MAX, {0}) rc=-1 errno=22 (Invalid argument)
Regression check in the same run: valid operations unchanged β
sched_setscheduler(0, SCHED_RR, {0}) rc=0, sched_setparam on an
SCHED_OTHER target still EINVAL (kern_sched.c:133-134 intent preserved),
unpriv calls still EPERM, sched_yield() rc=0. No new warnings; kernel
built with -Werror.
Exploit chain
None β not applicable (Info-severity conformance defect; root-gated).
Negative results recorded during this pass-2 audit (why the file is otherwise clean beyond known findings)
- UAF window
FIRST_LWP_IN_PROCβLWPHOLD(kern_p1003_1b.c:205-207et al.): killed β everyp_lwp_treeremoval requiresp->p_token(sys/kern/kern_exit.c:773,:1193), whichp31b_proc()holds across the window; both reapers drainlwp_lock(the LWPHOLD count) before removal (kern_exit.c:735-737,:1186-1193β the latter comment documents this exact contract). - pid validation: negative/garbage pids mask safely through
ALLPROC_HASH(pid) = pid & ALLPROC_HMASK(kern_proc.c:63) β ESRCH; SZOMB skipped (kern_proc.c:524). Verified empirically (ESRCH for -1, -99999999, 999999). - Token-order AB-BA (
p_tokenβlwp_tokenhere vslwp_tokeninlwpsignal,kern_sig.c:1163-1167): killed β DFly LWKT tokens are deadlock-free by construction (descheduled threads' tokens are pullable); the sig path takes lwp_token instead of, not nested with, p_token. - rr_get_interval tick math: none exists β
ksched_rr_get_intervalreturns the constant{0, 100000000}(kern_sched.c:63-64,272-278); its copyout is checked (kern_p1003_1b.c:348-349), unlike getparam's (DF-0171). - Policy mapping table:
getscheduleris a 3-case switch, not a table (kern_sched.c:103-121) β no index bound issue. - sched_param ABI: kernel and userland share
sys/sys/sched.h:47-50(4-byte struct defined outside any_KERNELguard) β no size mismatch. - Module unload UAF:
ksched_detachhas no caller (dead code; file is compiled-instandard,sys/conf/files:1971),kschedsingleton never freed. - Known, not re-reported: DF-0170 (ignored copyin Γ2, incl.
setscheduler:258), DF-0171 (ignored copyout:245), DF-0223
(SCHED_OTHER prio bounds,
kern_sched.c:181-184), DF-0224 (getparam uninit leak,kern_sched.c:142-150β demonstrated again inrun.root.logviasched_getparam(1)rc=0), DF-0225 (lwp_rtprio locking contract).
Fix verification
fixedApplied fix.diff (one-line default: EINVAL) to guest /usr/src, rebuilt with make nativekernel (17087-line -Werror-clean build log, BUILD_DONE_OK), installkernel + reboot into kernel #1. Re-running the identical probe: all four invalid policies now return EINVAL (rc=-1 errno=22); valid-path behavior unchanged (SCHED_RR set succeeds, SCHED_OTHER-target setparam still EINVAL, unpriv still EPERM, yield rc=0). Baseline defect gone, no regression.
['fix.diff', 'build.log (BUILD_DONE_OK)', 'run.patched.log vs run.root.log']
Confirmed kernel references
Detail
Evidence (decisive lines)
["run.root.log (baseline kernel #0): lines 'sched_setscheduler(0, {0,4711,-1,INT_MAX}, {0}) ... rc=0 errno=0' - success for invalid policies; 'sched_getscheduler(self) rc=3' after bogus-policy call shows process still SCHED_RR", 'run.unpriv.log (uid 1001): every pid-taking sched(2) call EPERM incl. pid==0; negative/nonexistent pids ESRCH - root gate bounds severity', 'run.patched.log (kernel #1 built with fix.diff): same four calls now rc=-1 errno=22; valid SCHED_RR set still rc=0; setparam-on-SCHED_OTHER still EINVAL - fix changes exactly the broken behavior, no regression', 'build.log: full nativekernel build output, BUILD_DONE_OK, -Werror clean', 'VERDICT.md: root-cause chain with path:line + the pass-2 negative results (UAF window killed via kern_exit.c:773/1193 p_token contract)']
PoC changes
Wrote probe from scratch (no seed existed): exercises all 8 syscalls across pid={0,self,other,nonexistent,negative} as both uid 1001 and uid 0; had to push source via scp because vm.sh run_user/run_root pipe the script through stdin.
Verified recommended fix
Add 'default: e = EINVAL; break;' to the policy switch in ksched_setscheduler (sys/kern/kern_sched.c) - one line, validated by full kernel rebuild.
Verdict
sys_sched_setscheduler (sys/kern/kern_p1003_1b.c:265-267) forwards the unvalidated policy int to ksched_setscheduler, whose switch (sys/kern/kern_sched.c:166-195) has no default case, so any policy outside {SCHED_FIFO=1, SCHED_OTHER=2, SCHED_RR=3} returns 0 (success) as a silent no-op, where POSIX requires EINVAL. Demonstrated live on the stock X86_64_GENERIC guest as root: policies 0, 4711, -1, INT_MAX all rc=0, and a process left at SCHED_RR stays real-time across a 'successful' bogus-policy call. The defect is root-gated: CAN_AFFECT (kern_p1003_1b.c:80) is cr_uid==0 even for pid==0, and the unpriv probe shows every pid-taking sched(2) call returning EPERM, so the complete unpriv reachable surface (sched_yield, sched_get_priority_max/min) is memory-safe. Info-severity API-contract defect, no memory-safety impact, no exploit chain.
No comments yet.