From dc8e1dd0e04372a639d1fa8d3dc5390ef8ba99f5 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Sat, 11 Jul 2026 14:55:57 -0400 Subject: [PATCH] fs: implement close_range(2) close_range() was missing entirely. It is the standard way to close a range of file descriptors in one call (commonly used to clean up inherited fds and by some language runtimes), so programs that call it failed. Implement it over the existing fd table: close (or, with CLOSE_RANGE_CLOEXEC, set O_CLOEXEC on) every open fd in the inclusive range [first, last], clamping last to the fd-table size and ignoring fds that are not open, as Linux does. CLOSE_RANGE_UNSHARE is a no-op in a single-process unikernel (there is nothing to unshare). first > last and unknown flags return EINVAL. Wire it as syscall SYS_close_range (adding __NR_close_range / SYS_close_range for x86-64 and aarch64) with a tracepoint, declare it in unistd.h under _GNU_SOURCE, and export the symbol from libc.so.6 and ld-musl.so.1. Add tests/tst-close-range.cc covering closing a contiguous run, a range that spans not-open fds, the CLOSE_RANGE_CLOEXEC variant, and the EINVAL paths. Passes on OSv under KVM. --- exported_symbols/osv_ld-musl.so.1.symbols | 1 + exported_symbols/osv_libc.so.6.symbols | 1 + fs/vfs/main.cc | 40 ++++++++++++ include/api/aarch64/bits/syscall.h | 2 + include/api/unistd.h | 1 + include/api/x64/bits/syscall.h | 2 + linux.cc | 1 + modules/tests/Makefile | 1 + syscalls/syscall_tracepoints.cc.in | 1 + syscalls/syscalls.cc.in | 1 + tests/tst-close-range.cc | 74 +++++++++++++++++++++++ 11 files changed, 125 insertions(+) create mode 100644 tests/tst-close-range.cc diff --git a/exported_symbols/osv_ld-musl.so.1.symbols b/exported_symbols/osv_ld-musl.so.1.symbols index d8621caad7..201b124fc3 100644 --- a/exported_symbols/osv_ld-musl.so.1.symbols +++ b/exported_symbols/osv_ld-musl.so.1.symbols @@ -78,6 +78,7 @@ clock_getres clock_gettime clock_nanosleep close +close_range closedir closelog confstr diff --git a/exported_symbols/osv_libc.so.6.symbols b/exported_symbols/osv_libc.so.6.symbols index 3608b1788b..658c51ae82 100644 --- a/exported_symbols/osv_libc.so.6.symbols +++ b/exported_symbols/osv_libc.so.6.symbols @@ -56,6 +56,7 @@ clock_getres clock_gettime clock_nanosleep close +close_range closedir closelog confstr diff --git a/fs/vfs/main.cc b/fs/vfs/main.cc index d4fc297c16..3a92ef49e9 100644 --- a/fs/vfs/main.cc +++ b/fs/vfs/main.cc @@ -299,6 +299,46 @@ int close(int fd) return -1; } +// close_range(2): close (or set close-on-exec on) every open fd in the inclusive +// range [first, last]. CLOSE_RANGE_UNSHARE is a no-op in a single-process +// unikernel (there is nothing to unshare). CLOSE_RANGE_CLOEXEC sets O_CLOEXEC +// instead of closing. +#ifndef CLOSE_RANGE_UNSHARE +#define CLOSE_RANGE_UNSHARE (1U << 1) +#endif +#ifndef CLOSE_RANGE_CLOEXEC +#define CLOSE_RANGE_CLOEXEC (1U << 2) +#endif +extern "C" OSV_LIBC_API +int close_range(unsigned int first, unsigned int last, unsigned int flags) +{ + if (first > last || (flags & ~(CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC))) { + errno = EINVAL; + return -1; + } + + unsigned int end = last; + if (end >= (unsigned int)FDMAX) { + end = FDMAX - 1; + } + for (unsigned int fd = first; fd <= end; fd++) { + if (flags & CLOSE_RANGE_CLOEXEC) { + // Set close-on-exec on the open fds in the range, leave them open. + struct file *fp; + if (fget(fd, &fp) == 0) { + FD_LOCK(fp); + fp->f_flags |= O_CLOEXEC; + FD_UNLOCK(fp); + fdrop(fp); + } + } else { + // Close each fd, ignoring ones that are not open (as Linux does). + fdclose(fd); + } + } + return 0; +} + TRACEPOINT(trace_vfs_mknod, "\"%s\" 0%0o 0x%x", const char*, mode_t, dev_t); TRACEPOINT(trace_vfs_mknod_ret, ""); TRACEPOINT(trace_vfs_mknod_err, "%d", int); diff --git a/include/api/aarch64/bits/syscall.h b/include/api/aarch64/bits/syscall.h index 6e1ca3a0f0..ce9deffe79 100644 --- a/include/api/aarch64/bits/syscall.h +++ b/include/api/aarch64/bits/syscall.h @@ -281,6 +281,7 @@ #define __NR_io_uring_setup 425 #define __NR_io_uring_enter 426 #define __NR_io_uring_register 427 +#define __NR_close_range 436 #define __NR_sys_io_uring_setup __NR_io_uring_setup #define __NR_sys_io_uring_enter __NR_io_uring_enter #define __NR_sys_io_uring_register __NR_io_uring_register @@ -584,6 +585,7 @@ #define SYS_io_uring_setup 425 #define SYS_io_uring_enter 426 #define SYS_io_uring_register 427 +#define SYS_close_range 436 #define SYS_open_tree 428 #define SYS_move_mount 429 #define SYS_fsopen 430 diff --git a/include/api/unistd.h b/include/api/unistd.h index 8d5c4485ff..6667a1d837 100644 --- a/include/api/unistd.h +++ b/include/api/unistd.h @@ -190,6 +190,7 @@ int getresuid(uid_t *, uid_t *, uid_t *); int getresgid(gid_t *, gid_t *, gid_t *); char *get_current_dir_name(void); int syncfs(int); +int close_range(unsigned int, unsigned int, unsigned int); #endif #if defined(_LARGEFILE64_SOURCE) || defined(_GNU_SOURCE) diff --git a/include/api/x64/bits/syscall.h b/include/api/x64/bits/syscall.h index 5a1d7310ee..f05a9ad2a7 100644 --- a/include/api/x64/bits/syscall.h +++ b/include/api/x64/bits/syscall.h @@ -647,6 +647,7 @@ #define SYS_io_uring_setup 425 #define SYS_io_uring_enter 426 #define SYS_io_uring_register 427 +#define SYS_close_range 436 #undef SYS_fstatat #undef SYS_pread @@ -661,6 +662,7 @@ #define __NR_io_uring_setup 425 #define __NR_io_uring_enter 426 #define __NR_io_uring_register 427 +#define __NR_close_range 436 #define __NR_sys_io_uring_setup __NR_io_uring_setup #define __NR_sys_io_uring_enter __NR_io_uring_enter #define __NR_sys_io_uring_register __NR_io_uring_register diff --git a/linux.cc b/linux.cc index 58a018e9ef..87af2e6d0f 100644 --- a/linux.cc +++ b/linux.cc @@ -67,6 +67,7 @@ #include extern "C" int eventfd2(unsigned int, int); +extern "C" int close_range(unsigned int first, unsigned int last, unsigned int flags); extern "C" OSV_LIBC_API long gettid() { diff --git a/modules/tests/Makefile b/modules/tests/Makefile index 391dbd9a81..6c91750755 100644 --- a/modules/tests/Makefile +++ b/modules/tests/Makefile @@ -120,6 +120,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-bsd-evh.so \ tst-fpu.so tst-preempt.so tst-tracepoint.so tst-hub.so \ misc-console.so misc-leak.so misc-readbench.so misc-mmap-anon-perf.so \ tst-mmap-file.so misc-mmap-big-file.so tst-mmap.so tst-huge.so \ + tst-close-range.so \ tst-elf-permissions.so misc-mutex.so misc-sockets.so tst-condvar.so \ tst-queue-mpsc.so tst-af-local.so tst-pipe.so tst-yield.so \ misc-ctxsw.so tst-read.so tst-symlink.so tst-openat.so \ diff --git a/syscalls/syscall_tracepoints.cc.in b/syscalls/syscall_tracepoints.cc.in index 772dfdcb9c..551df80592 100644 --- a/syscalls/syscall_tracepoints.cc.in +++ b/syscalls/syscall_tracepoints.cc.in @@ -8,6 +8,7 @@ TRACEPOINT(trace_syscall_gettid, "%d <=", pid_t); TRACEPOINT(trace_syscall_clock_gettime, "%d <= %d %p", int, clockid_t, struct timespec *); TRACEPOINT(trace_syscall_clock_getres, "%d <= %d %p", int, clockid_t, struct timespec *); TRACEPOINT(trace_syscall_close, "%d <= %d", int, int); +TRACEPOINT(trace_syscall_close_range, "%d <= %u %u %u", int, unsigned int, unsigned int, unsigned int); TRACEPOINT(trace_syscall_pipe2, "%d <= %p 0%0o", int, int *, int); #if CONF_core_epoll TRACEPOINT(trace_syscall_epoll_create1, "%d <= 0%0o", int, int); diff --git a/syscalls/syscalls.cc.in b/syscalls/syscalls.cc.in index a397b1eec0..19a560bbfc 100644 --- a/syscalls/syscalls.cc.in +++ b/syscalls/syscalls.cc.in @@ -8,6 +8,7 @@ SYSCALL2(clock_gettime, clockid_t, struct timespec *); SYSCALL2(clock_getres, clockid_t, struct timespec *); SYSCALL1(close, int); + SYSCALL3(close_range, unsigned int, unsigned int, unsigned int); SYSCALL2(pipe2, int *, int); #if CONF_core_epoll SYSCALL1(epoll_create1, int); diff --git a/tests/tst-close-range.cc b/tests/tst-close-range.cc new file mode 100644 index 0000000000..6da6c27d12 --- /dev/null +++ b/tests/tst-close-range.cc @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2026 Waldemar Kozaczuk + * + * This work is open source software, licensed under the terms of the + * BSD license as described in the LICENSE file in the top-level directory. + */ + +// Exercises close_range(2). Built and run as part of the OSv test image. + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include + +#include + +#include +#include + +#ifndef CLOSE_RANGE_CLOEXEC +#define CLOSE_RANGE_CLOEXEC (1U << 2) +#endif + +// Open n fds pointing at /dev/null and return the lowest one; they are +// contiguous because the fd table hands out the lowest free slots. +static int open_run(int n, int *fds) +{ + for (int i = 0; i < n; i++) { + fds[i] = open("/dev/null", O_RDONLY); + assert(fds[i] >= 0); + } + return fds[0]; +} + +int main() +{ + std::cerr << "Running close_range tests\n"; + + // Close a contiguous run of fds. + int fds[5]; + open_run(5, fds); + assert(close_range(fds[0], fds[4], 0) == 0); + // All of them are now closed: fcntl must report EBADF. + for (int i = 0; i < 5; i++) { + errno = 0; + assert(fcntl(fds[i], F_GETFD) == -1 && errno == EBADF); + } + + // A range that includes not-open fds is fine (Linux ignores them). + int a = open("/dev/null", O_RDONLY); + assert(a >= 0); + assert(close_range(a, a + 100, 0) == 0); + errno = 0; + assert(fcntl(a, F_GETFD) == -1 && errno == EBADF); + + // CLOSE_RANGE_CLOEXEC sets close-on-exec instead of closing. + int b = open("/dev/null", O_RDONLY); + assert(b >= 0); + assert((fcntl(b, F_GETFD) & FD_CLOEXEC) == 0); // not set yet + assert(close_range(b, b, CLOSE_RANGE_CLOEXEC) == 0); + assert(fcntl(b, F_GETFD) & FD_CLOEXEC); // now set + assert(fcntl(b, F_GETFD) != -1); // still open + close(b); + + // first > last is rejected; an unknown flag is rejected. + errno = 0; + assert(close_range(10, 5, 0) == -1 && errno == EINVAL); + errno = 0; + assert(close_range(0, 3, 0x40) == -1 && errno == EINVAL); + + std::cerr << "close_range tests PASSED\n"; + return 0; +}