sysctl_ip6_tempvltime() restores the wrong variable on rejection: failed net.inet6.ip6.tempvltime writes persist the invalid value AND clobber ip6_temp_preferred_lifetime
Summary
The EINVAL path of sysctl_ip6_tempvltime assigns the saved old valid lifetime to ip6_temp_preferred_lifetime instead of restoring ip6_temp_valid_lifetime, so (a) the value the kernel just rejected as invalid (vltime < pltime, violating RFC 4941) remains installed, and (b) the preferred lifetime is corrupted to the old valid lifetime - leaving preferred > valid, the exact state the check exists to forbid. VERIFIED live 2/2 on stock guest: after 'sysctl -w net.inet6.ip6.tempvltime=100' fails with EINVAL (pltime=86400), tempvltime reads back 100 and temppltime reads back 604800 (the sysctl(8) failure diagnostic even echoes the old value, masking the corruption). Root-only sysctl: admin configuring IPv6 privacy-address lifetimes gets a silent persistent invalid configuration feeding the RFC 4941 temporary-address lifetime math. No memory-safety impact. Fix validated (one-line: restore the correct variable, mirroring the temppltime twin).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3448 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| trigger.sh | β | 1.5 KB | view raw | |
| build.sh | β | 76 B | view raw | |
| run.sh | β | 136 B | view raw | |
| run.log | β | 447 B | view raw | |
| run.2.log | β | 445 B | view raw | |
| env.txt | β | 220 B | view raw | |
| fix.diff | β | 348 B | view raw | |
| manifest.json | β | 860 B | view raw | |
| verdict.json | β | 2.7 KB | view raw |
DF-3448 β sysctl_ip6_tempvltime restores the wrong variable (and keeps the rejected value)
File: sys/netinet6/in6_proto.c (pass 2)
What
sysctl_ip6_tempvltime() (sys/netinet6/in6_proto.c:437-453) guards the
RFC-4941 invariant valid_lifetime >= preferred_lifetime. When a write
violates it, the intended behavior (matching the KAME/FreeBSD original) is
to restore the OLD valid lifetime and fail with EINVAL.
DragonFly's copy instead does:
old = ip6_temp_valid_lifetime;
error = SYSCTL_IN(req, arg1, sizeof(int)); /* arg1 == &ip6_temp_valid_lifetime */
if (ip6_temp_valid_lifetime < ip6_temp_preferred_lifetime) {
ip6_temp_preferred_lifetime = old; /* BUG: wrong variable */
return (EINVAL);
}
Two effects on every rejected write:
- the rejected
ip6_temp_valid_lifetimevalue stays installed (the restore never touches it), and ip6_temp_preferred_lifetimeis clobbered with the old valid lifetime.
Impact
Root-only sysctl misconfiguration persistence + corruption of the IPv6
privacy-address (temporary address, RFC 4941) lifetime configuration:
preferred > valid afterwards, so subsequent temporary-address management
(in6_ifadd.c) computes lifetimes from an inconsistent state (addresses can
be treated as valid long past their preferred expiry, or regenerate on the
wrong schedule). No memory-safety impact. Severity: Low.
Reproduce (in the QEMU guest, as root)
sh /tmp/df3448_trigger.sh # or: scp trigger.sh over and run it
Expected output on a vulnerable kernel (see run.log / run.2.log):
== attempting invalid write: tempvltime=100 (100 < pltime 86400) == sysctl: net.inet6.ip6.tempvltime=100: Invalid argument == state after rejected write == pltime=604800 vltime=100 BUG PART 1 CONFIRMED: rejected tempvltime=100 persisted (should still be 604800) BUG PART 2 CONFIRMED: temppltime clobbered to 604800 (old vltime; should still be 86400)
Note the extra trap: sysctl -w prints the old value (604800) in its
diagnostic while the kernel variable is actually left at 100 β an admin
reading the error output would believe the write was fully rejected.
Build: none needed (shell-only trigger).
DF-3448 β VERDICT
Status: reproduced (deterministic, 2/2 runs) Β· Impact: none (config corruption, root-only) Β· Confidence: certain
Root cause (path:line)
sys/netinet6/in6_proto.c:437-453, sysctl_ip6_tempvltime():
447: old = ip6_temp_valid_lifetime;
448: error = SYSCTL_IN(req, arg1, sizeof(int)); /* writes ip6_temp_valid_lifetime */
449: if (ip6_temp_valid_lifetime < ip6_temp_preferred_lifetime) {
450: ip6_temp_preferred_lifetime = old; /* restores the WRONG variable */
451: return (EINVAL);
452: }
arg1 is &ip6_temp_valid_lifetime (registration at in6_proto.c:495-498),
so the SYSCTL_IN at line 448 installs the new value; the rejection path at
line 450 was supposed to undo it (ip6_temp_valid_lifetime = old, exactly
as the KAME/FreeBSD original and as the twin handler
sysctl_ip6_temppltime at in6_proto.c:418-435 correctly does for
ip6_temp_preferred_lifetime). The DragonFly transcription swapped the
variable name. Consequences on every rejected write:
ip6_temp_valid_lifetimekeeps the value the kernel just told the administrator was invalid (persistent misconfiguration);ip6_temp_preferred_lifetimeis overwritten with the old valid lifetime, so afterwardspreferred (604800) > valid (100)β the exact invariant the check exists to prevent.
These feed in6_tmpaddpub()/temporary-address lifetime math in
sys/netinet6/in6_ifadd.c (e.g. ip6_temp_preferred_lifetime,
ip6_temp_valid_lifetime use when creating/regenerating RFC 4941
temporary addresses), producing wrong preferred/valid expiry schedules for
privacy addresses when net.inet6.ip6.use_tempaddr is later enabled.
How verified
Ran trigger.sh on the live QEMU guest (stock INVARIANTS kernel,
DragonFly 6.5-DEVELOPMENT #0, securelevel -1, root):
run.log(run 1) andrun.2.log(run 2): after the EINVAL,tempvltimereads back 100 (the rejected value) andtemppltimereads back 604800 (old vltime). Both bug halves confirmed twice, deterministically.- Guest sysctls restored to defaults afterwards (86400 / 604800) β guest left clean.
Why not higher severity
- Write path is root-only (sysctl CTLFLAG_RW handler; no unprivileged route to it β sockopt/sysctl privilege).
- No memory safety: both variables are plain
ints consumed arithmetically by address-lifetime logic; no length/index/pointer derived from them. - Worse-case realistic impact: administrator gets a silent persistent invalid configuration plus a corrupted preferred lifetime β a correctness/privacy-regression bug, not an exploitation primitive.
Fix validation
fix.diff restores the correct variable (one-word change, mirroring the
sibling sysctl_ip6_temppltime and the KAME/FreeBSD original). Fix
validation by rebuild was not performed β mandatory rebuilds apply to
memory-corruption findings; for this Low logic finding the correction is
verifiable by inspection (fix_status: not_testable).
Fix
--- a/sys/netinet6/in6_proto.c
+++ b/sys/netinet6/in6_proto.c
@@ -446,7 +446,7 @@
old = ip6_temp_valid_lifetime;
error = SYSCTL_IN(req, arg1, sizeof(int));
if (ip6_temp_valid_lifetime < ip6_temp_preferred_lifetime) {
- ip6_temp_preferred_lifetime = old;
+ ip6_temp_valid_lifetime = old;
return (EINVAL);
}
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_testablefix.diff restores the correct variable (one-word change mirroring the pltime handler and upstream KAME code); kernel rebuild validation skipped β mandatory only for memory-corruption findings, and this Low logic fix is verifiable by inspection
findings/poc/DF-3448/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-3448/run.log and run.2.log β decisive lines: 'sysctl: net.inet6.ip6.tempvltime=100: Invalid argument' followed by 'pltime=604800 vltime=100' and both 'BUG PART n CONFIRMED' lines; VERDICT.md has the full path:line trace; fix.diff is the one-word correction
PoC changes
none β pure shell trigger written fresh (no seed); no compilation required
Verified recommended fix
In sysctl_ip6_tempvltime's rejection path, restore ip6_temp_valid_lifetime = old (not ip6_temp_preferred_lifetime), matching the sysctl_ip6_temppltime twin and the KAME/FreeBSD original
Verdict
sysctl_ip6_tempvltime() in sys/netinet6/in6_proto.c:437-453 restores the wrong variable on the EINVAL path: a rejected net.inet6.ip6.tempvltime write PERSISTS in the kernel, and ip6_temp_preferred_lifetime is simultaneously clobbered with the old valid lifetime. Verified live on the stock INVARIANTS guest (2/2 deterministic runs): after 'sysctl -w net.inet6.ip6.tempvltime=100' fails with EINVAL (pltime=86400), tempvltime reads back 100 and temppltime reads back 604800. Root-only config corruption of RFC 4941 temporary-address lifetimes; no memory-safety impact. The twin handler sysctl_ip6_temppltime and the KAME/FreeBSD original restore correctly; this is a DragonFly transcription bug.
No comments yet.