Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions litebox_runner_linux_userland/tests/sigpipe_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

// Tests for SIGPIPE signal delivery

#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>

// Skip this test on 32-bit x86 due to signal handling issues
#if defined(__i386__)
int main(void) {
printf("Skipping SIGPIPE tests on 32-bit x86\n");
return 0;
}
#else

static volatile sig_atomic_t sigpipe_received = 0;

static void sigpipe_handler(int sig) {
(void)sig;
sigpipe_received = 1;
}

// Test 1: SIGPIPE on write to closed pipe
int test_pipe_sigpipe(void) {
int pipefd[2];
sigpipe_received = 0;

// Set up SIGPIPE handler
struct sigaction sa, old_sa;
sigemptyset(&sa.sa_mask);
sa.sa_handler = sigpipe_handler;
sa.sa_flags = 0;
if (sigaction(SIGPIPE, &sa, &old_sa) == -1) {
perror("sigaction");
return 1;
}

// Create pipe
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}

// Close read end
close(pipefd[0]);

// Write to pipe should get SIGPIPE and EPIPE
char buf[] = "test";
ssize_t ret = write(pipefd[1], buf, sizeof(buf));

if (ret != -1) {
fprintf(stderr, "FAIL: write should have returned -1, got %zd\n", ret);
return 1;
}
if (errno != EPIPE) {
fprintf(stderr, "FAIL: errno should be EPIPE (%d), got %d\n", EPIPE, errno);
return 1;
}
if (!sigpipe_received) {
fprintf(stderr, "FAIL: SIGPIPE handler was not called\n");
return 1;
}

close(pipefd[1]);
sigaction(SIGPIPE, &old_sa, NULL);
printf("test_pipe_sigpipe: PASS\n");
return 0;
}

// Test 2: SIGPIPE ignored (SIG_IGN)
int test_pipe_sigpipe_ignored(void) {
int pipefd[2];

// Set SIGPIPE to SIG_IGN
struct sigaction sa, old_sa;
sigemptyset(&sa.sa_mask);
sa.sa_handler = SIG_IGN;
sa.sa_flags = 0;
if (sigaction(SIGPIPE, &sa, &old_sa) == -1) {
perror("sigaction");
return 1;
}

// Create pipe
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}

// Close read end
close(pipefd[0]);

// Write to pipe should get EPIPE (no signal because ignored)
char buf[] = "test";
ssize_t ret = write(pipefd[1], buf, sizeof(buf));

if (ret != -1) {
fprintf(stderr, "FAIL: write should have returned -1, got %zd\n", ret);
return 1;
}
if (errno != EPIPE) {
fprintf(stderr, "FAIL: errno should be EPIPE (%d), got %d\n", EPIPE, errno);
return 1;
}

close(pipefd[1]);
sigaction(SIGPIPE, &old_sa, NULL);
printf("test_pipe_sigpipe_ignored: PASS\n");
return 0;
}

// Test 3: Unix socket MSG_NOSIGNAL flag
int test_unix_socket_nosignal(void) {
int sv[2];
sigpipe_received = 0;

// Set up SIGPIPE handler
struct sigaction sa, old_sa;
sigemptyset(&sa.sa_mask);
sa.sa_handler = sigpipe_handler;
sa.sa_flags = 0;
if (sigaction(SIGPIPE, &sa, &old_sa) == -1) {
perror("sigaction");
return 1;
}

// Create socket pair
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
perror("socketpair");
return 1;
}

// Close read end
close(sv[0]);

// Send with MSG_NOSIGNAL - should NOT generate SIGPIPE
char buf[] = "test";
ssize_t ret = send(sv[1], buf, sizeof(buf), MSG_NOSIGNAL);

if (ret != -1) {
fprintf(stderr, "FAIL: send should have returned -1, got %zd\n", ret);
return 1;
}
if (errno != EPIPE) {
fprintf(stderr, "FAIL: errno should be EPIPE (%d), got %d\n", EPIPE, errno);
return 1;
}
if (sigpipe_received) {
fprintf(stderr, "FAIL: SIGPIPE should NOT have been received with MSG_NOSIGNAL\n");
return 1;
}

close(sv[1]);
sigaction(SIGPIPE, &old_sa, NULL);
printf("test_unix_socket_nosignal: PASS\n");
return 0;
}

// Test 4: Unix socket without MSG_NOSIGNAL (should get SIGPIPE)
int test_unix_socket_sigpipe(void) {
int sv[2];
sigpipe_received = 0;

// Set up SIGPIPE handler
struct sigaction sa, old_sa;
sigemptyset(&sa.sa_mask);
sa.sa_handler = sigpipe_handler;
sa.sa_flags = 0;
if (sigaction(SIGPIPE, &sa, &old_sa) == -1) {
perror("sigaction");
return 1;
}

// Create socket pair
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
perror("socketpair");
return 1;
}

// Close read end
close(sv[0]);

// Send without MSG_NOSIGNAL - SHOULD generate SIGPIPE
char buf[] = "test";
ssize_t ret = send(sv[1], buf, sizeof(buf), 0);

if (ret != -1) {
fprintf(stderr, "FAIL: send should have returned -1, got %zd\n", ret);
return 1;
}
if (errno != EPIPE) {
fprintf(stderr, "FAIL: errno should be EPIPE (%d), got %d\n", EPIPE, errno);
return 1;
}
if (!sigpipe_received) {
fprintf(stderr, "FAIL: SIGPIPE handler should have been called\n");
return 1;
}

close(sv[1]);
sigaction(SIGPIPE, &old_sa, NULL);
printf("test_unix_socket_sigpipe: PASS\n");
return 0;
}

int main(void) {
int failures = 0;

failures += test_pipe_sigpipe();
failures += test_pipe_sigpipe_ignored();
failures += test_unix_socket_nosignal();
failures += test_unix_socket_sigpipe();

if (failures > 0) {
fprintf(stderr, "\n%d test(s) failed\n", failures);
return 1;
}

printf("\nAll SIGPIPE tests passed!\n");
return 0;
}

#endif // !defined(__i386__)
4 changes: 2 additions & 2 deletions litebox_shim_linux/src/syscalls/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ impl Task {
}
};
if let Err(Errno::EPIPE) = res {
unimplemented!("send SIGPIPE to the current task");
self.send_sigpipe();
}
res
}
Expand Down Expand Up @@ -604,7 +604,7 @@ impl Task {
Descriptor::Unix { .. } => todo!(),
};
if let Err(Errno::EPIPE) = res {
unimplemented!("send SIGPIPE to the current task");
self.send_sigpipe();
}
res
}
Expand Down
34 changes: 21 additions & 13 deletions litebox_shim_linux/src/syscalls/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,8 +741,8 @@ impl GlobalState {
let is_nonblock =
self.get_status(fd).contains(OFlags::NONBLOCK) || flags.contains(SendFlags::DONTWAIT);

let ret = cx
.with_timeout(timeout)
// Note: SIGPIPE is handled at the Task level (in do_sendto/do_sendmsg)
cx.with_timeout(timeout)
.wait_on_events(
is_nonblock,
Events::OUT,
Expand All @@ -756,13 +756,7 @@ impl GlobalState {
Err(e) => Err(TryOpError::Other(Errno::from(e))),
},
)
.map_err(Errno::from);
if let Err(Errno::EPIPE) = ret
&& !flags.contains(SendFlags::NOSIGNAL)
{
unimplemented!("send signal SIGPIPE on EPIPE");
}
ret
.map_err(Errno::from)
}

/// Receive data via socket channel (lock-free path).
Expand Down Expand Up @@ -1350,7 +1344,7 @@ impl Task {
sockaddr: Option<SocketAddress>,
) -> Result<usize, Errno> {
let buf = buf.to_owned_slice(len).ok_or(Errno::EFAULT)?;
self.files.borrow().with_socket(
let ret = self.files.borrow().with_socket(
sockfd,
|fd| {
let sockaddr = sockaddr
Expand All @@ -1367,7 +1361,14 @@ impl Task {
.transpose()?;
file.sendto(self, &buf, flags, addr)
},
)
);
// Send SIGPIPE on EPIPE for both INET and Unix sockets (unless MSG_NOSIGNAL is set)
if let Err(Errno::EPIPE) = ret
&& !flags.contains(SendFlags::NOSIGNAL)
{
self.send_sigpipe();
}
ret
}

/// Handle syscall `sendmsg`
Expand Down Expand Up @@ -1408,7 +1409,7 @@ impl Task {
.msg_iov
.to_owned_slice(msg.msg_iovlen)
.ok_or(Errno::EFAULT)?;
self.files.borrow().with_socket(
let ret = self.files.borrow().with_socket(
sockfd,
|fd| {
let sock_addr = sock_addr
Expand All @@ -1431,7 +1432,14 @@ impl Task {
Ok(total_sent)
},
|_file| Err(Errno::ENOTSOCK),
)
);
// Send SIGPIPE on EPIPE unless MSG_NOSIGNAL is set
if let Err(Errno::EPIPE) = ret
&& !flags.contains(SendFlags::NOSIGNAL)
{
self.send_sigpipe();
}
ret
}

/// Handle syscall `recvfrom`
Expand Down
20 changes: 20 additions & 0 deletions litebox_shim_linux/src/syscalls/signal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,4 +656,24 @@ impl Task {
self.signals.last_exception.set(*info);
self.force_signal_with_info(signal, false, siginfo_exception(signal, fault_address));
}

/// Sends SIGPIPE to the current task.
///
/// This is called when writing to a pipe or socket that has been closed on
/// the reading end. The signal will be delivered after the current syscall
/// returns, following normal signal delivery rules:
/// - If SIG_DFL: process terminates
/// - If SIG_IGN: signal ignored
/// - If custom handler: handler runs
pub(crate) fn send_sigpipe(&self) {
let siginfo = Siginfo {
signo: Signal::SIGPIPE.as_i32(),
errno: 0,
code: SI_KERNEL,
#[cfg(target_arch = "x86_64")]
__pad: 0,
data: SiginfoData::new_zeroed(),
};
self.send_signal(Signal::SIGPIPE, siginfo);
}
}
Loading