Missing return after EINVAL in sys_read/sys_write/sys_extpwrite bypasses nbyte>SSIZE_MAX guard
| Field | Value |
|---|---|
| ID | DF-0023 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:N |
| CWE | CWE-754 Improper Check for Exceptional Conditions; CWE-696 Incorrect Behavior Order |
| File | sys/kern/sys_generic.c |
| Lines | 130-131, 161-162, 336-337, 368-369 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
In sys_read, sys_write, and sys_extpwrite, the guard
if ((ssize_t)uap->nbyte < 0) error = EINVAL; assigns to a local error but
does not return, so the dead-stored EINVAL is overwritten by the
subsequent kern_preadv/kern_pwritev call and the ssize_t-range validation
of the user-supplied size_t nbyte becomes a no-op. The sibling
sys_extpread (:161-162) implements the same check correctly as
return(EINVAL);, proving intent. A caller passing nbyte > SSIZE_MAX
(bit 63 set) now proceeds into kern_preadv/kern_pwritev with
auio.uio_resid set to that huge value. No kernel memory corruption or info
leak is produced (downstream uiomove caps per-call counts and copyout/
copyin enforce the user address range), so this is a validation-correctness /
defense-in-depth defect: the documented SSIZE_MAX contract is silently
broken for every downstream file-ops implementation.
Root cause
/* sys_read :130-131, sys_write :336-337, sys_extpwrite :368-369 */
if ((ssize_t)uap->nbyte < 0)
error = EINVAL; /* missing return */
aiov.iov_base = uap->buf;
aiov.iov_len = uap->nbyte;
...
auio.uio_resid = uap->nbyte; /* huge value reaches kern_preadv/pwritev */
Contrast the correct sibling sys_extpread :161-162:
if ((ssize_t)uap->nbyte < 0)
return(EINVAL);
nbyte is size_t (sys/sys/sysproto.h); auio.uio_resid is size_t
(sys/sys/_uio.h:69), so the huge value propagates verbatim.
Threat model & preconditions
- Attacker position: any local unprivileged user.
- Privileges gained or impact: none demonstrated.
read/write/extpwritewithnbyte > SSIZE_MAXproceed (loop until a natural boundary โ EOF / empty socket buffer / EFAULT from the exhausted user buffer) instead of returningEINVAL. Thessize_treturn value's range contract is broken, which could confuse a caller; downstreamuiomove/copyout/copyinbound the actual transfer, so no kernel memory-safety impact was reproduced. Recorded as a correctness/defense-in-depth fix. - Required config or capabilities: none; default kernel.
- Reachability:
read(2)/write(2)/extpwrite(2)withnbyte > SSIZE_MAX.
Proof of concept
PoC source: findings/poc/DF-0023/einval_noop.c
Build & run (unprivileged)
cc -o einval_noop findings/poc/DF-0023/einval_noop.c ./einval_noop
Expected output (bug present)
read(fd,buf,SIZE_MAX) = 0, errno=0 (NOT EINVAL) write(fd,buf,SSIZE_MAX+1) = 0, errno=0 (NOT EINVAL)
(Fixed: both -1, errno=EINVAL.)
Impact
Correctness / defense-in-depth. The SSIZE_MAX invariant relied on by the
ssize_t return value and by any driver doing signed resid math is broken;
no memory-safety impact was demonstrated because uiomove/copyout bound the
transfer. Rated Info.
Recommended fix
Add the missing return (mirroring sys_extpread):
--- a/sys/kern/sys_generic.c
+++ b/sys/kern/sys_generic.c
@@ -130,7 +130,7 @@
if ((ssize_t)uap->nbyte < 0)
- error = EINVAL;
+ return (EINVAL);
@@ -336,7 +336,7 @@
if ((ssize_t)uap->nbyte < 0)
- error = EINVAL;
+ return (EINVAL);
@@ -368,7 +368,7 @@
if ((ssize_t)uap->nbyte < 0)
- error = EINVAL;
+ return (EINVAL);
References
sys/kern/sys_generic.c:130-131โsys_readmissing return.sys/kern/sys_generic.c:336-337โsys_writemissing return.sys/kern/sys_generic.c:368-369โsys_extpwritemissing return.sys/kern/sys_generic.c:161-162โsys_extpread(correct pattern).- CWE-754, CWE-696.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/sys_generic.c. - pending Reported to DragonFlyBSD security contact.