Reachable panic("semop - can't undo undos") in sys_semop SEM_UNDO rollback via same-process thread race β unprivileged local kernel DoS
| Field | Value |
|---|---|
| ID | DF-2785 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-362 Race Condition β unhandled error (kernel panic) |
| File | sys/kern/sysv_sem.c |
| Lines | 998-1006 (rollback), 237-268 (adjust) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sys_semop's SEM_UNDO error path rolls undo adjustments back in reverse order and panic()s if any rollback adjust fails, on the assumption that reverse order "guarantees we won't run out of space". That guarantee is single-threaded only: undo entries are keyed (semid, semnum) in the process-shared p_sem_undo, and semundo_adjust() serializes only per call β nothing protects one thread's undo vector against siblings. A victim thread whose vector self-cancels an entry reaches the rollback with that entry absent by construction; if sibling churn threads keep the undo table full (un_cnt == semume == 25) in the gap, the rollback adjust takes not-found+full β EINVAL β panic. Reproduced on the stock INVARIANTS guest: unprivileged user panicked the kernel in ~3 seconds. Full-system DoS (reboot); negative control (lockstep churn, table never full) 0 errors in 21M iterations, proving the full-table precondition.
Proof of contest
VERIFIED (findings/poc/DF-2785/undo_race_panic.c, unpriv): 25 filler
undo entries + two churn threads oscillating un_cnt 24β25 + victim
looping the 4-op self-cancelling vector β panic: semop - can't undo
undos, sys_semop+0x65d, guest wedged in DDB ~3s after launch. Fixed
kernel (panic β kprintf + break) survived 300s+: 306M victim
iterations, 21247 failure-path errors, 170 formerly-fatal race events,
guest up.
Recommended fix
Minimal: replace the panic with kprintf + break (validated). Full fix: serialize the per-process undo vector (mutex held across sys_semop's adjust/rollback phase and semexit's apply loop). fix.diff in the pack.
Timeline
- 2026-08-31 Discovered during pass-2 audit of sysv_sem.c (GLM 5.3); unpriv panic reproduced ~3s + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2785 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| undo_race_panic.c | β | 3.7 KB | view raw | |
| build.sh | β | 76 B | view raw | |
| run.sh | β | 195 B | view raw | |
| build.log | β | 88 B | view raw | |
| run.log | β | 822 B | view raw | |
| panic.txt | β | 372 B | view raw | |
| env.txt | β | 305 B | view raw | |
| fix.diff | β | 656 B | view raw | |
| VERDICT.md | β | 2.7 KB | β raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-2785 VERDICT
Status: reproduced (unprivileged kernel panic β local DoS, certain). Fix validated (fixed).
Baseline (stock kernel #0, INVARIANTS): panic in ~3 seconds
First attempt design churned without filling the undo table (0 errors in 21M iterations β churn threads in lockstep kept only ~2 entries alive; kept as run.log commentary on iteration counts). Corrected churn (persistent 25 filler entries + dip/re-fill cycles, staggered threads) panicked the guest within ~3s of launch as an unprivileged user:
panic: semop - can't undo undos
cpuid = 3
Trace beginning at frame 0xfffff8011833b888
sys_semop() at sys_semop+0x65d
syscall2() at syscall2+0x11e
Debugger("panic")
(serial console; full capture in panic.txt; guest wedged in DDB β down.)
Root cause (sys/kern/sysv_sem.c:998-1006): the SEM_UNDO rollback loop
panic()s if a reverse-order rollback adjust fails. The "reverse
order guarantees we won't run out of space" argument is valid only
single-threaded: semundo_adjust() (:219-269) keys entries by
(semid, semnum) in the process-shared p->p_sem_undo and takes only
p_token, per call β nothing serializes one thread's undo vector
against sibling threads of the same process. Victim vector
{w,+1,U}{w,-1,U}{x,+1,U}{x,-1,U}: adjust#1 deletes the entry adjust#0
created (net-zero), so if adjust#2 fails on a full table
(un_cnt == semume == 25), the rollback of sops[1] looks up an entry
that is absent by construction while the table is full β EINVAL β
panic. Churn threads only need to re-fill the table inside the
sub-microsecond gap between the victim's adjust#1 and adjust#2.
Fix validation (patched kernel #1: panic β kprintf + break)
Race re-run for 300+ seconds on the patched kernel:
victim : 306184192 iters, 21247 errors (failure path heavily exercised) semop: SEM_UNDO rollback lost a race, pid 860 (dmesg) β¦ 170 occurrences of "lost a race" β¦ (the exact formerly-panicking path) guest: up (baseline: dead in ~3s)
Every one of the 170 race events would have been the panic on the stock kernel. Bad behavior gone: no panic; the condition is logged.
Impact ceiling
Medium β unprivileged, reliable (seconds) local kernel panic = local
DoS. panic() is an intentional kernel halt, so no memory corruption
beyond the DoS itself; a fuller fix would serialize undo vectors
per-process (mutex across the adjust/rollback phase) rather than only
removing the crash.
Artifacts
panic.txt (serial console), run.log (baseline narrative + fixed-run progress), run.fixed.log (patched-kernel run + dmesg count), env.txt, fix.diff (validated in-guest: build RC=0, installkernel RC=0).
Fix verification
fixedPatched kernel (fix.diff, nativekernel RC=0, installkernel RC=0) survived 300s+ of the identical race that killed the stock kernel in 3s; 170 formerly-fatal events logged, guest stayed up.
["run.fixed.log: 'victim : 306184192 iters, 21247 errors' + dmesg count 170 + guest 'up'", 'fix.diff']
Confirmed kernel references
Detail
Evidence (decisive lines)
["panic.txt: 'panic: semop - can't undo undos', sys_semop+0x65d via syscall2", "run.fixed.log: guest up, 21247 failure-path errors, dmesg x170 'semop: SEM_UNDO rollback lost a race, pid 860'", 'VERDICT.md: construction, timeline, baseline vs patched']
PoC changes
Two design iterations: (1) naive +1/-1 churn left 0 persistent entries (0 errors in 21M iters - kept as negative evidence); (2) persistent 25-filler setup + dip/re-fill churn with staggered threads panicked in ~3s.
Verified recommended fix
Replace the rollback panic with a kprintf+break (validated in-guest: no panic across 170 race events); longer term, hold a per-process undo mutex across the adjust/rollback phase.
Verdict
sys_semop()'s SEM_UNDO rollback panic("semop - can't undo undos") (sysv_sem.c:1004-1006) is reachable by an unprivileged user with two sibling threads: the reverse-order rollback 'cannot run out of space' guarantee is single-threaded only, because undo entries (keyed semid/semnum in the process-shared p_sem_undo) are mutated by sibling threads between the victim's per-call p_token sections. Victim vector {w,+1,U}{w,-1,U}{x,+1,U}{x,-1,U} with churn keeping un_cnt==semume==25 makes rollback of sops[1] find an absent entry in a full table -> EINVAL -> panic. Stock INVARIANTS guest panicked in ~3s (panic.txt). Patched kernel (panic->kprintf+break) survived 300s+ with 170 logged race events and 306M victim iterations.
No comments yet.