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

size_t underflow in exec_shell_imgact when argv[0] longer than interpreter+fname -> kernel panic

Field Value
ID DF-0243
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
CWE CWE-191 Integer Underflow
File sys/kern/imgact_shell.c
Lines 117-129
Area kern
Confidence certain
Discovered 2026-06-30
Reported pending

Summary

When executing a #! script, exec_shell_imgact() adjusts the argument buffer by computing offset -= length (:126) where both are size_t (unsigned). offset is the total size of interpreter tokens + script filename + NULs (~15 bytes for /bin/sh /tmp/s). length is strlen(argv[0]) + 1. The user controls argv[0] independently of the script path via execve(). When argv[0] is longer than the interpreter path + filename, offset < length and the unsigned subtraction wraps to ~SIZE_MAX, corrupting begin_envv, endp, and space (:127-129). This causes an immediate kernel panic on the subsequent pointer dereference. Triggerable by any unprivileged local user.

Root cause

sys/kern/imgact_shell.c:117-129:

offset += strlen(imgp->args->fname) + 1;     // :117 β€” add fname
length = strlen(imgp->args->begin_argv) + 1; // :118 β€” argv[0] length

if (offset > imgp->args->space + length)     // :120 β€” E2BIG check
    return (E2BIG);

bcopy(begin_argv + length, begin_argv + offset,
      endp - (begin_argv + length));          // :123-124

offset -= length;                             // :126 β€” UNDERFLOW
imgp->args->begin_envv += offset;             // :127 β€” CORRUPTED
imgp->args->endp += offset;                   // :128 β€” CORRUPTED
imgp->args->space -= offset;                  // :129 β€” CORRUPTED

Example with #!/bin/sh script at /tmp/s and argv[0] = 256 'A's: - offset = 8 (interpreter) + 7 (fname) = 15 - length = 257 - offset -= length = 15 - 257 β†’ wraps to SIZE_MAX - 241 - begin_envv += SIZE_MAX-241 β†’ wild pointer - endp += SIZE_MAX-241 β†’ wild pointer

The E2BIG guard at :120 cannot prevent this: 15 > 262144 + 257 is false, so the check passes.

Threat model & preconditions

  • Attacker position: Any unprivileged local user.
  • Impact: Kernel panic (reliable local DoS). Potential kernel memory corruption if the wrapped pointers land on mapped memory during the subsequent exec copy operations.
  • Required config: Default kernel. Any filesystem with executable scripts.
  • Reachability: execve("/path/to/script", [long_argv0], envp).

Proof of concept

PoC source: findings/poc/DF-0243/

#include <unistd.h>
#include <string.h>

int main(void) {
    char ao[256];
    memset(ao, 'A', sizeof(ao)-1);
    ao[sizeof(ao)-1] = '\0';
    char *argv[] = { ao, NULL };
    char *envp[] = { NULL };
    /* /tmp/s contains: #!/bin/sh\necho hi */
    execve("/tmp/s", argv, envp);
    _exit(1);
}

Expected output

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffff...  (corrupted begin_envv/endp)
panic: page fault

Handle the shrink case (offset < length) explicitly:

--- a/sys/kern/imgact_shell.c
+++ b/sys/kern/imgact_shell.c
@@ -123,11 +123,19 @@
    bcopy(imgp->args->begin_argv + length, imgp->args->begin_argv + offset,
        imgp->args->endp - (imgp->args->begin_argv + length));

-   offset -= length;       /* calculate actual adjustment */
-   imgp->args->begin_envv += offset;
-   imgp->args->endp += offset;
-   imgp->args->space -= offset;
+   if (offset >= length) {
+       size_t net = offset - length;
+       imgp->args->begin_envv += net;
+       imgp->args->endp += net;
+       imgp->args->space -= (int)net;
+   } else {
+       size_t net = length - offset;
+       imgp->args->begin_envv -= net;
+       imgp->args->endp -= net;
+       imgp->args->space += (int)net;
+   }

Timeline

  • 2026-06-30 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0243 Β· 19 files
FileTypeDescriptionSize
trigger.c trigger-source argv[0]-length sweep trigger (empty envp) 2.1 KB view raw
trigger2.c trigger-source large-environment trigger stressing the bcopy at imgact_shell.c:123-124 3.0 KB view raw
trigger3.c trigger-source NEW (verify run): max-delta (argv0=ARG_MAX) + nonexistent-interp + 8x75 parallel hammer; 700 underflow execs 4.3 KB view raw
setup.sh setup-script creates /tmp/df0243_s (#!/bin/sh) 225 B view raw
setup2.sh setup-script creates /tmp/df0243_s2 (sentinel-checking #!/bin/sh) 628 B view raw
math_proof.c proof-source userspace proof: size_t wrap == intended signed op for begin_envv/endp/space 3.0 KB view raw
build.sh build-script exact userspace build commands 104 B view raw
run.sh run-script exact run commands (sweep + env stress) 103 B view raw
build.log build-log this run's userspace build output (BUILD_OK, TRIGGER3_BUILT) 420 B view raw
run.log run-log DECISIVE baseline (#0 unpatched): math proof + sweep 256..262140 + env stress + trigger3 all OK, no panic 4.3 KB view raw
fix_run.log run-log single-fix kernel #1 run: byte-identical to run.log (diff empty) 4.3 KB view raw
fix_build.log build-log this run's kernel build/install evidence (rc=0, #1 Oct 12 06:41:45) 1.4 KB view raw
run.log.pass1 run-log pass-1 baseline run (preserved) 3.0 KB ↓ download
fix_run.log.pass1 run-log pass-1 fix-kernel run (preserved) 1.8 KB ↓ download
fix_build.log.pass1 build-log pass-1 full 35k-line kernel build transcript (preserved) 5.6 MB ↓ download
env.txt env both kernel unames, cc 8.3, kern.argmax=262144, end state (reset with-src) 1013 B view raw
fix.diff fix-diff defense-in-depth grow/shrink rewrite of imgact_shell.c:126-129 (validated: LOGS_IDENTICAL) 1.2 KB view raw
VERDICT.md verdict-narrative full narrative: mechanism walkthrough, first-party evidence, fix validation 3.6 KB ↓ raw
verdict.json verdict-machine machine verdict for persist_poc.py / persist_fix.py 4.8 KB view raw
VERDICT.md verdict-narrative full narrative: mechanism walkthrough, first-party evidence, fix validation
↓ download raw

DF-0243 VERDICT -- not_reproduced (impact: none; confidence: certain)

Bottom line

FALSE POSITIVE, independently confirmed. The size_t subtraction at sys/kern/imgact_shell.c:126 does wrap when argv[0] is longer than interpreter tokens + fname, but the wrap is harmless: begin_envv/endp are char so adding (2^64 - d) computes exactly begin_envv-d / endp-d (the intended post-shrink locations, always in-buffer since begin_envv >= begin_argv+length), and space (int) truncates (size_t)space+offset back to the exact intended space+d (bounded by 2ARG_MAX, far inside int range). The finding stopped at 'SIZE_MAX-241' and never evaluated the subsequent pointer adds. On the unpatched kernel (#0 Jul 2 snapshot), 700+ underflow-branch execs -- argv[0] swept 256..262140, argv[0]=262144 at the exact copyinstr bound with offset=15 (max delta d~262129), nonexistent-interpreter #!/a variants, large-env bcopy stress with sentinel verification, and 8x75 parallel execs -- all ran the scripts normally: no panic, no signal, no dmesg warnings, guest stayed up. The claimed kernel panic does not exist; impact none.

Reproduce

build: cd findings/poc/DF-0243 && ./build.sh && cc -O2 -Wall -o trigger3 trigger3.c
run:   ./run.sh && ./trigger3

Evidence in this pack

run.log (decisive baseline, unpatched #0): math_proof 3x ALL MATCH; 6x DF0243_NO_PANIC sweep; 4x DF0243_ENV_OK sentinel_intact; trigger3 phases (a)-(d) all OK ending 'TRIGGER3_OK: 700 underflow execs, max delta, no crash no signal'; zero CHILD_KILLED. fix_run.log (single-fix kernel #1, this run's build): byte-identical to run.log (diff empty). fix.diff applied to guest /usr/src, nativekernel rc=0, booted #1 Mon Oct 12 06:41:45 UTC 2026. Guest reset with-src afterwards (pristine #0 verified).

Kernel references

Fix validation (A/B)

Optional hardening only: rewrite imgact_shell.c:126-129 as an explicit grow/shrink branch (see fix.diff); no security fix required.

Baseline vs post-fix behavior is in run.log / run_fixed*.log (userspace) or panic.txt/hang.txt vs run_fixed_kernel.log (kernel); the identical rig ran both sides.

poc_changes

Added trigger3.c (new): max-delta execs with argv[0]=262144 (the exact ARG_MAX/copyinstr bound) + interp offset=15; nonexistent-interpreter #!/a variant exercising imgact-internal writes alone; large-env + near-max argv[0]; 8x75 parallel hammer. First draft of phase (c) mis-sized argv[0] (262144 + 80 env vars cannot fit ARG_MAX; exec fails EFAULT in exec_copyin_args before imgact) -- fixed by sizing argv0=215024. Re-ran pass-1 trigger/trigger2/math_proof verbatim. No changes needed to pass-1 sources or fix.diff.

Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 Attempts: 3 Runtime: ~1450s

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

No security fix exists to validate: the claimed bug never manifests (baseline_reproduced=0 -- nothing to reproduce; the finding is a false positive). The defense-in-depth fix.diff was nevertheless fully validated this run: applies cleanly, nativekernel build rc=0 (completed Oct 12 06:44:49 UTC), installkernel rc=0, boots as #1, and the entire suite output is byte-identical to the unpatched kernel (diff run.log fix_run.log empty) -- zero behavioral change by design. fix_status recorded as not_testable because persist_fix.py's schema has no not_applicable value.

fix_build.log (BUILD_RC=0, INSTALL_RC=0, kern.version #1 Mon Oct 12 06:41:45 UTC 2026); fix_run.log == run.log (LOGS_IDENTICAL); fix_build.log.pass1 preserves the full 35k-line build transcript of the identical patch.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Oct 12 06:41:45 UTC 2026

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (decisive baseline, unpatched #0): math_proof 3x ALL MATCH; 6x DF0243_NO_PANIC sweep; 4x DF0243_ENV_OK sentinel_intact; trigger3 phases (a)-(d) all OK ending 'TRIGGER3_OK: 700 underflow execs, max delta, no crash no signal'; zero CHILD_KILLED. fix_run.log (single-fix kernel #1, this run's build): byte-identical to run.log (diff empty). fix.diff applied to guest /usr/src, nativekernel rc=0, booted #1 Mon Oct 12 06:41:45 UTC 2026. Guest reset with-src afterwards (pristine #0 verified).

PoC changes

Added trigger3.c (new): max-delta execs with argv[0]=262144 (the exact ARG_MAX/copyinstr bound) + interp offset=15; nonexistent-interpreter #!/a variant exercising imgact-internal writes alone; large-env + near-max argv[0]; 8x75 parallel hammer. First draft of phase (c) mis-sized argv[0] (262144 + 80 env vars cannot fit ARG_MAX; exec fails EFAULT in exec_copyin_args before imgact) -- fixed by sizing argv0=215024. Re-ran pass-1 trigger/trigger2/math_proof verbatim. No changes needed to pass-1 sources or fix.diff.

Verified recommended fix

Optional hardening only: rewrite imgact_shell.c:126-129 as an explicit grow/shrink branch (see fix.diff); no security fix required.

Verdict

FALSE POSITIVE, independently confirmed. The size_t subtraction at sys/kern/imgact_shell.c:126 does wrap when argv[0] is longer than interpreter tokens + fname, but the wrap is harmless: begin_envv/endp are char so adding (2^64 - d) computes exactly begin_envv-d / endp-d (the intended post-shrink locations, always in-buffer since begin_envv >= begin_argv+length), and space (int) truncates (size_t)space+offset back to the exact intended space+d (bounded by 2ARG_MAX, far inside int range). The finding stopped at 'SIZE_MAX-241' and never evaluated the subsequent pointer adds. On the unpatched kernel (#0 Jul 2 snapshot), 700+ underflow-branch execs -- argv[0] swept 256..262140, argv[0]=262144 at the exact copyinstr bound with offset=15 (max delta d~262129), nonexistent-interpreter #!/a variants, large-env bcopy stress with sentinel verification, and 8x75 parallel execs -- all ran the scripts normally: no panic, no signal, no dmesg warnings, guest stayed up. The claimed kernel panic does not exist; impact none.