diff --git a/os/StarryOS/kernel/src/file/epoll.rs b/os/StarryOS/kernel/src/file/epoll.rs index 04f7a32ed3..df4cca5c96 100644 --- a/os/StarryOS/kernel/src/file/epoll.rs +++ b/os/StarryOS/kernel/src/file/epoll.rs @@ -23,7 +23,7 @@ use ax_kspin::SpinNoIrq; use axpoll::{IoEvents, PollSet, Pollable}; use bitflags::bitflags; use hashbrown::HashMap; -use linux_raw_sys::general::{EPOLLET, EPOLLONESHOT, epoll_event}; +use linux_raw_sys::general::{EPOLLET, EPOLLEXCLUSIVE, EPOLLONESHOT, epoll_event}; use crate::file::{FileLike, get_file_like}; @@ -38,6 +38,7 @@ bitflags! { pub struct EpollFlags: u32 { const EDGE_TRIGGER = EPOLLET; const ONESHOT = EPOLLONESHOT; + const EXCLUSIVE = EPOLLEXCLUSIVE; } } @@ -146,6 +147,7 @@ struct EpollInterest { key: EntryKey, event: EpollEvent, mode: SpinNoIrq, + exclusive: bool, in_ready_queue: AtomicBool, } @@ -155,10 +157,16 @@ impl EpollInterest { key, event, mode: SpinNoIrq::new(TriggerMode::from_flags(flags)), + exclusive: flags.contains(EpollFlags::EXCLUSIVE), in_ready_queue: AtomicBool::new(false), } } + #[inline] + fn is_exclusive(&self) -> bool { + self.exclusive + } + #[inline] fn is_enabled(&self) -> bool { self.mode.lock().is_enabled() @@ -353,6 +361,10 @@ impl Epoll { let mut guard = self.inner.interests.lock(); let old = guard.get_mut(&key).ok_or(AxError::NotFound)?; + // Linux forbids modifying an entry that was added as exclusive. + if old.is_exclusive() { + return Err(AxError::InvalidInput); + } // Preserve ready-queue membership across the swap. The ready_queue // only holds Weak pointing at the old Arc, so diff --git a/os/StarryOS/kernel/src/syscall/io_mpx/epoll.rs b/os/StarryOS/kernel/src/syscall/io_mpx/epoll.rs index f9e4fe9b6d..be5ba4dfab 100644 --- a/os/StarryOS/kernel/src/syscall/io_mpx/epoll.rs +++ b/os/StarryOS/kernel/src/syscall/io_mpx/epoll.rs @@ -8,7 +8,8 @@ use ax_task::future::{self, block_on, poll_io}; use axpoll::IoEvents; use bitflags::bitflags; use linux_raw_sys::general::{ - EPOLL_CLOEXEC, EPOLL_CTL_ADD, EPOLL_CTL_DEL, EPOLL_CTL_MOD, epoll_event, timespec, + EPOLL_CLOEXEC, EPOLL_CTL_ADD, EPOLL_CTL_DEL, EPOLL_CTL_MOD, EPOLLEXCLUSIVE, epoll_event, + timespec, }; use starry_signal::SignalSet; use starry_vm::{vm_read_slice, vm_write_slice}; @@ -25,6 +26,12 @@ use crate::{ }; const EP_MAX_EVENTS: usize = i32::MAX as usize / size_of::(); +const EPOLLEXCLUSIVE_OK_BITS: u32 = IoEvents::IN.bits() + | IoEvents::OUT.bits() + | IoEvents::ERR.bits() + | IoEvents::HUP.bits() + | EpollFlags::EDGE_TRIGGER.bits() + | EpollFlags::EXCLUSIVE.bits(); fn check_epoll_events_access(events: UserPtr, maxevents: usize) -> AxResult<()> { let len = maxevents @@ -111,12 +118,14 @@ pub fn sys_epoll_ctl( let epoll = Epoll::from_fd(epfd)?; debug!("sys_epoll_ctl <= epfd: {epfd}, op: {op}, fd: {fd}"); - let parse_event = || -> AxResult<(EpollEvent, EpollFlags)> { + let parse_event = || -> AxResult<(u32, EpollEvent, EpollFlags)> { let event = read_epoll_event(event)?; + let raw_events = event.events; let events = IoEvents::from_bits_truncate(event.events); let flags = - EpollFlags::from_bits(event.events & !events.bits()).ok_or(AxError::InvalidInput)?; + EpollFlags::from_bits(raw_events & !events.bits()).ok_or(AxError::InvalidInput)?; Ok(( + raw_events, EpollEvent { events, user_data: event.data, @@ -126,11 +135,21 @@ pub fn sys_epoll_ctl( }; match op { EPOLL_CTL_ADD => { - let (event, flags) = parse_event()?; + let (raw_events, event, flags) = parse_event()?; + // Linux only permits EPOLLEXCLUSIVE on ADD, and it is rejected for + // epoll targets as well. + if raw_events & EPOLLEXCLUSIVE != 0 + && (raw_events & !EPOLLEXCLUSIVE_OK_BITS != 0 || Epoll::from_fd(fd).is_ok()) + { + return Err(AxError::InvalidInput); + } epoll.add(fd, event, flags)?; } EPOLL_CTL_MOD => { - let (event, flags) = parse_event()?; + let (_, event, flags) = parse_event()?; + if flags.contains(EpollFlags::EXCLUSIVE) { + return Err(AxError::InvalidInput); + } epoll.modify(fd, event, flags)?; } EPOLL_CTL_DEL => { diff --git a/os/StarryOS/kernel/src/task/signal.rs b/os/StarryOS/kernel/src/task/signal.rs index 759199cdff..3f2336057f 100644 --- a/os/StarryOS/kernel/src/task/signal.rs +++ b/os/StarryOS/kernel/src/task/signal.rs @@ -472,7 +472,7 @@ pub fn send_signal_to_thread(tgid: Option, tid: Pid, sig: Option) -> AxResult<()> proc_data.clear_ptrace_stop(); } if let Some(tid) = proc_data.signal.send_signal(sig) { - // A thread was found that doesn't have the signal blocked — wake it. + // A thread was found that doesn't have the signal blocked. + // Mark it interrupted so blocking syscalls wrapped by + // `future::interruptible` can return EINTR promptly. if let Ok(task) = get_task(tid) { - ax_task::wake_task(&task); + task.interrupt(); } } else { // All threads have this signal blocked — the signal is now pending diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll-exclusive/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll-exclusive/c/CMakeLists.txt new file mode 100644 index 0000000000..38ce2b1bc5 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll-exclusive/c/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +project(test-epoll-exclusive C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(test-epoll-exclusive src/main.c) +target_include_directories(test-epoll-exclusive PRIVATE ../../common) +target_compile_options(test-epoll-exclusive PRIVATE -Wall -Wextra -Werror) + +install(TARGETS test-epoll-exclusive RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll-exclusive/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll-exclusive/c/src/main.c new file mode 100644 index 0000000000..18b601b5f0 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll-exclusive/c/src/main.c @@ -0,0 +1,110 @@ +#include "test_framework.h" + +#include +#include +#include + +static void close_pipe(int pipefd[2]) +{ + if (pipefd[0] >= 0) { + close(pipefd[0]); + } + if (pipefd[1] >= 0) { + close(pipefd[1]); + } +} + +int main(void) +{ + /* + * 回归目标: + * 1) Linux 合法的 EPOLLEXCLUSIVE ADD 必须被接受; + * 2) Linux 明确非法的 EPOLLEXCLUSIVE 组合必须返回 EINVAL。 + * 3) StarryOS 目前不放行 EPOLLWAKEUP,因此相关组合仍应返回 EINVAL。 + */ + TEST_START("epoll_ctl: EPOLLEXCLUSIVE Linux ABI checks"); + + int epfd = epoll_create1(EPOLL_CLOEXEC); + CHECK(epfd >= 0, "epoll_create1 succeeds"); + if (epfd < 0) { + TEST_DONE(); + } + + int ok_pipe[2] = {-1, -1}; + CHECK_RET(pipe(ok_pipe), 0, "pipe for EPOLLEXCLUSIVE positive case"); + if (ok_pipe[0] >= 0) { + struct epoll_event ev = { + .events = EPOLLIN | EPOLLEXCLUSIVE, + .data.fd = ok_pipe[0], + }; + CHECK_RET(epoll_ctl(epfd, EPOLL_CTL_ADD, ok_pipe[0], &ev), 0, + "epoll_ctl ADD with EPOLLEXCLUSIVE returns 0"); + + CHECK_ERR(epoll_ctl(epfd, EPOLL_CTL_MOD, ok_pipe[0], &ev), EINVAL, + "epoll_ctl MOD with EPOLLEXCLUSIVE returns EINVAL"); + + ev.events = EPOLLIN; + CHECK_ERR(epoll_ctl(epfd, EPOLL_CTL_MOD, ok_pipe[0], &ev), EINVAL, + "epoll_ctl MOD after exclusive ADD returns EINVAL"); + } + + int oneshot_pipe[2] = {-1, -1}; + CHECK_RET(pipe(oneshot_pipe), 0, "pipe for EPOLLONESHOT negative case"); + if (oneshot_pipe[0] >= 0) { + struct epoll_event ev = { + .events = EPOLLIN | EPOLLONESHOT | EPOLLEXCLUSIVE, + .data.fd = oneshot_pipe[0], + }; + CHECK_ERR(epoll_ctl(epfd, EPOLL_CTL_ADD, oneshot_pipe[0], &ev), EINVAL, + "epoll_ctl ADD rejects EPOLLONESHOT with EPOLLEXCLUSIVE"); + } + + int rdhup_pipe[2] = {-1, -1}; + CHECK_RET(pipe(rdhup_pipe), 0, "pipe for EPOLLRDHUP negative case"); + if (rdhup_pipe[0] >= 0) { + struct epoll_event ev = { + .events = EPOLLIN | EPOLLRDHUP | EPOLLEXCLUSIVE, + .data.fd = rdhup_pipe[0], + }; + CHECK_ERR(epoll_ctl(epfd, EPOLL_CTL_ADD, rdhup_pipe[0], &ev), EINVAL, + "epoll_ctl ADD rejects EPOLLRDHUP with EPOLLEXCLUSIVE"); + } + + int pri_pipe[2] = {-1, -1}; + CHECK_RET(pipe(pri_pipe), 0, "pipe for EPOLLPRI negative case"); + if (pri_pipe[0] >= 0) { + struct epoll_event ev = { + .events = EPOLLIN | EPOLLPRI | EPOLLEXCLUSIVE, + .data.fd = pri_pipe[0], + }; + CHECK_ERR(epoll_ctl(epfd, EPOLL_CTL_ADD, pri_pipe[0], &ev), EINVAL, + "epoll_ctl ADD rejects EPOLLPRI with EPOLLEXCLUSIVE"); + } + + int wake_pipe[2] = {-1, -1}; + CHECK_RET(pipe(wake_pipe), 0, "pipe for EPOLLWAKEUP negative case"); + if (wake_pipe[0] >= 0) { + struct epoll_event ev = { + .events = EPOLLIN | EPOLLWAKEUP | EPOLLEXCLUSIVE, + .data.fd = wake_pipe[0], + }; + CHECK_ERR(epoll_ctl(epfd, EPOLL_CTL_ADD, wake_pipe[0], &ev), EINVAL, + "epoll_ctl ADD rejects EPOLLWAKEUP with EPOLLEXCLUSIVE on StarryOS"); + } + + struct epoll_event ev = { + .events = EPOLLIN | EPOLLEXCLUSIVE, + .data.fd = epfd, + }; + CHECK_ERR(epoll_ctl(epfd, EPOLL_CTL_ADD, epfd, &ev), EINVAL, + "epoll_ctl ADD rejects epoll target with EPOLLEXCLUSIVE"); + + close_pipe(ok_pipe); + close_pipe(oneshot_pipe); + close_pipe(rdhup_pipe); + close_pipe(pri_pipe); + close_pipe(wake_pipe); + close(epfd); + + TEST_DONE(); +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/c/CMakeLists.txt new file mode 100644 index 0000000000..50c030feee --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/c/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +project(test-signal-interrupt-eintr C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(test-signal-interrupt-eintr src/main.c) +target_include_directories(test-signal-interrupt-eintr PRIVATE src) +target_compile_options(test-signal-interrupt-eintr PRIVATE -Wall -Wextra -Werror) + +install(TARGETS test-signal-interrupt-eintr RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/c/src/main.c new file mode 100644 index 0000000000..4ebbf3a172 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/c/src/main.c @@ -0,0 +1,151 @@ +#define _GNU_SOURCE + +/* + * test-signal-interrupt-eintr + * + * 测试目的: + * 1) 固定 StarryOS 信号打断阻塞 syscall 的 ABI 语义: + * 线程/进程阻塞在 interruptible 路径(本例用 poll)时, + * 收到可投递且未屏蔽信号后必须返回 -1,errno == EINTR。 + * 2) 避免仅依赖 nginx 多 worker 集成场景; + * 一旦 task.interrupt() 语义回退,本用例应直接失败。 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t got_usr1 = 0; + +static void on_usr1(int signo) +{ + if (signo == SIGUSR1) { + got_usr1 = 1; + } +} + +static int child_run(int notify_fd, int block_fd) +{ + /* + * 子进程安装可投递信号处理器: + * - 不设置 SA_RESTART,确保阻塞 syscall 被信号打断后返回 EINTR; + * - 该语义用于固定 task.interrupt() 的回归行为。 + */ + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = on_usr1; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGUSR1, &sa, NULL) != 0) { + perror("child: sigaction(SIGUSR1)"); + return 1; + } + + sigset_t empty; + sigemptyset(&empty); + if (sigprocmask(SIG_SETMASK, &empty, NULL) != 0) { + perror("child: sigprocmask(SIG_SETMASK)"); + return 1; + } + + char ready = 'R'; + /* 告知父进程:子进程已完成初始化,可开始触发信号。 */ + if (write(notify_fd, &ready, 1) != 1) { + perror("child: notify parent"); + return 1; + } + + struct pollfd pfd = { + .fd = block_fd, + .events = POLLIN, + }; + + errno = 0; + /* 关键断言:阻塞 poll 在 SIGUSR1 到达后必须返回 -1/EINTR。 */ + int r = poll(&pfd, 1, -1); + int e = errno; + if (r == -1 && e == EINTR && got_usr1) { + printf("PASS: poll interrupted by SIGUSR1 with EINTR\n"); + return 0; + } + + fprintf(stderr, + "FAIL: poll result mismatch: ret=%d errno=%d (%s) got_usr1=%d\n", + r, e, strerror(e), got_usr1); + return 1; +} + +int main(void) +{ + int block_pipe[2] = {-1, -1}; + int sync_pipe[2] = {-1, -1}; + if (pipe(block_pipe) != 0 || pipe(sync_pipe) != 0) { + perror("pipe"); + return 1; + } + + pid_t child = fork(); + if (child < 0) { + perror("fork"); + return 1; + } + + if (child == 0) { + close(sync_pipe[0]); + close(block_pipe[1]); + int rc = child_run(sync_pipe[1], block_pipe[0]); + close(sync_pipe[1]); + close(block_pipe[0]); + _exit(rc); + } + + close(sync_pipe[1]); + close(block_pipe[0]); + + /* + * 父子握手同步,避免用 sleep 猜时序: + * 只有收到子进程 ready 后,父进程才发送 SIGUSR1。 + */ + char ready = 0; + if (read(sync_pipe[0], &ready, 1) != 1 || ready != 'R') { + fprintf(stderr, "FAIL: parent failed to receive child ready signal\n"); + kill(child, SIGKILL); + waitpid(child, NULL, 0); + return 1; + } + + if (kill(child, SIGUSR1) != 0) { + perror("parent: kill(SIGUSR1)"); + kill(child, SIGKILL); + waitpid(child, NULL, 0); + return 1; + } + + int status = 0; + if (waitpid(child, &status, 0) != child) { + perror("parent: waitpid"); + return 1; + } + + close(sync_pipe[0]); + close(block_pipe[1]); + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + printf("ALL TESTS PASSED\n"); + return 0; + } + + if (WIFSIGNALED(status)) { + fprintf(stderr, "FAIL: child killed by signal %d\n", WTERMSIG(status)); + } else if (WIFEXITED(status)) { + fprintf(stderr, "FAIL: child exited with code %d\n", WEXITSTATUS(status)); + } else { + fprintf(stderr, "FAIL: unexpected child wait status=0x%x\n", status); + } + return 1; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-aarch64.toml new file mode 100644 index 0000000000..da57244e42 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-aarch64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", "-cpu", "cortex-a53", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-signal-interrupt-eintr" +success_regex = ["(?m)^ALL TESTS PASSED\\s*$"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)^FAIL:'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-loongarch64.toml b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-loongarch64.toml new file mode 100644 index 0000000000..09815bfe44 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-loongarch64.toml @@ -0,0 +1,17 @@ +args = [ + "-machine", "virt", + "-cpu", "la464", + "-nographic", + "-m", "128M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-loongarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-signal-interrupt-eintr" +success_regex = ["(?m)^ALL TESTS PASSED\\s*$"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)^FAIL:'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-riscv64.toml new file mode 100644 index 0000000000..e6288f4283 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-riscv64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-signal-interrupt-eintr" +success_regex = ["(?m)^ALL TESTS PASSED\\s*$"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)^FAIL:'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-x86_64.toml new file mode 100644 index 0000000000..1093258284 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-signal-interrupt-eintr/qemu-x86_64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-signal-interrupt-eintr" +success_regex = ["(?m)^ALL TESTS PASSED\\s*$"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)^FAIL:'] +timeout = 60