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

Stack buffer overflow via unbounded ksprintf into psbuf[512] in /proc/<pid>/rlimit

Field Value
ID DF-0935
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-121 Stack-based Buffer Overflow
File sys/vfs/procfs/procfs_rlimit.c
Lines 64, 77, 88, 90, 99, 101, 106
Area vfs
Confidence likely
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

procfs_dorlimit formats all 12 rlimit entries into a fixed 512-byte stack buffer (psbuf[512]) using unbounded ksprintf() calls. The theoretical maximum output of the format strings is 12 * (max_ident(9) + 1 + max_digits(19) + 1 + max_digits(19) + 1) = 600 bytes, which exceeds the buffer by 88 bytes. The 512-byte size (marked "XXX - conservative" at line 64) holds for current default kernel caps, but the code uses the unsafe ksprintf (which writes via *d++=cc with no bounds, subr_prf.c:549) instead of bounded ksnprintf like the sibling procfs_dostatus (procfs_status.c:99-101). If kernel caps are relaxed via loader tunables (kern.maxdsiz, kern.maxssiz) the overflow corrupts the kernel stack frame.

Root cause

The function declares char psbuf[512]; /* XXX - conservative */ at procfs_rlimit.c:64 and formats into it with unbounded ksprintf:

ps += ksprintf(ps, "%s ", rlimit_ident[i]);               /* :77 */
ps += ksprintf(ps, "-1 ");       /  ps += ksprintf(ps, "%llu ", ...);  /* :88, :90 */
ps += ksprintf(ps, "-1\n");      /  ps += ksprintf(ps, "%llu\n", ...); /* :99, :101 */

ksprintf (subr_prf.c:411-422) is unbounded β€” it calls kvcprintf(cfmt, NULL, buf, ap) and the NULL callback makes PCHAR expand to *d++=cc; retval++ (subr_prf.c:549) with no length check.

Per-entry maximum output is rlimit_ident[i] (up to 9 chars for "posixlock") + 1 space + up to 19 decimal digits for rlim_cur (RLIM_INFINITY-1 = 9223372036854775806) + 1 space + up to 19 digits for rlim_max + 1 newline = 50 bytes. Across RLIM_NLIMITS=12 entries (sys/sys/resource.h:122) this is 600 bytes; psbuf is only 512.

The neighboring procfs_dostatus (procfs_status.c:56-61, 91-167) demonstrates the correct pattern: it uses ksnprintf(ps, psbuf + sizeof(psbuf) - ps, ...) with a DOCHECK macro that goto bailout on overflow. procfs_dorlimit was not written that way.

Threat model & preconditions

  • Attacker position: Local unprivileged user. The attacker controls their own process's rlimits via setrlimit(2).
  • Privileges gained or impact: Stack corruption (saved RBP, return address) β†’ kernel panic or potential kernel-RCE primitive.
  • Required config or capabilities: kern_setrlimit (kern_plimit.c:268-385) does NOT cap values for RLIMIT_CPU, RLIMIT_FSIZE, RLIMIT_CORE, RLIMIT_RSS, RLIMIT_SBSIZE, or RLIMIT_VMEM. An unprivileged user can set both rlim_cur and rlim_max to RLIM_INFINITY-1 (19 decimal digits) for these six resources. To trigger the overflow, the administrator must raise the boot-time tunables kern.maxdsiz and/or kern.maxssiz (subr_param.c:221-226) so the capped resources can also store 19-digit values; in that non-default config, the overflow corrupts the kernel stack of the reading LWP.
  • Reachability: cat /proc/<attacker-pid>/rlimit (or any /proc/<pid>/rlimit since the file is world-readable, mode 0444).

Proof of concept

PoC source: findings/poc/DF-0935/

Build & run

cc -o dfpoc-rlimit-overflow dfpoc-rlimit-overflow.c
./dfpoc-rlimit-overflow

On default config: prints "read ~270 bytes" (no overflow).

On a system with kern.maxdsiz="9223372036854775806" and kern.maxssiz="9223372036854775806" set in /boot/loader.conf: the read will overflow psbuf[512] and the kernel will panic on return (corrupted saved RBP/retaddr) or, with a slab-grooming payload, redirect control flow.

Expected output

On the non-default config:

Fatal kernel trap ...
...
procfs_dorlimit(...) at procfs_dorlimit+0x...

Or, with INVARIANTS, an explicit stack-corruption diagnostic.

Impact

Kernel stack corruption β†’ panic (reliable DoS on the non-default config). Potential kernel-RCE if the saved return address can be steered to a controlled gadget (speculative; requires heap/slab grooming not proven here).

On the default kernel configuration, total output stays under 512 bytes (the six uncapped resources display as "-1" when set to RLIM_INFINITY, not as 19-digit numbers; only RLIM_INFINITY-1 expands to 19 digits, and the capped resources stay short), so the overflow does not trigger.

Use bounded ksnprintf with explicit remaining-length accounting, mirroring procfs_dostatus:

--- a/sys/vfs/procfs/procfs_rlimit.c
+++ b/sys/vfs/procfs/procfs_rlimit.c
@@ -61,12 +61,25 @@
 {
    struct proc *p = lp->lwp_proc;
    size_t xlen;
+   size_t remain;
    char *ps;
    int error;
    int i;
    char psbuf[512];        /* XXX - conservative */
+   int n;

    if (uio->uio_rw != UIO_READ)
        return (EOPNOTSUPP);

    ps = psbuf;
+   remain = sizeof(psbuf);

    for (i = 0; i < RLIM_NLIMITS; i++) {
+       if (remain < 4) { error = ENOMEM; goto bailout; }
+       n = ksnprintf(ps, remain, "%s ", rlimit_ident[i]);
+       if (n < 0 || (size_t)n >= remain) { error = ENOMEM; goto bailout; }
+       ps += n; remain -= n;
        /*
         * current limit
         */
        if (p->p_rlimit[i].rlim_cur == RLIM_INFINITY) {
+           n = ksnprintf(ps, remain, "-1 ");
+           if (n < 0 || (size_t)n >= remain) { error = ENOMEM; goto bailout; }
+           ps += n; remain -= n;
        } else {
-           ps += ksprintf(ps, "%llu ",
-               (unsigned long long)p->p_rlimit[i].rlim_cur);
+           n = ksnprintf(ps, remain, "%llu ",
+               (unsigned long long)p->p_rlimit[i].rlim_cur);
+           if (n < 0 || (size_t)n >= remain) { error = ENOMEM; goto bailout; }
+           ps += n; remain -= n;
        }
        /*
         * maximum limit
         */
        if (p->p_rlimit[i].rlim_max == RLIM_INFINITY) {
+           n = ksnprintf(ps, remain, "-1\n");
+           if (n < 0 || (size_t)n >= remain) { error = ENOMEM; goto bailout; }
+           ps += n; remain -= n;
        } else {
-           ps += ksprintf(ps, "%llu\n",
-               (unsigned long long)p->p_rlimit[i].rlim_max);
+           n = ksnprintf(ps, remain, "%llu\n",
+               (unsigned long long)p->p_rlimit[i].rlim_max);
+           if (n < 0 || (size_t)n >= remain) { error = ENOMEM; goto bailout; }
+           ps += n; remain -= n;
        }
    }

    xlen = ps - psbuf;
    error = uiomove_frombuf(psbuf, xlen, uio);
+bailout:
    return (error);
 }

Alternatively, enlarge the buffer to safely hold the theoretical maximum (char psbuf[768] β€” max 600 + NUL + margin). The ksnprintf fix is preferred because it is robust to future changes that might lengthen rlimit_ident[] or add new rlimits, and because it makes the function fail safe (ENOMEM) instead of corrupting memory.

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0935 Β· 13 files
FileTypeDescriptionSize
dfpoc-rlimit-overflow.c trigger-source original PoC (unchanged) β€” prints 'no overflow on this config' 2.0 KB view raw
maxcalc.c trigger-source computes max procfs_dorlimit output across 3 cap scenarios; all < 512 2.4 KB view raw
maxinflate.c trigger-source maximally inflates all 12 rlimits as unprivileged user; reads /proc/self/rlimit 1.4 KB view raw
build.sh build-script builds all three PoC binaries 65 B view raw
run.sh run-script runs all three disproofs 89 B view raw
build.log build-log final successful build, full output 195 B view raw
run.log run-log decisive run, full output 1.6 KB view raw
env.txt environment uname, cc version, cap values, constants 331 B view raw
VERDICT.md verdict full narrative with path:line citations 7.2 KB ↓ raw
README.md readme human-facing summary 2.3 KB ↓ raw
fix.diff suggested-fix DEFENSE-IN-DEPTH ONLY (ksprintf->ksnprintf); not a security fix; bug not exploitable 1.8 KB view 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
README.md readme human-facing summary
↓ download raw

DF-0935 β€” PoC evidence pack

Verdict: FALSE POSITIVE (overflow cannot trigger). See VERDICT.md for the full analysis.

What the finding claims

procfs_dorlimit formats 12 rlimit entries into char psbuf[512] using unbounded ksprintf. The finding claims total output can reach 600 bytes and overflow the buffer by 88.

What actually happens

  • The unbounded ksprintf is real, but the data flowing into it is bounded.
  • 3 of the 12 rlimits (RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_POSIXLOCKS) are clamped by int-typed sysctls (maxfilesperproc, maxprocperuid, maxposixlocksperuid) inside kern_setrlimit (kern_plimit.c:358-380). int caps cannot hold 19-digit values β€” only 10-digit values (max 2147483647).
  • The finding's "12 Γ— 19-digit = 600 bytes" math wrongly assumes all 12 resources can hold 19-digit values.
  • The real maximum (under the most extreme admin config: every achievable cap raised to its absolute max) is 499 bytes β€” under 512.

Files

File Purpose
dfpoc-rlimit-overflow.c Original PoC (unchanged). Prints "no overflow".
maxcalc.c Computes the exact max output across 3 cap scenarios. All < 512.
maxinflate.c Maximally inflates all 12 rlimits as unprivileged user, reads /proc/self/rlimit. Confirms 431 bytes on default config.
build.sh / run.sh Repro scripts.
build.log / run.log Full untrimmed build/run output.
env.txt Guest environment.
VERDICT.md Full narrative verdict with path:line citations.
fix.diff Defense-in-depth hardening (ksprintf β†’ ksnprintf), NOT a security fix.
manifest.json Machine-readable catalog.

Reproduce

ssh dfbsd-maxx   # unprivileged user (uid 1001)
cd poc/DF-0935   # after copying this folder to the guest
./build.sh && ./run.sh

Expected: Scenario A (every achievable cap maxed): 499 bytes -> UNDER 512, read 431 bytes, OVERFLOW? no. No panic, no kernel message, guest stays up.

Fix status

not_applicable for the security claim β€” there is no misbehavior to fix. fix.diff is hardening only (mirrors the bounded ksnprintf pattern already used by the sibling procfs_dostatus in procfs_status.c). Per procedure, no Phase 8 kernel build is performed because status = not_reproduced.

VERDICT.md verdict full narrative with path:line citations
↓ download raw

DF-0935 β€” Verdict: FALSE POSITIVE (overflow cannot trigger)

One-line verdict

NOT REPRODUCED β€” false positive. The unbounded ksprintf is real, but the cited security impact (stack overflow) is mathematically unreachable: the maximum possible output of procfs_dorlimit is 499 bytes, below the 512-byte psbuf, under every achievable configuration including the most extreme admin override. The finding's "600-byte maximum" arithmetic is wrong.

Mechanism the finding claims

procfs_dorlimit (sys/vfs/procfs/procfs_rlimit.c:55-110) formats 12 rlimit entries into char psbuf[512] (line 64) via unbounded ksprintf (lines 77, 88, 90, 99, 101). ksprintf is genuinely unbounded (sys/kern/subr_prf.c:549 expands PCHAR to *d++=cc with no length check). The finding claims total output can reach 600 bytes (12 Γ— 50), overflow psbuf[512] by 88 bytes, and corrupt the kernel stack frame.

Why the impact does NOT manifest (rigorous bound)

The finding's math is wrong on two independent counts

Count 1 β€” only 9 of the 12 rlimits can ever hold 19-digit values, not 12. The finding's "600 bytes" assumes every entry's rlim_cur/rlim_max can be RLIM_INFINITY-1 (9223372036854775806, 19 decimal digits). But three of the twelve resources are clamped by int (not u_quad_t) sysctls inside kern_setrlimit (sys/kern/kern_plimit.c:358-380):

Resource Cap variable Type Max value (digits)
RLIMIT_NOFILE (8) maxfilesperproc int 2147483647 (10)
RLIMIT_NPROC (7) maxprocperuid int 2147483647 (10)
RLIMIT_POSIXLOCKS (11) maxposixlocksperuid int 2147483647 (10)

int caps cannot store 19-digit values. kern_setrlimit clamps both rlim_cur and rlim_max to the cap value (kern_plimit.c:359-360, 366-367, 376-377), so even when an attacker calls setrlimit(RLIMIT_NOFILE, {RLIM_INFINITY-1, RLIM_INFINITY-1}), the stored value is at most INT_MAX (10 digits).

Count 2 β€” even raising kern.maxdsiz / kern.maxssiz (the finding's stated precondition) leaves 13 bytes of headroom. With every achievable cap raised to its maximum (maxdsiz and maxssiz to RLIM_INFINITY-1; the three int caps to INT_MAX), and the user setting every uncapped resource to RLIM_INFINITY-1, the maximum possible output is:

       ident  cur          max          bytes
[ 0] cpu       19 digits    19 digits    44
[ 1] fsize     19 digits    19 digits    46
[ 2] data      19 digits    19 digits    45   (maxdsiz raised)
[ 3] stack     19 digits    19 digits    46   (maxssiz raised)
[ 4] core      19 digits    19 digits    45
[ 5] rss       19 digits    19 digits    44
[ 6] memlock   19 digits    19 digits    48
[ 7] nproc     10 digits    10 digits    28   (INT_MAX clamp)
[ 8] nofile    10 digits    10 digits    29   (INT_MAX clamp)
[ 9] sbsize    19 digits    19 digits    47
[10] vmem      19 digits    19 digits    45
[11] posixlock 10 digits    10 digits    32   (INT_MAX clamp)
                                         ----
                              Scenario A total: 499 bytes  (UNDER 512)

The cited overflow (88 bytes past 512) cannot occur. The realistic ceiling is 499 bytes β€” 13 bytes of headroom remain even in the most contrived admin configuration. On the default kernel, the realistic maximum is 431 bytes (user maximally inflates; maxdsiz/maxssiz stay at defaults of 32 GB / 512 MB).

Per-scenario output (computed on-guest, see maxcalc.c)

Scenario Total bytes vs psbuf[512]
Default config, default rlimits (/proc/self/rlimit) 186 safe
Default config, user maximally inflates (the PoC's path) 431 safe
maxdsiz+maxssiz raised to RLIM_INFINITY-1, others default 467 safe
Every cap maxed (maxdsiz, maxssiz β†’ 2^63-1; 3 int caps β†’ INT_MAX) 499 safe (13-byte headroom)
Finding's claimed theoretical max 600 unreachable

Empirical confirmation

  • Original PoC: ./dfpoc-rlimit-overflow β†’ read 397 bytes from /proc/870/rlimit, no overflow on this config. (Full run.log.)
  • Maximally-inflate harness (sets all 12 rlimits to RLIM_INFINITY-1, reads /proc/self/rlimit): read 431 bytes. No overflow. (run.log.)

Why the finding's arithmetic was off

The reviewer applied 12 Γ— (max_ident + 1 + max_digits + 1 + max_digits + 1) uniformly across all twelve entries, treating max_digits as 19 for every resource. That assumption only holds for the seven resources NOT handled by the switch in kern_setrlimit (kern_plimit.c:307-381) plus the two u_quad_t-capped ones (RLIMIT_DATA, RLIMIT_STACK), whose caps CAN be raised to RLIM_INFINITY-1 via the loader tunables. It does not hold for RLIMIT_NOFILE/RLIMIT_NPROC/RLIMIT_POSIXLOCKS, whose caps are int sysctls (subr_param.c:76, 80, 82) β€” bounded at INT_MAX = 2147483647.

Defense-in-depth note (still worth fixing, but not a security bug)

The unbounded ksprintf into a fixed stack buffer is fragile: it relies on an arithmetic invariant across three independent int sysctls and two u_quad_t tunables, plus RLIM_NLIMITS and rlim_t. If any of those changed (e.g. a new rlimit added, int widened, or a new cap variable introduced), the bound could silently break. The sibling procfs_dostatus uses bounded ksnprintf with a DOCHECK macro for exactly this reason (procfs_status.c:99-167).

A defense-in-depth hardening fix (convert to ksnprintf with explicit remaining-length accounting, mirroring procfs_dostatus) is provided in fix.diff. It is hardening, not a security fix β€” the cited bug cannot manifest on any current or achievable configuration.

PoC changes

  • dfpoc-rlimit-overflow.c β€” unchanged (compiles cleanly, runs cleanly, prints the expected "no overflow" result; this is itself the disproof).
  • Added maxcalc.c β€” a small program that computes the exact maximum procfs_dorlimit output under three cap scenarios (default / maxdsiz+maxssiz raised / all caps maxed). All three stay under 512.
  • Added maxinflate.c β€” a harness that maximally inflates all 12 rlimits as an unprivileged user and reads /proc/self/rlimit. Confirms 431 bytes.

Conclusion

  • status = not_reproduced
  • impact = none (no overflow, no panic, no leak)
  • confidence = certain (math is exact; confirmed on-guest)
  • fix_status = not_applicable for the security claim; an optional defense-in-depth hardening diff is provided but does not need kernel build validation because there is no observable "before" misbehavior to make "gone".

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

not_applicable -- false positive. No security misbehavior to fix.

n/a

Confirmed kernel references

Detail

Exploit chain

none -- refuted. No overflow primitive exists.

Evidence (decisive lines)

3 scenarios: A=499B, B=467B, C=431B, all <512. maxinflate: read 431 bytes, OVERFLOW? no.

PoC changes

Authored: maxcalc.c (3 cap scenarios), maxinflate.c (empirical inflate), fix.diff (defense-in-depth ksnprintf), VERDICT.md, manifest.json.

Verified recommended fix

No security fix needed (false positive). Defense-in-depth: convert ksprintf to ksnprintf with remain accounting. Matches finding proposal. Full diff in findings/poc/DF-0935/fix.diff.

Verdict

FALSE POSITIVE. Unbounded ksprintf into psbuf[512] is real but output bounded under 512B. Finding's '12x19digit=600B' math wrong: 3 rlimits clamped by INT sysctls (max 10 digits). Max output 499B across all achievable configs. Empirical: 431B read from /proc/self/rlimit.