Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ $(BUILD_DIR)/test-usbdev-ioctl-departed: tests/test-usbdev-ioctl-departed.c \
@echo " CROSS $<"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -I$(BUILD_DIR) -o $@ $<

# test-rlimit-fsize starts a second thread to show SIGXFSZ is sent to the thread
# that wrote.
$(BUILD_DIR)/test-rlimit-fsize: tests/test-rlimit-fsize.c | $(BUILD_DIR)
@echo " CROSS $< (with -lpthread)"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread

# test-usbdev-ioctl churns open/read/close on one usbdevfs node from four
# threads, so a close and a sibling's open contend for the same fd number.
$(BUILD_DIR)/test-usbdev-ioctl: tests/test-usbdev-ioctl.c | $(BUILD_DIR)
Expand Down
6 changes: 6 additions & 0 deletions scripts/check-eintr-contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@
"restartable",
"Polled retry helper; reports before the operation it guards runs.",
),
"syscall/io.c::copy_past_fsize": (
"restartable",
"Waits for a byte in an empty pipe before the write path can refuse "
"it, and only while the copy has moved nothing. It reads nothing "
"itself, so a restart re-runs the call over the same input.",
),
"syscall/io.c::io_wait_fd_or_interrupted": (
"restartable",
"Reports readiness or EINTR without transferring anything itself. It "
Expand Down
10 changes: 10 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,16 @@ int main(int argc, char **argv)
*/
signal(SIGPIPE, SIG_IGN);

/* A host write past this process's own RLIMIT_FSIZE raises SIGXFSZ, whose
* default action kills elfuse. The guest's limit lives in the guest table
* and never reaches the host, and sys_rlimit_init lifts the host soft limit
* to the hard one, so a host limit is only reached by guest_init sizing the
* slab file, or by a guest that raised its own limit past the host hard
* limit. Ignoring it turns either into EFBIG: the guest sees a failed
* write, and guest_init falls back to an anonymous slab.
*/
signal(SIGXFSZ, SIG_IGN);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring SIGXFSZ avoids the crash, but a guest that raises its soft RLIMIT_FSIZE up to its hard limit still gets EFBIG from the host's lower soft limit. Linux lets that write through. Now that elfuse enforces the guest limit itself, raising the host soft RLIMIT_FSIZE to the host hard limit at startup (after the guest table is seeded) would remove that gap. Keep SIG_IGN for the hard-limit case. The new tests/test-rlimit-seed.sh block asserts the EFBIG and would need to expect success instead.

@xalestar xalestar Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0a9306a. After sys_rlimit_init seeds the guest table, it raises the host soft RLIMIT_FSIZE to the host hard limit. This happens before host_rlimit_check_arm records the host limits. SIG_IGN stays, for a guest that raises its limit past the host hard limit. The tests/test-rlimit-seed.sh case now expects the write to land, with an 8192-byte file under ulimit -S -f 4. Its new case "boots under a host hard file size limit" runs under ulimit -f 4. All 9 cases pass on elfuse. On 9f5ed24 the write case fails.


/* Block the vCPU-preemption signals and start the sigwait thread before any
* vCPU thread exists, so both the normal path and the fork-child path below
* inherit the block on every thread they spawn.
Expand Down
57 changes: 57 additions & 0 deletions src/syscall/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ _Static_assert(NAME_MAX == DIRENT64_NAME_MAX,
#include "syscall/poll.h" /* epoll_dup_fd */
#include "syscall/proc.h"
#include "syscall/proc-pidfd.h" /* pidfd_pin */
#include "syscall/signal.h"
#include "syscall/sys.h"

/* Linux dirent64 layout. */
typedef struct {
Expand Down Expand Up @@ -3659,6 +3661,31 @@ int64_t sys_faccessat(guest_t *g,

/* truncate. */

/* RLIMIT_FSIZE on a truncate, after inode_newsize_ok (Linux fs/attr.c): growing
* a regular file past the soft limit fails with EFBIG and SIGXFSZ, and
* shrinking one that is already past it does not.
*
* The kernel refuses a descriptor that is not open for writing before it looks
* at the size (do_sys_ftruncate), so such a descriptor is passed through and
* the host call reports it.
*/
static int64_t truncate_fsize_check(int host_fd, int64_t length)
{
uint64_t limit = sys_rlimit_cur(LINUX_RLIMIT_FSIZE);
if (limit == LINUX_RLIM_INFINITY || length < 0 ||
(uint64_t) length <= limit)
return 0;

struct stat st;
if (fstat(host_fd, &st) < 0 || !S_ISREG(st.st_mode) || st.st_size >= length)
return 0;
int fl = fcntl(host_fd, F_GETFL);
if (fl < 0 || (fl & O_ACCMODE) == O_RDONLY)
return 0;
sys_rlimit_send_xfsz();
return -LINUX_EFBIG;
}

int64_t sys_ftruncate(int fd, int64_t length)
{
/* One lookup for the type, the seals and the descriptor, so a sibling's
Expand All @@ -3677,6 +3704,15 @@ int64_t sys_ftruncate(int fd, int64_t length)
return -LINUX_EPERM;
}

/* setattr_prepare runs inode_newsize_ok before shmem_setattr looks at the
* grow and shrink seals.
*/
int64_t fsz = truncate_fsize_check(host_ref.fd, length);
if (fsz < 0) {
host_fd_ref_close(&host_ref);
return fsz;
}

if (seals & (LINUX_F_SEAL_SHRINK | LINUX_F_SEAL_GROW)) {
struct stat st;
if (fstat(host_ref.fd, &st) == 0) {
Expand Down Expand Up @@ -3724,6 +3760,11 @@ int64_t sys_truncate(guest_t *g, uint64_t path_gva, int64_t length)
return -LINUX_EINVAL;
return linux_errno();
}
int64_t fsz = truncate_fsize_check(fd, length);
if (fsz < 0) {
close(fd);
return fsz;
}
if (ftruncate(fd, length) < 0) {
close_keep_errno(fd);
return linux_errno();
Expand All @@ -3732,6 +3773,22 @@ int64_t sys_truncate(guest_t *g, uint64_t path_gva, int64_t length)
return 0;
}

/* A truncate that would grow a regular file past RLIMIT_FSIZE is refused
* from stat and the write permission faccessat reports, after the
* permission check vfs_truncate makes first. The file is not opened to
* check it: closing any descriptor on a file drops every POSIX record lock
* the process holds on it, guest F_SETLK locks included, and vfs_truncate
* opens nothing. A rename between the stat and the truncate below can still
* let the truncate grow a file the check did not see.
*/
struct stat st;
if (length >= 0 && (uint64_t) length > sys_rlimit_cur(LINUX_RLIMIT_FSIZE) &&
stat(tx.host_path, &st) == 0 && S_ISREG(st.st_mode) &&
st.st_size < length &&
faccessat(AT_FDCWD, tx.host_path, W_OK, AT_EACCESS) == 0) {
sys_rlimit_send_xfsz();
return -LINUX_EFBIG;
}
if (truncate(tx.host_path, length) < 0)
return linux_errno();
return 0;
Expand Down
4 changes: 4 additions & 0 deletions src/syscall/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,9 @@ int fd_get_type(int guest_fd);
* guest asked for, which on an owned fd is the only place it is recorded. seals
* rides along so a write path can reject a sealed memfd from the state it
* pinned, rather than from a second lookup that may describe another file.
* call_nonblock is never read from the table: a caller sets it on its own copy
* when the syscall itself asks for a nonblocking transfer (SPLICE_F_NONBLOCK),
* whatever mode the description carries.
*/
typedef struct {
int type;
Expand All @@ -558,6 +561,7 @@ typedef struct {
bool can_block;
bool nonblock_owned;
bool guest_nonblock;
bool call_nonblock;
} fd_block_state_t;

fd_block_state_t fd_block_state(int guest_fd);
Expand Down
Loading
Loading