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 | 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 |
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 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.