β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2937

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2937 Β· 13 files
FileTypeDescriptionSize
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), whose CAN_AFFECT gate (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, even sched_getscheduler(0) (see run.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.

VERDICT.md
↓ download raw

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:

  1. sys/kern/kern_p1003_1b.c:265-267 β€” uap->policy forwarded to ksched_setscheduler() without validation.
  2. sys/kern/kern_sched.c:166-195 β€” switch (policy) has cases only for SCHED_RR/SCHED_FIFO/SCHED_OTHER and no default:; e stays 0 and return 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-207 et al.): killed β€” every p_lwp_tree removal requires p->p_token (sys/kern/kern_exit.c:773, :1193), which p31b_proc() holds across the window; both reapers drain lwp_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_token here vs lwp_token in lwpsignal, 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_interval returns 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: getscheduler is 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 _KERNEL guard) β€” no size mismatch.
  • Module unload UAF: ksched_detach has no caller (dead code; file is compiled-in standard, sys/conf/files:1971), ksched singleton 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 in run.root.log via sched_getparam(1) rc=0), DF-0225 (lwp_rtprio locking contract).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 3 17:37:43 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.