Skip to content

Commit 3d56ce6

Browse files
committed
signal: implement signalfd(2)
signalfd() was stubbed to return ENOSYS, so event loops that consume signals through a file descriptor (a common pattern with epoll) could not run. Implement it as a pollable special_file (modeled on eventfd) backed by a global registry of live signalfd objects. OSv is a single process, so a signalfd watches a set of signals for the whole image. kill() now checks the registry first: if any signalfd is watching the raised signal, the signal is queued to those fds (as a signalfd_siginfo) and their pollers are woken, and the signal is consumed -- the default action and any handler are skipped. This matches Linux, where a signalfd-consumed signal must be blocked and is dequeued through the fd. - signalfd(-1, mask, flags) creates a new fd; signalfd(fd, mask, flags) updates an existing one's mask. SFD_CLOEXEC and SFD_NONBLOCK are honored. - read() returns as many queued signalfd_siginfo structures as fit in the buffer, blocking (or EAGAIN when non-blocking) when the queue is empty, and rejecting a buffer smaller than one siginfo with EINVAL. - poll() reports POLLIN when signals are queued. The delivery hook (osv_signalfd_deliver) is declared in <osv/signal.hh> and called from kill() in libc/signal.cc; the stub there is removed. Add tests/tst-signalfd.cc covering delivery of a watched signal to the fd (instead of the default poweroff action), the EAGAIN/poll/queue-order paths, and the EINVAL cases. Passes on OSv under KVM (1 and 2 vCPUs); tst-sigwait and tst-sigaction continue to pass (normal signal delivery is unaffected for signals no signalfd is watching).
1 parent 3aba46c commit 3d56ce6

6 files changed

Lines changed: 268 additions & 8 deletions

File tree

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,6 +2024,8 @@ libc += resource.o
20242024
libc += mount.o
20252025
libc += eventfd.o
20262026
libc_to_hide += eventfd.o
2027+
libc += signalfd.o
2028+
libc_to_hide += signalfd.o
20272029
libc += timerfd.o
20282030
libc_to_hide += timerfd.o
20292031
libc += shm.o

include/osv/signal.hh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,9 @@ public:
3434

3535
}
3636

37+
// signalfd delivery hook (libc/signalfd.cc). Called by kill() when a signal is
38+
// raised: returns true if a signalfd consumed the signal, in which case the
39+
// caller must not run the default action or a handler.
40+
extern "C" bool osv_signalfd_deliver(int signo);
41+
3742
#endif /* OSV_SIGNAL_HH_ */

libc/signal.cc

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <osv/mutex.h>
1616
#include <osv/condvar.h>
1717
#include <osv/power.hh>
18+
#include <osv/signal.hh>
1819
#include <osv/clock.hh>
1920
#include <api/setjmp.h>
2021
#include <osv/stubbing.hh>
@@ -364,6 +365,11 @@ int kill(pid_t pid, int sig)
364365
return -1;
365366
}
366367
unsigned sigidx = sig - 1;
368+
// If a signalfd is watching this signal, deliver it to the fd and consume
369+
// the signal (do not run the default action or a handler).
370+
if (osv_signalfd_deliver(sig)) {
371+
return 0;
372+
}
367373
if (is_sig_dfl(signal_actions[sigidx])) {
368374
// Our default is to power off.
369375
debugf("Uncaught signal %d (\"%s\"). Powering off.\n",
@@ -697,14 +703,6 @@ int sigaltstack(const stack_t *ss, stack_t *oss)
697703
return 0;
698704
}
699705

700-
extern "C" OSV_LIBC_API
701-
int signalfd(int fd, const sigset_t *mask, int flags)
702-
{
703-
WARN_STUBBED();
704-
errno = ENOSYS;
705-
return -1;
706-
}
707-
708706
extern "C" OSV_LIBC_API
709707
int sigwaitinfo(const sigset_t *__restrict mask,
710708
siginfo_t *__restrict si)

libc/signalfd.cc

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright (C) 2026 Waldemar Kozaczuk
3+
*
4+
* This work is open source software, licensed under the terms of the
5+
* BSD license as described in the LICENSE file in the top-level directory.
6+
*/
7+
8+
// signalfd(2): deliver signals to a file descriptor instead of via a handler.
9+
//
10+
// OSv is a single process, so a signalfd watches a set of signals for the whole
11+
// image. When kill() raises a signal that some signalfd is watching, the
12+
// signal is queued to those fds (and their pollers woken) instead of running
13+
// the default action or a handler -- matching Linux, where a signalfd-consumed
14+
// signal must be blocked and is dequeued through the fd.
15+
16+
#include <sys/signalfd.h>
17+
#include <fs/fs.hh>
18+
#include <libc/libc.hh>
19+
#include <osv/fcntl.h>
20+
#include <osv/mutex.h>
21+
#include <osv/condvar.h>
22+
#include <osv/poll.h>
23+
#include <osv/signal.hh>
24+
25+
#include <deque>
26+
#include <set>
27+
#include <signal.h>
28+
#include <errno.h>
29+
30+
namespace {
31+
32+
class signal_fd;
33+
34+
// Registry of all live signalfd objects, so kill() can find the ones watching a
35+
// given signal. Guarded by its own lock.
36+
mutex registry_lock;
37+
std::set<signal_fd*> registry;
38+
39+
class signal_fd final : public special_file {
40+
public:
41+
signal_fd(const sigset_t& mask, int flags)
42+
: special_file(FREAD | flags, DTYPE_UNSPEC), _mask(mask)
43+
{
44+
WITH_LOCK(registry_lock) { registry.insert(this); }
45+
}
46+
47+
int close() override {
48+
WITH_LOCK(registry_lock) { registry.erase(this); }
49+
return 0;
50+
}
51+
52+
int read(struct uio* uio, int flags) override;
53+
int poll(int events) override;
54+
55+
// Is this fd watching signal `signo` (1-based)?
56+
bool watches(int signo) const {
57+
return sigismember(&_mask, signo) == 1;
58+
}
59+
60+
// Called from the signal path to enqueue a delivered signal.
61+
void deliver(const signalfd_siginfo& si) {
62+
WITH_LOCK(_mutex) {
63+
_queue.push_back(si);
64+
_blocked_reader.wake_all();
65+
}
66+
poll_wake(this, POLLIN);
67+
}
68+
69+
void set_mask(const sigset_t& mask) { _mask = mask; }
70+
71+
private:
72+
mutable mutex _mutex;
73+
sigset_t _mask;
74+
condvar _blocked_reader;
75+
std::deque<signalfd_siginfo> _queue;
76+
};
77+
78+
int signal_fd::read(struct uio* uio, int flags)
79+
{
80+
if (uio->uio_resid < (ssize_t)sizeof(signalfd_siginfo)) {
81+
return EINVAL;
82+
}
83+
84+
WITH_LOCK(_mutex) {
85+
while (_queue.empty()) {
86+
if (f_flags & FNONBLOCK) {
87+
return EAGAIN;
88+
}
89+
_blocked_reader.wait(_mutex);
90+
}
91+
// Copy out as many queued siginfos as fit in the caller's buffer.
92+
while (!_queue.empty() &&
93+
uio->uio_resid >= (ssize_t)sizeof(signalfd_siginfo)) {
94+
signalfd_siginfo si = _queue.front();
95+
_queue.pop_front();
96+
int err = uiomove(&si, sizeof(si), uio);
97+
if (err) {
98+
return err;
99+
}
100+
}
101+
}
102+
return 0;
103+
}
104+
105+
int signal_fd::poll(int events)
106+
{
107+
int rc = 0;
108+
WITH_LOCK(_mutex) {
109+
if (!_queue.empty() && (events & POLLIN)) {
110+
rc |= POLLIN;
111+
}
112+
}
113+
return rc;
114+
}
115+
116+
} // anonymous namespace
117+
118+
// Called by kill() (libc/signal.cc) when a signal is raised. Returns true if at
119+
// least one signalfd consumed the signal, in which case the caller must not run
120+
// the default action or a handler.
121+
bool osv_signalfd_deliver(int signo)
122+
{
123+
signalfd_siginfo si;
124+
memset(&si, 0, sizeof(si));
125+
si.ssi_signo = signo;
126+
si.ssi_pid = getpid();
127+
128+
bool delivered = false;
129+
WITH_LOCK(registry_lock) {
130+
for (auto* sfd : registry) {
131+
if (sfd->watches(signo)) {
132+
sfd->deliver(si);
133+
delivered = true;
134+
}
135+
}
136+
}
137+
return delivered;
138+
}
139+
140+
extern "C" OSV_LIBC_API
141+
int signalfd(int fd, const sigset_t* mask, int flags)
142+
{
143+
if (!mask || (flags & ~(SFD_CLOEXEC | SFD_NONBLOCK))) {
144+
return libc_error(EINVAL);
145+
}
146+
147+
int of = 0;
148+
if (flags & SFD_NONBLOCK) {
149+
of |= O_NONBLOCK;
150+
}
151+
if (flags & SFD_CLOEXEC) {
152+
of |= O_CLOEXEC;
153+
}
154+
155+
if (fd == -1) {
156+
// Create a new signalfd.
157+
try {
158+
fileref f = make_file<signal_fd>(*mask, of);
159+
fdesc newfd(f);
160+
return newfd.release();
161+
} catch (int error) {
162+
return libc_error(error);
163+
}
164+
}
165+
166+
// Update the mask of an existing signalfd.
167+
fileref f(fileref_from_fd(fd));
168+
if (!f) {
169+
return libc_error(EBADF);
170+
}
171+
auto* sfd = dynamic_cast<signal_fd*>(f.get());
172+
if (!sfd) {
173+
return libc_error(EINVAL);
174+
}
175+
sfd->set_mask(*mask);
176+
return fd;
177+
}

modules/tests/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-bsd-evh.so \
122122
tst-mmap-file.so misc-mmap-big-file.so tst-mmap.so tst-huge.so \
123123
tst-prctl.so \
124124
tst-mmap-file-cow.so \
125+
tst-signalfd.so \
125126
tst-elf-permissions.so misc-mutex.so misc-sockets.so tst-condvar.so \
126127
tst-queue-mpsc.so tst-af-local.so tst-pipe.so tst-yield.so \
127128
misc-ctxsw.so tst-read.so tst-symlink.so tst-openat.so \

tests/tst-signalfd.cc

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (C) 2026 Waldemar Kozaczuk
3+
*
4+
* This work is open source software, licensed under the terms of the
5+
* BSD license as described in the LICENSE file in the top-level directory.
6+
*/
7+
8+
// Exercises signalfd(2): a signal raised while a signalfd watches it is
9+
// delivered to the fd (and consumed) rather than running the default action.
10+
// Built and run as part of the OSv test image.
11+
12+
#include <sys/signalfd.h>
13+
#include <sys/poll.h>
14+
#include <signal.h>
15+
#include <unistd.h>
16+
#include <errno.h>
17+
18+
#include <cassert>
19+
#include <cstring>
20+
#include <iostream>
21+
22+
int main()
23+
{
24+
std::cerr << "Running signalfd tests\n";
25+
26+
// Create a signalfd watching SIGUSR1 and SIGUSR2.
27+
sigset_t mask;
28+
sigemptyset(&mask);
29+
sigaddset(&mask, SIGUSR1);
30+
sigaddset(&mask, SIGUSR2);
31+
int sfd = signalfd(-1, &mask, SFD_NONBLOCK);
32+
assert(sfd >= 0);
33+
34+
// Nothing pending yet: a nonblocking read returns EAGAIN.
35+
signalfd_siginfo si;
36+
ssize_t n = read(sfd, &si, sizeof(si));
37+
assert(n == -1 && errno == EAGAIN);
38+
39+
// Raise SIGUSR1: it must be delivered to the fd, not the default action
40+
// (which would power the guest off).
41+
assert(kill(getpid(), SIGUSR1) == 0);
42+
43+
n = read(sfd, &si, sizeof(si));
44+
assert(n == (ssize_t)sizeof(si));
45+
assert(si.ssi_signo == (uint32_t)SIGUSR1);
46+
47+
// Raise SIGUSR2, then verify poll() reports it readable, then read it.
48+
assert(kill(getpid(), SIGUSR2) == 0);
49+
struct pollfd pfd { sfd, POLLIN, 0 };
50+
assert(poll(&pfd, 1, 1000) == 1);
51+
assert(pfd.revents & POLLIN);
52+
n = read(sfd, &si, sizeof(si));
53+
assert(n == (ssize_t)sizeof(si));
54+
assert(si.ssi_signo == (uint32_t)SIGUSR2);
55+
56+
// Two signals queued before a read: both should come out in order.
57+
assert(kill(getpid(), SIGUSR1) == 0);
58+
assert(kill(getpid(), SIGUSR2) == 0);
59+
signalfd_siginfo two[2];
60+
n = read(sfd, two, sizeof(two));
61+
assert(n == (ssize_t)sizeof(two));
62+
assert(two[0].ssi_signo == (uint32_t)SIGUSR1);
63+
assert(two[1].ssi_signo == (uint32_t)SIGUSR2);
64+
65+
// A short buffer (< one siginfo) is rejected with EINVAL.
66+
char tiny[8];
67+
n = read(sfd, tiny, sizeof(tiny));
68+
assert(n == -1 && errno == EINVAL);
69+
70+
// Bad flags rejected.
71+
errno = 0;
72+
assert(signalfd(-1, &mask, 0x40) == -1 && errno == EINVAL);
73+
74+
close(sfd);
75+
std::cerr << "signalfd tests PASSED\n";
76+
return 0;
77+
}

0 commit comments

Comments
 (0)