β¬’ 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 366 B view raw
run.sh run-script runs all three disproofs 504 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 6.8 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 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.