Skip to content

Commit e235833

Browse files
committed
signal: implement signalfd(2)
Replace the ENOSYS signalfd() stub with a real implementation: create (or update) a file descriptor that a program reads to receive signals as signalfd_siginfo records instead of via a handler. - signal_fd is a special_file holding the watched sigset and a queue of siginfos; read() copies out as many queued records as fit (blocking or EAGAIN under SFD_NONBLOCK), poll() reports POLLIN when the queue is non-empty. read() peeks-copies-then-dequeues so a failed copy-out cannot drop a queued signal. - kill()/the signal-raise path (libc/signal.cc) calls osv_signalfd_deliver(): if any signalfd watches the signal it is queued to those fds (pollers woken) and the signal is consumed (no default action / handler). The old WARN_STUBBED signalfd() in libc/signal.cc is removed. - signalfd() updating an existing fd's mask takes registry_lock, the same lock the dispatch path holds while reading the mask, so a mask update cannot race a delivery. Note on Linux fidelity: on Linux a signal is consumed by a signalfd only when it is blocked in the caller's mask; OSv's signal model is already a simplification and does not track a precise per-thread blocked mask here, so a watched signal is consumed unconditionally. Sufficient for the common use (block the signals, read them through the fd); documented in the source. Covered by tst-signalfd (which blocks the signals first, as a real signalfd user does, so it is meaningful on Linux too).
1 parent 62c7d90 commit e235833

6 files changed

Lines changed: 295 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>
@@ -412,6 +413,11 @@ int kill(pid_t pid, int sig)
412413
return -1;
413414
}
414415
unsigned sigidx = sig - 1;
416+
// If a signalfd is watching this signal, deliver it to the fd and consume
417+
// the signal (do not run the default action or a handler).
418+
if (osv_signalfd_deliver(sig)) {
419+
return 0;
420+
}
415421
if (is_sig_dfl(signal_actions[sigidx])) {
416422
// Our default is to power off.
417423
debugf("Uncaught signal %d (\"%s\"). Powering off.\n",
@@ -745,14 +751,6 @@ int sigaltstack(const stack_t *ss, stack_t *oss)
745751
return 0;
746752
}
747753

748-
extern "C" OSV_LIBC_API
749-
int signalfd(int fd, const sigset_t *mask, int flags)
750-
{
751-
WARN_STUBBED();
752-
errno = ENOSYS;
753-
return -1;
754-
}
755-
756754
extern "C" OSV_LIBC_API
757755
int sigwaitinfo(const sigset_t *__restrict mask,
758756
siginfo_t *__restrict si)

libc/signalfd.cc

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright (C) 2026 Greg Burd
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.
14+
//
15+
// Note on Linux fidelity: on Linux a signal is consumed by a signalfd only
16+
// when it is blocked in the caller's signal mask; an unblocked signal still
17+
// runs its handler/default action. OSv's signal model is already a
18+
// simplification (see libc/signal.cc - kill() runs the handler on a fresh
19+
// thread rather than a designated one), and it does not track a precise
20+
// per-thread blocked mask here, so a watched signal is consumed by the
21+
// signalfd unconditionally. This is sufficient for the common use (an app
22+
// blocks the signals it hands to a signalfd and reads them through the fd) but
23+
// is not bit-for-bit Linux semantics.
24+
25+
#include <sys/signalfd.h>
26+
#include <fs/fs.hh>
27+
#include <libc/libc.hh>
28+
#include <osv/fcntl.h>
29+
#include <osv/mutex.h>
30+
#include <osv/condvar.h>
31+
#include <osv/poll.h>
32+
#include <osv/signal.hh>
33+
34+
#include <unistd.h>
35+
#include <string.h>
36+
37+
#include <deque>
38+
#include <set>
39+
#include <signal.h>
40+
#include <errno.h>
41+
42+
namespace {
43+
44+
class signal_fd;
45+
46+
// Registry of all live signalfd objects, so kill() can find the ones watching a
47+
// given signal. Guarded by its own lock.
48+
mutex registry_lock;
49+
std::set<signal_fd*> registry;
50+
51+
class signal_fd final : public special_file {
52+
public:
53+
signal_fd(const sigset_t& mask, int flags)
54+
: special_file(FREAD | flags, DTYPE_UNSPEC), _mask(mask)
55+
{
56+
WITH_LOCK(registry_lock) { registry.insert(this); }
57+
}
58+
59+
int close() override {
60+
WITH_LOCK(registry_lock) { registry.erase(this); }
61+
return 0;
62+
}
63+
64+
int read(struct uio* uio, int flags) override;
65+
int poll(int events) override;
66+
67+
// Is this fd watching signal `signo` (1-based)?
68+
bool watches(int signo) const {
69+
return sigismember(&_mask, signo) == 1;
70+
}
71+
72+
// Called from the signal path to enqueue a delivered signal.
73+
void deliver(const signalfd_siginfo& si) {
74+
WITH_LOCK(_mutex) {
75+
_queue.push_back(si);
76+
_blocked_reader.wake_all();
77+
}
78+
poll_wake(this, POLLIN);
79+
}
80+
81+
// Update the watched-signal set. Taken under registry_lock (the same lock
82+
// held while the signal-dispatch path calls watches()), so a concurrent
83+
// signalfd() mask update cannot race a delivery's read of _mask.
84+
void set_mask(const sigset_t& mask) {
85+
WITH_LOCK(registry_lock) { _mask = mask; }
86+
}
87+
88+
private:
89+
mutable mutex _mutex;
90+
sigset_t _mask;
91+
condvar _blocked_reader;
92+
std::deque<signalfd_siginfo> _queue;
93+
};
94+
95+
int signal_fd::read(struct uio* uio, int flags)
96+
{
97+
if (uio->uio_resid < (ssize_t)sizeof(signalfd_siginfo)) {
98+
return EINVAL;
99+
}
100+
101+
WITH_LOCK(_mutex) {
102+
while (_queue.empty()) {
103+
if (f_flags & FNONBLOCK) {
104+
return EAGAIN;
105+
}
106+
_blocked_reader.wait(_mutex);
107+
}
108+
// Copy out as many queued siginfos as fit in the caller's buffer.
109+
// Peek, copy, and only dequeue after a successful copy so a failed
110+
// copy-out never silently drops a queued signal. (OSv's uiomove uses
111+
// memcpy and does not currently fail, but keep the ordering correct.)
112+
while (!_queue.empty() &&
113+
uio->uio_resid >= (ssize_t)sizeof(signalfd_siginfo)) {
114+
signalfd_siginfo si = _queue.front();
115+
int err = uiomove(&si, sizeof(si), uio);
116+
if (err) {
117+
// If nothing was copied yet, report the error; otherwise the
118+
// bytes already copied are a valid short read.
119+
return err;
120+
}
121+
_queue.pop_front();
122+
}
123+
}
124+
return 0;
125+
}
126+
127+
int signal_fd::poll(int events)
128+
{
129+
int rc = 0;
130+
WITH_LOCK(_mutex) {
131+
if (!_queue.empty() && (events & POLLIN)) {
132+
rc |= POLLIN;
133+
}
134+
}
135+
return rc;
136+
}
137+
138+
} // anonymous namespace
139+
140+
// Called by kill() (libc/signal.cc) when a signal is raised. Returns true if at
141+
// least one signalfd consumed the signal, in which case the caller must not run
142+
// the default action or a handler.
143+
bool osv_signalfd_deliver(int signo)
144+
{
145+
signalfd_siginfo si;
146+
memset(&si, 0, sizeof(si));
147+
si.ssi_signo = signo;
148+
si.ssi_pid = getpid();
149+
150+
bool delivered = false;
151+
WITH_LOCK(registry_lock) {
152+
for (auto* sfd : registry) {
153+
if (sfd->watches(signo)) {
154+
sfd->deliver(si);
155+
delivered = true;
156+
}
157+
}
158+
}
159+
return delivered;
160+
}
161+
162+
extern "C" OSV_LIBC_API
163+
int signalfd(int fd, const sigset_t* mask, int flags)
164+
{
165+
if (!mask || (flags & ~(SFD_CLOEXEC | SFD_NONBLOCK))) {
166+
return libc_error(EINVAL);
167+
}
168+
169+
int of = 0;
170+
if (flags & SFD_NONBLOCK) {
171+
of |= O_NONBLOCK;
172+
}
173+
if (flags & SFD_CLOEXEC) {
174+
of |= O_CLOEXEC;
175+
}
176+
177+
if (fd == -1) {
178+
// Create a new signalfd.
179+
try {
180+
fileref f = make_file<signal_fd>(*mask, of);
181+
fdesc newfd(f);
182+
return newfd.release();
183+
} catch (int error) {
184+
return libc_error(error);
185+
}
186+
}
187+
188+
// Update the mask of an existing signalfd.
189+
fileref f(fileref_from_fd(fd));
190+
if (!f) {
191+
return libc_error(EBADF);
192+
}
193+
auto* sfd = dynamic_cast<signal_fd*>(f.get());
194+
if (!sfd) {
195+
return libc_error(EINVAL);
196+
}
197+
sfd->set_mask(*mask);
198+
return fd;
199+
}

modules/tests/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-bsd-evh.so \
123123
tst-pthread-timedlock.so \
124124
tst-signal-fills.so \
125125
tst-epoll-pwait2.so \
126+
tst-signalfd.so \
126127
tst-prctl.so \
127128
tst-mmap-file-cow.so \
128129
tst-elf-permissions.so misc-mutex.so misc-sockets.so tst-condvar.so \

tests/tst-signalfd.cc

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (C) 2026 Greg Burd
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+
// Block these signals first, as an application using signalfd is expected
32+
// to (on Linux the signal must be blocked to be consumed via the fd rather
33+
// than run its handler/default action). This keeps the test meaningful on
34+
// Linux too.
35+
assert(sigprocmask(SIG_BLOCK, &mask, nullptr) == 0);
36+
int sfd = signalfd(-1, &mask, SFD_NONBLOCK);
37+
assert(sfd >= 0);
38+
39+
// Nothing pending yet: a nonblocking read returns EAGAIN.
40+
signalfd_siginfo si;
41+
ssize_t n = read(sfd, &si, sizeof(si));
42+
assert(n == -1 && errno == EAGAIN);
43+
44+
// Raise SIGUSR1: it must be delivered to the fd, not the default action
45+
// (which would power the guest off).
46+
assert(kill(getpid(), SIGUSR1) == 0);
47+
48+
n = read(sfd, &si, sizeof(si));
49+
assert(n == (ssize_t)sizeof(si));
50+
assert(si.ssi_signo == (uint32_t)SIGUSR1);
51+
52+
// Raise SIGUSR2, then verify poll() reports it readable, then read it.
53+
assert(kill(getpid(), SIGUSR2) == 0);
54+
struct pollfd pfd { sfd, POLLIN, 0 };
55+
assert(poll(&pfd, 1, 1000) == 1);
56+
assert(pfd.revents & POLLIN);
57+
n = read(sfd, &si, sizeof(si));
58+
assert(n == (ssize_t)sizeof(si));
59+
assert(si.ssi_signo == (uint32_t)SIGUSR2);
60+
61+
// Two signals queued before a read: both should come out in order.
62+
assert(kill(getpid(), SIGUSR1) == 0);
63+
assert(kill(getpid(), SIGUSR2) == 0);
64+
signalfd_siginfo two[2];
65+
n = read(sfd, two, sizeof(two));
66+
assert(n == (ssize_t)sizeof(two));
67+
assert(two[0].ssi_signo == (uint32_t)SIGUSR1);
68+
assert(two[1].ssi_signo == (uint32_t)SIGUSR2);
69+
70+
// A short buffer (< one siginfo) is rejected with EINVAL.
71+
char tiny[8];
72+
n = read(sfd, tiny, sizeof(tiny));
73+
assert(n == -1 && errno == EINVAL);
74+
75+
// Bad flags rejected.
76+
errno = 0;
77+
assert(signalfd(-1, &mask, 0x40) == -1 && errno == EINVAL);
78+
79+
close(sfd);
80+
std::cerr << "signalfd tests PASSED\n";
81+
return 0;
82+
}

0 commit comments

Comments
 (0)