Skip to content
Draft
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
95 changes: 81 additions & 14 deletions crates/sandlock-core/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1408,7 +1408,25 @@ async fn sendto_on_behalf(
)
}
}
_ => NotifAction::Continue,
_ => {
// Non-IP destination with the unix fs-gate off (or an abstract
// unix address). Continue only if the socket is actually AF_UNIX:
// the kernel then constrains the send to AF_UNIX regardless of a
// racing addr swap, so an IP destination policy cannot be
// bypassed. An AF_INET socket presenting a non-IP address is the
// kernel's EAFNOSUPPORT; Continuing it (deciding on the transient
// address family rather than the stable socket domain) would let a
// swap to a denied IP ride out on the kernel's re-read.
let dup = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
Ok(fd) => fd,
Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
};
if socket_is_unix(dup.as_raw_fd()) {
NotifAction::Continue
} else {
NotifAction::Errno(libc::EAFNOSUPPORT)
}
}
}
}
}
Expand Down Expand Up @@ -1478,10 +1496,11 @@ async fn sendmsg_on_behalf(
let protocol = query_socket_protocol(dup_fd.as_raw_fd());

match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, msghdr_ptr).await {
Ok(m) => {
Ok(MsgOutcome::Send(m)) => {
let blocking = wants_blocking(dup_fd.as_raw_fd(), flags);
resolve_send(dup_fd, m, flags, blocking)
}
Ok(MsgOutcome::ContinueUnixDatagram) => NotifAction::Continue,
Err(errno) => NotifAction::Errno(errno),
}
}
Expand Down Expand Up @@ -1756,18 +1775,34 @@ fn complete_batch_entry(
/// (`ECONNREFUSED`), so an IP send whose protocol can't be resolved is refused
/// 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.
/// 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.
/// What the caller should do with one `msghdr` after the parse/decide phase.
enum MsgOutcome {
/// Send this materialized message on-behalf (inline, deferred if it blocks).
Send(MaterializedMsg),
/// A named (pathname/abstract) `AF_UNIX` datagram on an `AF_UNIX` socket with
/// the unix fs-gate off: the caller returns `NotifAction::Continue`. The IP
/// destination policy does not apply to a unix send, and with no fs grants
/// the target path is unrestricted, so the kernel handles it in the child's
/// namespace (an on-behalf send would resolve the path in ours). Gating on
/// the stable socket domain is TOCTOU-safe: the kernel constrains an
/// `AF_UNIX` socket to `AF_UNIX` destinations, so a racing `msg_name` swap to
/// an IP address cannot bypass the destination policy.
ContinueUnixDatagram,
}

/// Returns a [`MsgOutcome`]: a [`MaterializedMsg`] the caller sends (inline and,
/// if it would block, deferred) via [`resolve_send`], `Continue` for a named
/// unix datagram, 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(
notif: &SeccompNotif,
ctx: &Arc<SupervisorCtx>,
notif_fd: RawFd,
dup_fd: &std::os::unix::io::OwnedFd,
protocol: Option<Protocol>,
msghdr_ptr: u64,
) -> Result<MaterializedMsg, i32> {
) -> Result<MsgOutcome, i32> {
let msghdr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56) {
Ok(b) if b.len() >= 56 => b,
_ => return Err(libc::EFAULT),
Expand Down Expand Up @@ -1798,9 +1833,20 @@ async fn send_msghdr_on_behalf(
if !connected {
let ip = match parse_ip_from_sockaddr(&addr_bytes) {
Some(ip) => ip,
// A non-IP, non-connected address on an IP send path (e.g. the
// sockaddr changed under us). Fail closed.
None => return Err(libc::EAFNOSUPPORT),
// A non-IP, non-connected address. On an AF_UNIX socket this is a
// named unix datagram (the fs-gate-off case; the fs-gate-on case was
// handled by `unix_sendmsg_gate` before we got here) — Continue it,
// mirroring `sendto`, since the IP policy doesn't govern unix traffic
// and the kernel constrains an AF_UNIX socket to AF_UNIX destinations.
// An AF_INET socket with a non-IP address is the kernel's
// EAFNOSUPPORT; failing closed also avoids a racing swap to a denied
// IP riding out on a Continue.
None => {
if socket_is_unix(dup_fd.as_raw_fd()) {
return Ok(MsgOutcome::ContinueUnixDatagram);
}
return Err(libc::EAFNOSUPPORT);
}
};
let dest_port = parse_port_from_sockaddr(&addr_bytes);
// A non-connected IP send must have a resolved protocol to key the
Expand Down Expand Up @@ -1840,13 +1886,13 @@ async fn send_msghdr_on_behalf(
socket_is_unix(dup_fd.as_raw_fd()),
)?;

Ok(MaterializedMsg {
Ok(MsgOutcome::Send(MaterializedMsg {
data,
control: control_buf,
addr: if connected { Vec::new() } else { addr_bytes },
_scm_fds: scm_fds,
_pinned: None,
})
}))
}

// ============================================================
Expand Down Expand Up @@ -1956,7 +2002,18 @@ async fn sendmmsg_on_behalf(
let m = match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, entry_ptr)
.await
{
Ok(m) => m,
Ok(MsgOutcome::Send(m)) => m,
Ok(MsgOutcome::ContinueUnixDatagram) => {
// Named unix datagram batch on a unix socket (fs-gate off). A
// unix socket carries only unix entries, so this is reached at
// entry 0 with nothing sent yet — Continue the whole syscall.
// Defensive: if somehow reached mid-batch, report the count
// already delivered as a short send.
if sent == 0 {
return NotifAction::Continue;
}
break;
}
Err(errno) => {
first_errno = Some(errno);
break;
Expand Down Expand Up @@ -2036,7 +2093,17 @@ async fn sendmmsg_on_behalf(
// Every entry is OnBehalf (IP, non-connected) per the prescan above, so
// the resolved protocol is always required and present here.
let m = match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, Some(protocol), entry_ptr).await {
Ok(m) => m,
Ok(MsgOutcome::Send(m)) => m,
Ok(MsgOutcome::ContinueUnixDatagram) => {
// This loop has a resolved IP protocol, so the socket is not
// AF_UNIX and this arm is unreachable in practice; handle it as a
// whole-call Continue at entry 0 for exhaustiveness, else a short
// send.
if sent == 0 {
return NotifAction::Continue;
}
break;
}
Err(errno) => {
first_errno = Some(errno);
break;
Expand Down
Loading