Enforce RLIMIT_FSIZE on regular-file writes
Intent: a guest RLIMIT_FSIZE limits the guest's writes to regular files as it does on Linux. This is follow-up 1 of #448, which stores the limit in the guest table and leaves it unenforced.
Failure fixed: on main, a guest that sets RLIMIT_FSIZE to 4 KiB and writes 8 KiB gets 8192 back and no signal. On Linux the write returns 4096, the next write fails with EFBIG, and SIGXFSZ goes to the thread that wrote.
This PR: the first seven commits fix gaps that already exist on main and that the limit relies on. Each stands alone, so it can be tracked and reverted on its own.
- copy_file_range runs
generic_file_rw_checksfirst: EISDIR for a directory, EINVAL for another non-regular file, EBADF for a wrong access mode or an O_APPEND output. - copy_file_range returns EOVERFLOW when either position plus
lenwraps (generic_copy_file_checks), a negative offset included. - sendfile and splice refuse an O_APPEND output with EINVAL, as
do_splice_directanddo_splicedo. A pipe output is still accepted. - splice honors SPLICE_F_NONBLOCK on a pipe or AF_UNIX stream input, inherited stdin included; a TCP input follows its own O_NONBLOCK. A full pipe output answers EAGAIN before anything is read, so no input is lost.
- pwrite and pwritev on an O_APPEND descriptor write at end of file, as Linux does, instead of at the offset given.
- pwrite64 and pwritev return EINVAL when
offset + countoverflowsoff_t(rw_verify_area). A pipe still gets ESPIPE. - elfuse ignores host SIGXFSZ, so a guest that raises its soft limit past the host one gets EFBIG instead of killing elfuse (#437).
The last commit enforces the limit, on regular files only:
- write, pwrite64, writev, pwritev and pwritev2 cut a request at the soft limit, and refuse one that starts at or past it (
generic_write_check_limits). - truncate, ftruncate and fallocate refuse to grow a file past it (
inode_newsize_ok). truncate by path checks withstatandfaccessatand never opens the file, so the guest's record locks survive. - sendfile and splice stop at the limit, and raise SIGXFSZ only when the input still has a byte to give.
- copy_file_range cuts or refuses the whole request up front (
generic_copy_file_checks). - Pipes, sockets and descriptors not open for writing are not limited.
Review focus:
sys_rlimit_send_xfszqueues SIGXFSZ on the calling thread (send_sigtocurrent), falling back to the process only when the thread is not found.- O_APPEND and RWF_APPEND writes, pwrite on an O_APPEND descriptor included, are measured from end of file, since
generic_write_checksmoves the position there. - The
writefast path insyscall_dispatchsends every write tosys_writewhile the limit is finite. copy_past_fsizewaits for a byte on an empty pipe before refusing, assplice_from_pipe_nextdoes, and is listed as restartable inscripts/check-eintr-contract.py: it reads nothing and waits only while the copy has moved nothing.
Left out: fallocate on a read-only descriptor answers EINVAL or 0 where Linux answers EBADF; that is #462, and the new test only asserts no SIGXFSZ there.
Known gap: two writers that both size themselves against one position or one file size can each fit the same remaining room, so the file ends past the limit by at most one request per extra writer. Linux closes this with the inode lock; #464 tracks it.
Verified on e9fdbbb: make check-format, make lint and make check pass; make test-matrix-elfuse-aarch64 gives 316 passed, 0 failed, 10 skipped; make test-matrix-qemu-aarch64 gives 289 passed, 1 failed, 35 skipped; the failure is test-futex-requeue-race on the qemu reference kernel, a timing check that passed 5 of 5 reruns. Each commit builds. test-rlimit-fsize and the new test-io-opt and test-readv-writev steps pass on elfuse and on the qemu reference kernel, and fail on an elfuse built from e9fdbbb.
Fixes #437
Summary by cubic
Enforces the guest RLIMIT_FSIZE on regular-file writes so a finite limit actually bounds file growth. The limit was previously stored but never read: a guest with a 4 KiB limit could write 8 KiB and get 8192 bytes back with no signal.
write,pwrite64,writev,pwritevandpwritev2cut a request at the soft limit and refuse one that starts at or past it;truncate,ftruncateandfallocaterefuse to grow a file past it;sendfileandsplicestop at the limit.copy_file_rangenow rejects bad ends before copying — EISDIR for a directory, EINVAL for a non-regular file, EBADF for an input not open for reading or an output not open for writing or open O_APPEND — then cuts or refuses its whole request at the limit.truncateby path now checks and truncates through a single descriptor opened for writing, so a rename cannot put one file under the check and another under the truncate.- Each refusal sends SIGXFSZ to the calling thread, not to a sibling that has it unblocked. A copy that hits the limit before moving anything returns the input's read error rather than 0.
- The limit applies only to regular files; pipes, sockets and descriptors not open for writing are unaffected. O_APPEND and RWF_APPEND writes are measured from end of file, and zero-length writes are never refused.
fallocateon a read-only descriptor still returns EINVAL or 0 instead of Linux's EBADF; tracked in #462.