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 forRLIMIT_CPU,RLIMIT_FSIZE,RLIMIT_CORE,RLIMIT_RSS,RLIMIT_SBSIZE, orRLIMIT_VMEM. An unprivileged user can set bothrlim_curandrlim_maxtoRLIM_INFINITY-1(19 decimal digits) for these six resources. To trigger the overflow, the administrator must raise the boot-time tunableskern.maxdsizand/orkern.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>/rlimitsince 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.
Recommended fix
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
sys/vfs/procfs/procfs_status.c:56-61, 99-167βprocfs_dostatusdemonstrates the correct boundedksnprintf+DOCHECKpattern.sys/kern/subr_prf.c:411-422, 549βksprintfis unbounded (PCHARexpands to*d++=ccwith no length check).sys/sys/resource.h:122, 133-146βRLIM_NLIMITS=12and therlimit_ident[]strings.sys/kern/kern_plimit.c:268-385βkern_setrlimitdoes not cap the six uncapped resources.sys/kern/subr_param.c:221-226βkern.maxdsiz/maxssizloader tunables.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0935 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
ksprintfis real, but the data flowing into it is bounded. - 3 of the 12 rlimits (
RLIMIT_NOFILE,RLIMIT_NPROC,RLIMIT_POSIXLOCKS) are clamped byint-typed sysctls (maxfilesperproc,maxprocperuid,maxposixlocksperuid) insidekern_setrlimit(kern_plimit.c:358-380).intcaps 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.
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. (Fullrun.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 maximumprocfs_dorlimitoutput 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_reproducedimpact=none(no overflow, no panic, no leak)confidence=certain(math is exact; confirmed on-guest)fix_status=not_applicablefor 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_testablenot_applicable -- false positive. No security misbehavior to fix.
n/a
Confirmed kernel references
- sys/vfs/procfs/procfs_rlimit.c:64
- sys/vfs/procfs/procfs_rlimit.c:77
- sys/vfs/procfs/procfs_rlimit.c:88
- sys/vfs/procfs/procfs_rlimit.c:90
- sys/vfs/procfs/procfs_rlimit.c:99
- sys/vfs/procfs/procfs_rlimit.c:101
- sys/kern/kern_plimit.c:358-363
- sys/kern/kern_plimit.c:365-374
- sys/kern/kern_plimit.c:375-380
- sys/kern/subr_param.c:76
- sys/kern/subr_param.c:80
- sys/kern/subr_param.c:82
- sys/platform/pc64/include/vmparam.h:60
- sys/platform/pc64/include/vmparam.h:66
- sys/sys/resource.h:125
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.
No comments yet.