NetBSD-Bugs archive
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]
kern/60681: write(2) of 2 GiB or more that fails leaves the file offset wrong (int resid in ffs/ext2fs/lfs/msdosfs/udf write rollback)
>Number: 60681
>Category: kern
>Synopsis: write(2) of 2 GiB or more that fails leaves the file offset wrong (int resid in ffs/ext2fs/lfs/msdosfs/udf write rollback)
>Confidential: no
>Severity: non-critical
>Priority: low
>Responsible: kern-bug-people
>State: open
>Class: sw-bug
>Submitter-Id: net
>Arrival-Date: Fri Sep 04 00:40:00 +0000 2026
>Originator: Thomas Waldmann
>Release: NetBSD 11.0 (same code in -current, netbsd-10 and netbsd-9)
>Organization:
>Environment:
System: NetBSD netbsd11 11.0 NetBSD 11.0 (GENERIC) #0: Thu Jul 30 15:23:12 UTC 2026 mkrepro%mkrepro.NetBSD.org@localhost:/usr/src/sys/arch/amd64/compile/GENERIC amd64
Architecture: x86_64
Machine: amd64
KVM guest, 12 vCPUs, 8 GiB RAM, root file system ffs on /dev/dk0
>Description:
The write paths of ffs, ext2fs, lfs, msdosfs and udf save the original
uio_resid in a variable of type int and use it afterwards:
sys/ufs/ufs/ufs_readwrite.c WRITE(): int ... resid; BUFWR(): int resid, ...;
ufs_post_write_update(..., int resid, int oerror)
sys/ufs/ext2fs/ext2fs_readwrite.c same pattern (ext2fs_post_write_update)
sys/ufs/lfs/ulfs_readwrite.c same pattern (ulfs_post_write_update)
sys/fs/msdosfs/msdosfs_vnops.c msdosfs_write(): int resid;
sys/fs/udf/udf_vnops.c udf_write(): int resid, extended;
When the write fails, the file is truncated back to its old size and the
uio is rolled back with that value:
uio->uio_offset -= resid - uio->uio_resid;
uio->uio_resid = resid;
and "resid > uio->uio_resid" decides whether anything was written
(clearing of the setuid/setgid bits, synchronous inode update).
uio_resid is a size_t and a single write(2) may request up to SSIZE_MAX
bytes (sys/kern/sys_generic.c, dofilewrite()). For a request of 2^31
bytes or more the int is truncated (negative for 2 GiB <= n < 4 GiB), so
on an error the rollback stores a wrong uio_resid and uio_offset, and
vn_write() (sys/kern/vfs_vnops.c) then does
*offset += count - uio->uio_resid;
which moves the file offset by count - (size_t)(int)count although
nothing was written: +4 GiB for any request between 2 GiB and 4 GiB,
+8 GiB for an 8 GiB request, etc. The return value (-1 and errno) and
the file contents and size are correct, only the offset is wrong. A
program that retries the write on the same descriptor after freeing
space (ENOSPC, EDQUOT) writes 4 GiB past the intended position, leaving
a sparse hole. The setuid/setgid bits are also cleared for such
requests even when 0 bytes were written, since (size_t)resid is huge.
Reads are not affected (the read paths use vsize_t). tmpfs, nfs and
zfs are not affected (no rollback with a saved int).
>How-To-Repeat:
Compile the attached faultoff.c. It maps "total" bytes of anonymous
memory, makes the page at offset "fault_at" PROT_NONE so that write(2)
fails with EFAULT after fault_at bytes (the same rollback code runs for
ENOSPC and EDQUOT), writes the buffer to a new file on ffs and prints the
return value, the file offset (lseek SEEK_CUR) and the file size:
cc -Wall -O2 -o faultoff faultoff.c
for t in 1073741824 2147483647 2147483648 3221225472 4296015872; do
./faultoff /root/fo.bin $t 1048576
done
Observed on NetBSD 11.0/amd64, ffs on /:
write(1073741824 bytes = 1.000 GiB, PROT_NONE page at 1048576) returned -1 errno=14 (Bad address); fd offset after=0 (0.000 GiB); file size after=0
OK: error + full rollback (offset unchanged)
write(2147483647 bytes = 2.000 GiB, PROT_NONE page at 1048576) returned -1 errno=14 (Bad address); fd offset after=0 (0.000 GiB); file size after=0
OK: error + full rollback (offset unchanged)
write(2147483648 bytes = 2.000 GiB, PROT_NONE page at 1048576) returned -1 errno=14 (Bad address); fd offset after=4294967296 (4.000 GiB); file size after=0
*** BUG: fd offset 4294967296 is inconsistent with return value -1
write(3221225472 bytes = 3.000 GiB, PROT_NONE page at 1048576) returned -1 errno=14 (Bad address); fd offset after=4294967296 (4.000 GiB); file size after=0
*** BUG: fd offset 4294967296 is inconsistent with return value -1
write(4296015872 bytes = 4.001 GiB, PROT_NONE page at 1048576) returned -1 errno=14 (Bad address); fd offset after=4294967296 (4.000 GiB); file size after=0
*** BUG: fd offset 4294967296 is inconsistent with return value -1
An 8 GiB + 1 MiB request ends with the offset at 8589934592. Requests
below 2^31 bytes roll back correctly (offset 0).
faultoff.c:
/*
* faultoff - show the file offset left behind by a write(2) that fails
* partway through on NetBSD.
*
* The ffs, ext2fs, lfs, msdosfs and udf write paths keep the original
* uio_resid in an "int resid" and use it to roll the uio back when the
* write fails (the file is truncated back to its old size). For requests
* of 2^31 bytes and more that value is truncated, the rollback restores
* garbage and vn_write() advances the file offset although nothing was
* written.
*
* usage: faultoff outfile total fault_at
*
* Maps "total" bytes (plus one page) of anonymous memory, makes the page
* containing offset "fault_at" PROT_NONE (if fault_at < total), touches
* the readable prefix and then write(2)s "total" bytes from the buffer.
* Prints the return value/errno, the file offset afterwards and the
* file size. A correct rollback leaves the offset unchanged.
*/
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static size_t parse_size(const char *);
static void usage(void) __dead;
static size_t
parse_size(const char *s)
{
unsigned long long val;
char *ep;
errno = 0;
val = strtoull(s, &ep, 0);
if (s[0] == '\0' || *ep != '\0' || errno != 0)
errx(EXIT_FAILURE, "illegal number -- %s", s);
return (size_t)val;
}
static void
usage(void)
{
fprintf(stderr, "usage: %s outfile total fault_at\n", getprogname());
exit(EXIT_FAILURE);
}
int
main(int argc, char *argv[])
{
struct stat st;
char *buf;
size_t total, fault_at, pg, maplen, fpage;
ssize_t n;
off_t pos;
int fd, error;
setprogname(argv[0]);
if (argc != 4)
usage();
total = parse_size(argv[2]);
fault_at = parse_size(argv[3]);
pg = (size_t)sysconf(_SC_PAGESIZE);
maplen = (total + 2 * pg - 1) & ~(pg - 1);
buf = mmap(NULL, maplen, PROT_READ | PROT_WRITE,
MAP_ANON | MAP_PRIVATE, -1, 0);
if (buf == MAP_FAILED)
err(EXIT_FAILURE, "mmap");
if (fault_at < total) {
fpage = fault_at & ~(pg - 1);
if (mprotect(buf + fpage, pg, PROT_NONE) == -1)
err(EXIT_FAILURE, "mprotect");
memset(buf, 0x5a, fpage);
} else {
memset(buf, 0x5a, total);
}
fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1)
err(EXIT_FAILURE, "open %s", argv[1]);
errno = 0;
n = write(fd, buf, total);
error = errno;
pos = lseek(fd, 0, SEEK_CUR);
if (pos == -1)
err(EXIT_FAILURE, "lseek");
if (fstat(fd, &st) == -1)
err(EXIT_FAILURE, "fstat");
printf("write(%zu bytes = %.3f GiB, PROT_NONE page at %zu) "
"returned %zd", total, (double)total / (1 << 30), fault_at, n);
if (n == -1)
printf(" errno=%d (%s)", error, strerror(error));
printf("; fd offset after=%" PRIdMAX " (%.3f GiB); "
"file size after=%" PRIdMAX "\n", (intmax_t)pos,
(double)pos / (1 << 30), (intmax_t)st.st_size);
if (n == -1 && pos == 0)
printf("OK: error + full rollback (offset unchanged)\n");
else if (n >= 0 && pos == n)
printf("OK: short/complete write, offset == return value\n");
else
printf("*** BUG: fd offset %" PRIdMAX " is inconsistent "
"with return value %zd\n", (intmax_t)pos, n);
(void)close(fd);
(void)unlink(argv[1]);
exit(EXIT_SUCCESS);
}
>Fix:
Keep the saved length as size_t (attached netbsd-int-resid.patch): change
"int resid" to "size_t resid" in the five files above and in the
ufs/ext2fs/ulfs_post_write_update() prototypes and definitions. All uses
(resid = uio->uio_resid, resid > uio->uio_resid, resid - uio->uio_resid)
are size_t arithmetic on values where resid >= uio->uio_resid, so no other
change is needed.
Verified: with the patch applied to the 11.0 sources (syssrc.tgz, all
hunks apply) and GENERIC rebuilt natively, the reproducer reports
"fd offset after=0 ... OK: error + full rollback" for every cell,
including 2 GiB, 3 GiB, 4 GiB + 1 MiB and 8 GiB + 1 MiB requests, and
the successful 3 GiB read()/write() still return the exact count.
Patch (apply with patch -p0 in src/):
Keep the original request length of a write as size_t in the write rollback code.
The ffs, ext2fs, lfs, msdosfs and udf write paths save uio_resid in an int
and use it to roll the uio back when the write fails (after truncating the
file back to its old size) and to decide whether data was written (suid/sgid
clearing, sync update). For requests of 2^31 bytes or more the value is
truncated, the rollback restores a wrong uio_resid/uio_offset and vn_write()
then moves the file offset although nothing was written.
ufs_readwrite.c hunks are against -current (rev 1.131), the others against
netbsd-11; the changed lines are identical on both. Apply with -p0 in src/.
--- sys/ufs/ufs/ufs_readwrite.c.orig
+++ sys/ufs/ufs/ufs_readwrite.c
@@ -54,3 +54,3 @@
static int ufs_post_read_update(struct vnode *, int, int);
static int ufs_post_write_update(struct vnode *, struct uio *, int,
- kauth_cred_t, off_t, int, int);
+ kauth_cred_t, off_t, size_t, int);
@@ -251,6 +251,7 @@
kauth_cred_t cred;
off_t osize, origoff, oldoff, preallocoff, endallocoff, nsize;
- int blkoffset, error, flags, ioflag, resid;
+ int blkoffset, error, flags, ioflag;
+ size_t resid;
int aflag;
vsize_t bytelen;
bool async;
@@ -473,5 +474,6 @@
struct buf *bp;
off_t osize;
- int resid, xfersize, size, blkoffset;
+ size_t resid;
+ int xfersize, size, blkoffset;
daddr_t lbn;
int error;
@@ -560,6 +562,6 @@
static int
ufs_post_write_update(struct vnode *vp, struct uio *uio, int ioflag,
- kauth_cred_t cred, off_t osize, int resid, int oerror)
+ kauth_cred_t cred, off_t osize, size_t resid, int oerror)
{
struct inode *ip = VTOI(vp);
int error = oerror;
--- sys/ufs/ext2fs/ext2fs_readwrite.c.orig
+++ sys/ufs/ext2fs/ext2fs_readwrite.c
@@ -85,3 +85,3 @@
static int ext2fs_post_read_update(struct vnode *, int, int);
static int ext2fs_post_write_update(struct vnode *, struct uio *, int,
- kauth_cred_t, off_t, int, int);
+ kauth_cred_t, off_t, size_t, int);
@@ -269,6 +269,7 @@
struct ufsmount *ump;
off_t osize;
- int blkoffset, error, ioflag, resid;
+ int blkoffset, error, ioflag;
+ size_t resid;
vsize_t bytelen;
off_t oldoff = 0; /* XXX */
bool async;
@@ -368,4 +369,5 @@
off_t osize;
daddr_t lbn;
- int resid, blkoffset, xfersize;
+ size_t resid;
+ int blkoffset, xfersize;
int error;
@@ -437,6 +439,6 @@
static int
ext2fs_post_write_update(struct vnode *vp, struct uio *uio, int ioflag,
- kauth_cred_t cred, off_t osize, int resid, int oerror)
+ kauth_cred_t cred, off_t osize, size_t resid, int oerror)
{
struct inode *ip = VTOI(vp);
int error = oerror;
--- sys/ufs/lfs/ulfs_readwrite.c.orig
+++ sys/ufs/lfs/ulfs_readwrite.c
@@ -49,3 +49,3 @@
static int ulfs_post_read_update(struct vnode *, int, int);
static int ulfs_post_write_update(struct vnode *, struct uio *, int,
- kauth_cred_t, off_t, int, int);
+ kauth_cred_t, off_t, size_t, int);
@@ -238,6 +238,7 @@
kauth_cred_t cred;
off_t osize, origoff, oldoff, preallocoff, endallocoff, nsize;
- int blkoffset, error, flags, ioflag, resid;
+ int blkoffset, error, flags, ioflag;
+ size_t resid;
int aflag;
vsize_t bytelen;
bool async;
@@ -421,6 +422,7 @@
struct buf *bp;
off_t osize;
- int resid, xfersize, size, blkoffset;
+ size_t resid;
+ int xfersize, size, blkoffset;
daddr_t lbn;
int error;
bool need_unreserve = false;
@@ -516,6 +518,6 @@
static int
ulfs_post_write_update(struct vnode *vp, struct uio *uio, int ioflag,
- kauth_cred_t cred, off_t osize, int resid, int oerror)
+ kauth_cred_t cred, off_t osize, size_t resid, int oerror)
{
struct inode *ip = VTOI(vp);
int error = oerror;
--- sys/fs/msdosfs/msdosfs_vnops.c.orig
+++ sys/fs/msdosfs/msdosfs_vnops.c
@@ -547,6 +547,6 @@
kauth_cred_t a_cred;
} */ *ap = v;
- int resid;
+ size_t resid;
int error = 0;
int ioflag = ap->a_ioflag;
u_long osize;
--- sys/fs/udf/udf_vnops.c.orig
+++ sys/fs/udf/udf_vnops.c
@@ -294,4 +294,5 @@
vsize_t len;
int aflag = ioflag & IO_SYNC ? B_SYNC : 0;
int error;
- int resid, extended;
+ size_t resid;
+ int extended;
Home |
Main Index |
Thread Index |
Old Index