diff --git a/exported_symbols/osv_ld-musl.so.1.symbols b/exported_symbols/osv_ld-musl.so.1.symbols index d8621caad..201b124fc 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 3608b1788..658c51ae8 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 d4fc297c1..3a92ef49e 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 6e1ca3a0f..ce9deffe7 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 8d5c4485f..6667a1d83 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 5a1d7310e..f05a9ad2a 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 58a018e9e..87af2e6d0 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 391dbd9a8..6c9175075 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 772dfdcb9..551df8059 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 a397b1eec..19a560bbf 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 000000000..6da6c27d1 --- /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; +}