Skip to content
Merged
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
25 changes: 10 additions & 15 deletions crates/sandlock-core/src/network/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,17 @@ pub(super) async fn connect_on_behalf(
Err(e) => return NotifAction::Errno(e),
};

// Execute. Record the original destination *before* connect to prevent
// a TOCTOU race: the proxy may receive the request before we write the
// mapping if we did it after connect(). The IP comes from `addr_bytes`
// (our immune copy). The dup from the SO_PROTOCOL probe above is
// reused rather than pidfd_getfd-ing a second time.
if plan.record_orig_dest {
if let Some(ref map) = orig_dest_map {
// The local-address probe must bind in the socket's own
// family, which for a v4-mapped destination is AF_INET6
// even though the canonical `ip` is V4.
record_orig_dest(map, dup_fd.as_raw_fd(), sockaddr_is_ipv6(&addr_bytes), ip);
let metadata_addr = addr_bytes.clone();
NotifAction::prepare(&metadata_addr, dup_fd.as_raw_fd(), move || {
// Record the original destination before connect so the proxy
// cannot receive the connection before its mapping is available.
if plan.record_orig_dest {
if let Some(ref map) = orig_dest_map {
record_orig_dest(map, dup_fd.as_raw_fd(), sockaddr_is_ipv6(&addr_bytes), ip);
}
}
}
connect_dup(dup_fd.as_raw_fd(), &plan.addr)
// dup_fd dropped here, closing supervisor's copy
connect_dup(dup_fd.as_raw_fd(), &plan.addr)
})
} else {
// Non-IP family. A NAMED (pathname) AF_UNIX connect is a gap Landlock
// cannot close (it has no access right for unix-socket connect), so a
Expand Down Expand Up @@ -463,4 +459,3 @@ mod tests {
}

}

75 changes: 31 additions & 44 deletions crates/sandlock-core/src/network/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use super::materialize::{
named_unix_socket_path, parse_ip_from_sockaddr, parse_port_from_sockaddr, ChildMsghdr,
MaterializedMsg, MAX_SEND_BUF,
};
use super::send_engine::{batch_send_step, resolve_send, wants_blocking, BatchStep};
use super::send_engine::{prepare_batch, prepare_send, wants_blocking};
use super::unix::{
mmsg_entry_named_unix_path, sendmmsg_named_unix_on_behalf, sendto_named_unix_on_behalf,
sendto_pinned_unix_on_behalf, unix_sendmsg_gate,
Expand Down Expand Up @@ -103,7 +103,7 @@ pub(super) async fn sendto_on_behalf(
_pinned: None,
};
let blocking = wants_blocking(dup_fd.as_raw_fd(), flags);
resolve_send(dup_fd, m, flags, blocking)
prepare_send(dup_fd, m, flags, blocking)
} else {
// Non-IP family. Gate a NAMED AF_UNIX datagram the same way as connect:
// sendto to a named socket is a WRITE on its inode, so deny unless the
Expand Down Expand Up @@ -202,7 +202,7 @@ pub(super) async fn sendto_on_behalf(
_pinned: None,
};
let blocking = wants_blocking(dup_fd.as_raw_fd(), flags);
resolve_send(dup_fd, m, flags, blocking)
prepare_send(dup_fd, m, flags, blocking)
}
// Reject (a non-IP address on a non-unix socket — the
// address-family-swap shape — or an AF_UNIX address with no
Expand Down Expand Up @@ -289,7 +289,7 @@ pub(super) async fn sendmsg_on_behalf(
match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, msghdr_ptr).await {
Ok(m) => {
let blocking = wants_blocking(dup_fd.as_raw_fd(), flags);
resolve_send(dup_fd, m, flags, blocking)
prepare_send(dup_fd, m, flags, blocking)
}
Err(errno) => NotifAction::Errno(errno),
}
Expand Down Expand Up @@ -357,7 +357,7 @@ fn prescan_msghdr(
/// rather than escaping the allowlist.
///
/// Returns a [`MaterializedMsg`] the caller sends (inline and, if it would
/// block, deferred) via [`resolve_send`] / [`send_materialized`]; or an errno.
/// block, deferred) via [`prepare_send`] / [`send_materialized`]; or an errno.
/// ECONNREFUSED is used both for "destination blocked by policy" and for
/// "couldn't parse a port from the sockaddr"; EIO for sub-buffer read failures.
async fn send_msghdr_on_behalf(
Expand Down Expand Up @@ -508,7 +508,8 @@ pub(super) async fn sendmmsg_on_behalf(
// rather than refused with ECONNREFUSED. On-behalf (not Continue) keeps
// it TOCTOU-safe against a racing fd swap.
let protocol = query_socket_protocol(dup_fd.as_raw_fd());
let mut sent: usize = 0;
let mut entries = Vec::new();
let mut batch_bytes = 0usize;
let mut first_errno: Option<i32> = None;
for i in 0..vlen {
let entry_ptr = mmsg_entry_ptr(msgvec_ptr, i);
Expand All @@ -521,25 +522,19 @@ pub(super) async fn sendmmsg_on_behalf(
break;
}
};
match batch_send_step(
&dup_fd, m, flags, notif_fd, notif.id, notif.pid,
mmsg_msglen_addr(entry_ptr), sent,
) {
BatchStep::Sent => sent += 1,
BatchStep::Done(action) => return action,
BatchStep::Stop(errno) => {
if sent == 0 {
first_errno = Some(errno);
}
break;
}
let socket = match dup_fd.try_clone() {
Ok(socket) => socket,
Err(_) => { first_errno = Some(libc::EIO); break; }
};
batch_bytes += m.data.len();
if batch_bytes > super::materialize::MAX_SEND_BUF {
first_errno = Some(libc::EMSGSIZE);
break;
}
entries.push((socket, m, mmsg_msglen_addr(entry_ptr)));
}
return if sent > 0 {
NotifAction::ReturnValue(sent as i64)
} else {
NotifAction::Errno(first_errno.unwrap_or(ECONNREFUSED))
};
return prepare_batch(entries, flags, notif_fd, notif.id, notif.pid,
first_errno.unwrap_or(ECONNREFUSED));
}

// No destination policy: the connected fast path is safe (nothing to
Expand All @@ -565,7 +560,8 @@ pub(super) async fn sendmmsg_on_behalf(
None => return NotifAction::Errno(ECONNREFUSED),
};

let mut sent: usize = 0;
let mut entries = Vec::new();
let mut batch_bytes = 0usize;
let mut first_errno: Option<i32> = None;

for i in 0..vlen {
Expand All @@ -579,27 +575,18 @@ pub(super) async fn sendmmsg_on_behalf(
break;
}
};
match batch_send_step(
&dup_fd, m, flags, notif_fd, notif.id, notif.pid,
mmsg_msglen_addr(entry_ptr), sent,
) {
BatchStep::Sent => sent += 1,
BatchStep::Done(action) => return action,
BatchStep::Stop(errno) => {
if sent == 0 {
first_errno = Some(errno);
}
break;
}
let socket = match dup_fd.try_clone() {
Ok(socket) => socket,
Err(_) => { first_errno = Some(libc::EIO); break; }
};
batch_bytes += m.data.len();
if batch_bytes > super::materialize::MAX_SEND_BUF {
first_errno = Some(libc::EMSGSIZE);
break;
}
entries.push((socket, m, mmsg_msglen_addr(entry_ptr)));
}

if sent > 0 {
NotifAction::ReturnValue(sent as i64)
} else {
// Defensive: vlen > 0 + no successes means at least one attempt
// failed, so first_errno is set. Fall back to ECONNREFUSED
// rather than panicking on the unwrap if invariants ever drift.
NotifAction::Errno(first_errno.unwrap_or(ECONNREFUSED))
}
prepare_batch(entries, flags, notif_fd, notif.id, notif.pid,
first_errno.unwrap_or(ECONNREFUSED))
}
101 changes: 99 additions & 2 deletions crates/sandlock-core/src/network/send_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ fn send_materialized_at(fd: RawFd, m: &MaterializedMsg, offset: usize, flags: i3
unsafe { libc::sendmsg(fd, &msg, flags) }
}

/// Prepare a send without writing to the socket before callback approval.
pub(crate) fn prepare_send(dup_fd: OwnedFd, m: MaterializedMsg, flags: i32, child_blocking: bool) -> NotifAction {
NotifAction::prepare(&m.addr.clone(), dup_fd.as_raw_fd(), move || {
execute_send(dup_fd, m, flags, child_blocking)
})
}

/// Resolve a materialized send to a terminal action. The first attempt is
/// non-blocking (`MSG_DONTWAIT`) on the seccomp loop, so it never blocks there.
/// A non-blocking child gets whatever that one attempt returns (short count or
Expand All @@ -58,7 +65,7 @@ fn send_materialized_at(fd: RawFd, m: &MaterializedMsg, offset: usize, flags: i3
/// kernel's "a blocking send of N returns N" contract without occupying the
/// loop or a worker thread — a stream send that partially fit continues from
/// the sent offset; a full send buffer defers from offset 0.
pub(crate) fn resolve_send(dup_fd: OwnedFd, m: MaterializedMsg, flags: i32, child_blocking: bool) -> NotifAction {
fn execute_send(dup_fd: OwnedFd, m: MaterializedMsg, flags: i32, child_blocking: bool) -> NotifAction {
let ret = send_materialized_at(dup_fd.as_raw_fd(), &m, 0, flags | libc::MSG_DONTWAIT);
if ret >= 0 {
let sent = ret as usize;
Expand All @@ -78,6 +85,44 @@ pub(crate) fn resolve_send(dup_fd: OwnedFd, m: MaterializedMsg, flags: i32, chil
NotifAction::Errno(err)
}

pub(crate) fn prepare_batch(
entries: Vec<(OwnedFd, MaterializedMsg, u64)>,
flags: i32,
notif_fd: RawFd,
notif_id: u64,
notif_pid: u32,
error: i32,
) -> NotifAction {
let Some((socket, first, _)) = entries.first() else {
return NotifAction::Errno(error);
};
use super::materialize::{parse_ip_from_sockaddr, parse_port_from_sockaddr};
let extra_destinations = entries.iter().skip(1).map(|(_, message, _)| {
(parse_ip_from_sockaddr(&message.addr), parse_port_from_sockaddr(&message.addr))
}).collect();
let addr = first.addr.clone();
let fd = socket.as_raw_fd();
let mut action = NotifAction::prepare(&addr, fd, move || {
let mut sent = 0;
for (socket, message, msglen_addr) in entries {
match batch_send_step(&socket, message, flags, notif_fd, notif_id, notif_pid, msglen_addr, sent) {
BatchStep::Sent => sent += 1,
BatchStep::Done(action) => return action,
BatchStep::Stop(errno) => return if sent == 0 {
NotifAction::Errno(errno)
} else {
NotifAction::ReturnValue(sent as i64)
},
}
}
NotifAction::ReturnValue(sent as i64)
});
if let NotifAction::Prepared(operation) = &mut action {
operation.extra_destinations = extra_destinations;
}
action
}

/// Byte-level completion core: await writability on the dup'd fd through the
/// Tokio IO driver's epoll (never blocking a worker thread) and push the rest of
/// the message, advancing `offset` past each partial send, until the whole
Expand Down Expand Up @@ -117,7 +162,7 @@ async fn push_until_done(
}
}

/// Deferred tail of [`resolve_send`] for a single message: complete the send and
/// Deferred tail of [`prepare_send`] for a single message: complete the send and
/// return the byte count (matching a blocking send of N returning N; a partial
/// stream then error returns the partial count).
async fn defer_send(dup_fd: OwnedFd, m: MaterializedMsg, flags: i32, offset: usize) -> NotifAction {
Expand Down Expand Up @@ -253,3 +298,55 @@ pub(crate) fn batch_send_step(
}
BatchStep::Stop(err)
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use std::os::unix::net::UnixStream;

#[tokio::test]
async fn prepared_send_waits_for_approval_and_can_defer_completion() {
let (sender, mut receiver) = UnixStream::pair().unwrap();
receiver.set_nonblocking(true).unwrap();
let capacity: libc::c_int = 4096;
assert_eq!(unsafe {
libc::setsockopt(sender.as_raw_fd(), libc::SOL_SOCKET, libc::SO_SNDBUF,
&capacity as *const _ as *const libc::c_void,
std::mem::size_of_val(&capacity) as libc::socklen_t)
}, 0);
let data = vec![42; 1024 * 1024];
let expected = data.clone();
let message = MaterializedMsg {
data, addr: Vec::new(), control: None,
_scm_fds: Vec::new(), _pinned: None,
};
let action = prepare_send(sender.into(), message, 0, true);
let mut probe = [0; 1];
assert_eq!(receiver.read(&mut probe).unwrap_err().kind(), std::io::ErrorKind::WouldBlock);
let NotifAction::Prepared(operation) = action else { panic!("expected prepared send") };
let NotifAction::Defer(deferred) = operation.execute() else { panic!("expected partial send to defer") };
receiver.set_nonblocking(false).unwrap();
let reader = std::thread::spawn(move || {
let mut received = vec![0; expected.len()];
receiver.read_exact(&mut received).unwrap();
assert_eq!(received, expected);
});
assert!(matches!(deferred.run().await, NotifAction::ReturnValue(n) if n == 1024 * 1024));
reader.join().unwrap();
}

#[test]
fn dropping_prepared_send_has_no_side_effect() {
let (sender, mut receiver) = UnixStream::pair().unwrap();
receiver.set_nonblocking(true).unwrap();
let fd = sender.try_clone().unwrap().into();
let message = MaterializedMsg {
data: b"denied".to_vec(), addr: Vec::new(), control: None,
_scm_fds: Vec::new(), _pinned: None,
};
drop(prepare_send(fd, message, 0, false));
let mut buf = [0; 6];
assert_eq!(receiver.read(&mut buf).unwrap_err().kind(), std::io::ErrorKind::WouldBlock);
}
}
Loading
Loading