Missing SEMVMX upper-bound in semop/semexit allows semval overflow, wrap, spurious wakeups, and rollback corruption
| Field | Value |
|---|---|
| ID | DF-0046 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L |
| CWE | CWE-190 Integer Overflow or Wraparound; CWE-682 Incorrect Calculation |
| File | sys/kern/sysv_sem.c |
| Lines | 848-854 (semop positive), 530 (SETVAL), 1139 (semexit) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
SEMVMX (32767) is exported to userland via seminfo, and POSIX/SVID requires
semop to fail with ERANGE when an operation would make semval exceed
SEMVMX, but the kernel never enforces the upper bound. The positive-sem_op
branch (:848-854) does semptr->semval += sopptr->sem_op with no upper-
bound check (the negative branch at :827 has the lower-bound analogue), and
semexit's positive-adjval path (:1139) and SETVAL (:530, intβu_short
truncation) are likewise unchecked. The in-tree comment at :160
("SEMVMX unused - user param") documents that SEMVMX is advisory only.
Consequences: semval (a u_short) wraps past 65535; the wrap-to-0 case
spuriously satisfies the semzcnt wakeup (:834) waking waiters that believe
the semaphore drained (broken mutual exclusion); and once wrapped, the rollback
paths (:886, :1013) operate on the wrapped u_short, leaving semval in a
state unrelated to its pre-call value. No kernel memory corruption was found
(the wrap stays inside the self-contained u_short field) β impact is IPC-state
integrity / local DoS via broken synchronization, not privilege escalation.
Root cause
} else { /* positive sem_op branch */
semptr->semval += sopptr->sem_op; /* :849 no SEMVMX upper-bound check */
if (sopptr->sem_flg & SEM_UNDO)
do_undos = 1;
if (semptr->semncnt > 0)
wakeup(semptr);
}
Compare the negative branch (:827) which has
if (semptr->semval + sopptr->sem_op < 0). semval is u_short (:40). The
same omission is at SETVAL (:530, semptr->semval = real_arg.val) and
semexit (:1139, semptr->semval += adjval).
Threat model & preconditions
- Attacker position: any local user with
SEM_Aon a semaphore set. - Privileges gained or impact: corruption of the set's
semval, including semaphores shared with other principals (other UIDs in the same group, or root-owned setuid helpers sharing the set). The corruption is silent and persistent, and lets a waiter enter a critical section believingsemval==0when its logical value is much larger β a cross-user synchronization/DoS primitive. The rollback wrap can leave a victim's semaphore in an arbitrary state. No path to kernel memory corruption (the wrap stays inside the self-containedu_shortfield). - Required config or capabilities:
SEM_Aon the set; default kernel. - Reachability:
semop(2)with large positive ops;semctl(SETVAL).
Proof of concept
PoC source: findings/poc/DF-0046/sem_wrap.c
Build & run (unprivileged)
cc -o sem_wrap findings/poc/DF-0046/sem_wrap.c ./sem_wrap
Expected output
A wrapped semval after four +32767 ops (no ERANGE), and a non-zero
semval after a failed-and-rolled-back op (should be 0).
Impact
IPC-state integrity / POSIX non-compliance / local DoS via broken SysV semaphore synchronization. No kernel memory corruption. Rated Low.
Recommended fix
Enforce SEMVMX on the positive-op, SETVAL, and semexit paths (the
negative branch already shows the pattern):
--- a/sys/kern/sysv_sem.c
+++ b/sys/kern/sysv_sem.c
@@ -848 +848,5 @@
} else {
+ if (semptr->semval + sopptr->sem_op > seminfo.semvmx) {
+ eval = ERANGE;
+ lwkt_relpooltoken(semptr);
+ goto done;
+ }
semptr->semval += sopptr->sem_op;
(and the analogous check at SETVAL :530 and semexit :1139).
References
sys/kern/sysv_sem.c:848-854β positive-op branch, noSEMVMXcheck.sys/kern/sysv_sem.c:160β in-tree comment "SEMVMX unused - user param".sys/kern/sysv_sem.c:40βsemvalisu_short.- CWE-190 Integer Overflow; CWE-682 Incorrect Calculation.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/sysv_sem.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0046 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| sem_wrap.c | trigger-source | positive-op wrap PoC + wrap-to-0 spurious wait-for-zero release | 4.1 KB | view raw |
| fix_test.c | trigger-source | supplementary test exercising positive-op and SETVAL SEMVMX bounds | 1.6 KB | view raw |
| build.sh | build-script | cc -o sem_wrap sem_wrap.c; cc -o fix_test fix_test.c | 201 B | view raw |
| run.sh | run-script | ./sem_wrap (unprivileged) | 367 B | view raw |
| run.log | run-log | baseline run on unpatched #0 kernel (bug confirmed) | 342 B | view raw |
| fix_run.log | run-log | patched #1 kernel runs (ERANGE at op2 + fix_test all-pass) | 888 B | view raw |
| fix_build.log | build-log | nativekernel build of fix.diff, full output, rc=0 | 5.6 MB | β download |
| fix.diff | suggested-fix | SEMVMX bound at semop(+op) :849, SETVAL :530, semexit :1139 | 1.0 KB | view raw |
| env.txt | environment | uname, cc version, semvmx/semaem sysctls | 234 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, impact, before/after fix validation | 5.9 KB | β raw |
| README.md | readme | how to build/run, expected output on buggy vs fixed kernel | 2.7 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0046 β PoC
sem_wrap.c β semval (u_short) overflow/wrap via the missing SEMVMX
upper-bound in semop/SETVAL/semexit, with wrap-to-0 breaking wait-for-zero
(semop(0)) synchronization semantics.
fix_test.c β supplementary test exercising the positive-op and SETVAL paths
(used to confirm the fix on the patched kernel).
The bug
SEMVMX (32767) is exported to userland via seminfo and POSIX/SVID requires
semop/SETVAL to fail with ERANGE when an operation would make semval
exceed SEMVMX. The DragonFly kernel never enforces the upper bound at any of
the three write sites:
| Path | Citation | Code |
|---|---|---|
| semop (+op) | sys/kern/sysv_sem.c:848-854 |
semptr->semval += sopptr->sem_op; |
| SETVAL | sys/kern/sysv_sem.c:530 |
semptr->semval = real_arg.val; |
| semexit | sys/kern/sysv_sem.c:1139 |
semptr->semval += adjval; |
The negative-sem_op branch (:827) has the lower-bound analogue
(if (semptr->semval + sopptr->sem_op < 0)), and the in-tree comment at :160
admits "SEMVMX unused - user param". semval is u_short (:40), so large
positive ops wrap past 65535.
Consequences (demonstrated by sem_wrap.c):
- POSIX violation β
semvalexceedsSEMVMXwith noERANGEreturned (four+32767ops landsemvalat131068 mod 65536 = 65532). - Wrap-to-0 breaks mutual exclusion β
+32767 + +32767 + +2wrapssemvalto exactly 0 (logical value 65536). A process blocked insemop(semnum=0, op=0)(wait-for-zero,semzcnt) is spuriously released even though the logical semaphore value is large and positive β broken SysV semaphore synchronization.
No kernel memory corruption (semval is a self-contained u_short field);
impact is IPC-state integrity / POSIX non-compliance / local DoS via broken
synchronization. No escalation path (no memory write primitive).
Build & run (unprivileged)
cc -o sem_wrap sem_wrap.c cc -o fix_test fix_test.c # supplementary ./sem_wrap
Expected output
BUG present (unpatched #0 kernel)
[1] semval after four +32767 ops = 65532 (SEMVMX=32767, POSIX would have ERANGE'd at op2)
-> BUG CONFIRMED: semval 65532 > SEMVMX 32767 (no ERANGE)
[2] wait-for-zero child RELEASED after wrap-to-0 (semval logical=65536, u_short=0) -> BUG: spurious release, mutual exclusion broken
FIXED kernel (#1, with fix.diff applied)
sem_wrap: op2 (POSIX says ERANGE; DragonFly allows): Result too large
(fix_test additionally confirms SETVAL also returns ERANGE for val > SEMVMX,
and normal small ops still work β no regression.)
DF-0046 β Verdict
Verdict: REPRODUCED (logic bug / POSIX non-compliance / local DoS). Fix VALIDATED.
Summary
The DragonFlyBSD SysV semaphore implementation never enforces the SEMVMX
(32767) upper bound that POSIX/SVID requires. semval is a u_short
(sys/kern/sysv_sem.c:40); three write sites mutate it without any check
against SEMVMX:
| Path | Citation | Defect |
|---|---|---|
semop +op |
sys/kern/sysv_sem.c:849 |
semptr->semval += sopptr->sem_op; β no bound |
SETVAL |
sys/kern/sysv_sem.c:530 |
semptr->semval = real_arg.val; β intβu_short trunc, no bound |
semexit |
sys/kern/sysv_sem.c:1139 |
semptr->semval += adjval; β no bound |
The negative-sem_op branch (sys/kern/sysv_sem.c:827) does have the
lower-bound analogue (if (semptr->semval + sopptr->sem_op < 0)), confirming
the upper-bound omission is the bug, not an intentional design. The in-tree
comment at sys/kern/sysv_sem.c:160 itself admits "SEMVMX unused - user param".
Mechanism (trigger β primitive β effect)
- Trigger β unprivileged user calls
semget(IPC_PRIVATE, ...)thensemopwith large positivesem_opvalues (each β€ 32767, theshortrange). No privilege check beyond the set's IPC mode bits. - Primitive β the positive-op branch at
:849doessemptr->semval += sopptr->sem_opwith noSEMVMXcheck. Four+32767ops accumulate0 + 4Γ32767 = 131068, which wraps theu_shortto131068 mod 65536 = 65532. The wrap is silent (noERANGE). - Effect (demonstrated two ways):
- POSIX violation:
semval = 65532 > SEMVMX (32767)with noERANGE. A POSIX-compliant kernel would have returnedERANGEat the second op. - Wrap-to-0 breaks mutual exclusion:+32767 + +32767 + +2 = 65536wrapssemvalto exactly 0 (logical value 65536). A process blocked insemop(0)(wait-for-zero,semzcntwaiter) is spuriously released β the PoC's forked child exits with code 42 (released) where it should have blocked indefinitely. This breaks SysV semaphore mutual exclusion for any consumer that relies on the zero state.
Impact
IPC-state integrity / POSIX non-compliance / local DoS via broken SysV
semaphore synchronization. No kernel memory corruption β the wrap stays
inside the self-contained u_short field; there is no write primitive, no
adjacent-object corruption, no escalation path. Rated Low (matches the
finding's severity). The wrap-to-0 spurious-release is the most
security-relevant consequence (broken cross-process mutual exclusion), but
it requires a victim process sharing the semaphore set and using
wait-for-zero semantics.
Exploit chain
none β this is a pure logic/integer bug with no memory-corruption
primitive. semval is a self-contained u_short; the wrap does not corrupt
adjacent kernel memory. There is no write primitive to convert to control
flow, no slab grooming, no function pointer to hijack. The realistic impact
ceiling is the wrap-to-0 spurious wakeup demonstrated in observation [2],
which is a synchronization-integrity / local-DoS issue.
PoC changes
The original sem_wrap.c made a "rollback leaves non-zero" claim that does
not hold (u_short arithmetic is closed under mod 65536, so symmetric
add/subtract in the rollback returns to the original value β the rollback
correctly lands at 0). I replaced that false claim with a sharper, more
security-relevant demonstration: the wrap-to-0 spurious release of a
wait-for-zero child, which is the actual broken-mutual-exclusion primitive.
Also added a supplementary fix_test.c that exercises both the positive-op
and SETVAL paths for use on the patched kernel.
Fix validation (Phase 8)
Authored fix.diff adding SEMVMX enforcement at all three write sites:
- positive-op (
:849):if ((int)semptr->semval + sopptr->sem_op > seminfo.semvmx) { eval = ERANGE; lwkt_relpooltoken(semptr); goto done; }β matches the finding's proposed pattern and the existing negative-branch style. - SETVAL (
:530):if (real_arg.val < 0 || real_arg.val > seminfo.semvmx) { eval = ERANGE; break; }β also catches theintβu_shorttruncation case (e.g.val = 70000would have stored70000 & 0xffff = 4464). - semexit (
:1139): clamp toSEMVMX(semexit cannot fail; the process is exiting). Defense-in-depth β once semop and SETVAL are bounded, semexit's input state is always β€ SEMVMX, so the clamp is a backstop.
Before/after (clean comparison on the same guest):
| Kernel | sem_wrap observation [1] (4Γ+32767) |
sem_wrap observation [2] (wrap-to-0) |
fix_test SETVAL =SEMVMX+1 |
|---|---|---|---|
#0 unpatched baseline |
semval = 65532, no ERANGE (BUG) |
child RELEASED (broken mutex) | n/a (would store 4464) |
#1 patched (fix.diff) |
op2 returns ERANGE (FIXED) |
unreachable (op2 fails first) | returns ERANGE (FIXED) |
Patched kernel: DragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 23:47:04 UTC 2026,
sha256(/boot/kernel/kernel) = 425bca4973a730ceafb4cda4a71b64cd26e10c45d41e26eb55e26e171243619e.
Smoke test (normal -1 op) still works (semval=32766), no regression.
Recommended fix
fix.diff supersedes the finding markdown's proposal (which only patched
the positive-op branch). The verified fix covers all three write sites
(positive-op, SETVAL, semexit) for a complete bound enforcement. The finding
markdown's ## Recommended fix only showed the positive-op hunk and noted
"and the analogous check at SETVAL :530 and semexit :1139" β fix.diff
implements exactly that, with semexit using a clamp (not ERANGE) since it
cannot fail.
Fix verification
fixedVALIDATED: baseline semval=65532 no ERANGE + wait-for-zero release; patched ERANGE at op2 + SETVAL; smoke -1 OK.
BEFORE #0: semval=65532 (>SEMVMX), no ERANGE, broken mutex. AFTER #1: ERANGE at op2, SETVAL ERANGE, -1 OK (no regression).
Confirmed kernel references
Detail
Exploit chain
none -- pure logic/integer bug. semval is self-contained u_short; no write primitive, no adjacent memory corruption, no slab grooming. IPC-state integrity / local-DoS.
Evidence (decisive lines)
baseline #0: semval=65532 (>SEMVMX=32767, no ERANGE); wait-for-zero child released after wrap-to-0 (broken mutex). patched #1: ERANGE at op2; SETVAL=SEMVMX+1 -> ERANGE; smoke -1 OK (semval=32766, no regression).
PoC changes
Rewrote sem_wrap.c: replaced flawed rollback claim with wrap-to-0 spurious wait-for-zero release demo. Added fix_test.c (positive-op + SETVAL SEMVMX bounds). Added build.sh, run.sh, VERDICT.md, manifest.json, fix.diff, full logs.
Verified recommended fix
fix.diff enforces SEMVMX at all three write sites: (1) semop positive-op at :849 (ERANGE if >SEMVMX); (2) SETVAL at :530 (ERANGE if >SEMVMX, also catches int->u_short trunc); (3) semexit at :1139 (clamp to SEMVMX). Supersedes finding markdown (adds SETVAL + semexit). Full git-apply-able diff in findings/poc/DF-0046/fix.diff.
Verdict
REPRODUCED. sysv_sem.c never enforces SEMVMX (32767) upper bound. Positive-op at :849 does semval += sem_op with no check. Four +32767 ops land at 65532 with no ERANGE. Security-relevant consequence: +32767 +32767 +2 wraps semval to 0, spuriously releasing a wait-for-zero child (broken mutual exclusion). No memory corruption.
No comments yet.