Skip to content
Open
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
33 changes: 33 additions & 0 deletions crates/sandlock-core/src/chroot/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,39 @@ fn resolve_chroot_path_existing(
resolve_existing_in_root(ctx.root, &full_path)
}

/// Pin the socket inode a named unix address reaches in the sandbox's view
/// and require write access to it, as the kernel's connect does.
///
/// The child's real root and cwd are the host's, so the kernel cannot be left
/// to resolve the address: the grant is checked on where the pinned inode
/// actually sits, after symlinks inside the root are followed.
pub(crate) fn pin_writable_unix_target(
notif: &SeccompNotif,
ctx: &ChrootCtx<'_>,
sun_path: &Path,
) -> Result<OwnedFd, i32> {
let vpath = build_virtual_path(
notif,
libc::AT_FDCWD as i64,
sun_path.to_str().ok_or(libc::ECONNREFUSED)?,
ctx,
)
.ok_or(libc::ECONNREFUSED)?;
let confined = confine(&vpath);
let (root, sub) = match ctx.mount_target(&confined) {
Some((mount, sub)) => (mount, sub),
None => (ctx.root, confined.to_string_lossy().into_owned()),
};
let fd = openat2_in_root(root, &sub, libc::O_PATH | libc::O_CLOEXEC, 0)
.map_err(|_| libc::ECONNREFUSED)?;
let pinned = unsafe { OwnedFd::from_raw_fd(fd) };
let host = std::fs::read_link(format!("/proc/self/fd/{}", fd)).map_err(|_| libc::EACCES)?;
match ctx.host_to_virtual(&host) {
Some(virtual_path) if ctx.can_write(&virtual_path) => Ok(pinned),
_ => Err(libc::EACCES),
}
}

/// Convert a Path to CString, returning Errno on failure.
fn path_cstr(path: &Path, err: i32) -> Result<CString, NotifAction> {
CString::new(path.to_str().unwrap_or("")).map_err(|_| NotifAction::Errno(err))
Expand Down
49 changes: 20 additions & 29 deletions crates/sandlock-core/src/network/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use super::materialize::{
};
use super::query_socket_protocol;
use super::unix::connect_named_unix_on_behalf;
use super::verdict::{classify_destination, path_under_any, Destination};
use super::verdict::{classify_destination, Destination};

// ============================================================
// connect_on_behalf — perform connect() on behalf of the child (TOCTOU-safe)
Expand Down Expand Up @@ -102,14 +102,22 @@ pub(super) async fn connect_on_behalf(

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);
// A blocking connect waits on the peer, which must not stall the loop.
NotifAction::native(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)
connect_dup(dup_fd.as_raw_fd(), &plan.addr)
})
})
.held_if(held)
} else {
Expand All @@ -126,28 +134,11 @@ pub(super) async fn connect_on_behalf(
// never return Continue on the deny path, so it is TOCTOU-safe.
// Abstract sockets (no path) are handled by the Landlock abstract scope.
match named_unix_socket_path(&addr_bytes) {
// Resolve the symlink-followed target and connect on-behalf to
// the pinned inode, so a symlink inside a granted dir cannot
// redirect to an ungranted socket.
Some(path) if ctx.policy.supervises_unix_connect() => {
if ctx.policy.chroot_root.is_some() {
// Chroot mode: the child's paths are virtual, so a lexical
// check against the (virtual) write grants is consistent,
// and host socket paths are absent from the chroot view
// anyway. Deny unless under a write grant.
if path_under_any(&path, &ctx.policy.chroot_writable) {
NotifAction::Continue
} else {
NotifAction::Errno(libc::EACCES)
}
} else {
// Non-chroot: resolve the symlink-followed real target and
// connect on-behalf to the pinned inode, so a symlink inside
// a granted dir cannot redirect to an ungranted socket.
connect_named_unix_on_behalf(
notif.pid,
sockfd,
&path,
&ctx.policy.chroot_writable,
)
}
connect_named_unix_on_behalf(notif, ctx, sockfd, &path)
}
// Abstract/unnamed socket, non-AF_UNIX family, or gate disabled.
_ => NotifAction::Continue,
Expand Down
13 changes: 6 additions & 7 deletions crates/sandlock-core/src/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,18 @@ fn socket_is_unix(fd: RawFd) -> bool {
/// 1. Non-IP families (AF_UNIX etc.): the IP policy doesn't apply;
/// Landlock IPC scoping is the enforcement boundary. One sub-case is
/// narrower: `sendto`'s non-IP fall-through does NOT Continue once a
/// destination policy is active. That fall-through has no chroot check —
/// it is whatever misses the named-path gate, i.e. a pathless AF_UNIX or a
/// non-AF_UNIX destination — so it fails closed under `--chroot` too.
/// destination policy is active. That fall-through is whatever misses the
/// named-path gate, i.e. a pathless AF_UNIX or a non-AF_UNIX destination,
/// so it fails closed under `--chroot` too.
/// There the send goes on-behalf on a
/// pinned fd — a named unix target resolved in the child's root view, or a
/// non-unix destination on a unix socket (the NETLINK_ROUTE
/// virtualization) — or fails closed with EAFNOSUPPORT for an AF_UNIX
/// address with no pathname to pin, notably an abstract one: the
/// supervisor has no Landlock domain, so an on-behalf abstract send would
/// escape the child's scope. The other non-IP arms still Continue under a
/// destination policy — `connect`'s abstract/unnamed/non-AF_UNIX arm and
/// both chroot named-unix arms (`connect`, `sendto`/`sendmsg`), which is
/// the residue tracked by issues #27 / #143.
/// escape the child's scope. `connect`'s abstract/unnamed/non-AF_UNIX
/// arm still Continues under a destination policy, which is the residue
/// tracked by issues #27 / #143.
/// 2. Connected sockets with addr_ptr == 0 — the address was already
/// validated at connect time, so the kernel re-read of (nothing) is
/// moot.
Expand Down
53 changes: 7 additions & 46 deletions crates/sandlock-core/src/network/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ 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,
};
use super::verdict::{
check_ip_destination, classify_send_path, path_under_any, DestShape, Destination, SendPath,
};
use super::verdict::{check_ip_destination, classify_send_path, DestShape, Destination, SendPath};
use super::{query_socket_protocol, socket_is_unix, Protocol};

// ============================================================
Expand Down Expand Up @@ -110,26 +108,9 @@ pub(super) async fn sendto_on_behalf(
// sendto to a named socket is a WRITE on its inode, so deny unless the
// resolved real target is under an fs-write grant.
match named_unix_socket_path(&addr_bytes) {
Some(path) if ctx.policy.supervises_unix_send() => {
if ctx.policy.chroot_root.is_some() {
if path_under_any(&path, &ctx.policy.chroot_writable) {
NotifAction::Continue
} else {
NotifAction::Errno(libc::EACCES)
}
} else {
sendto_named_unix_on_behalf(
notif,
notif_fd,
sockfd,
buf_ptr,
buf_len,
flags,
&path,
&ctx.policy.chroot_writable,
)
}
}
Some(path) if ctx.policy.supervises_unix_send() => sendto_named_unix_on_behalf(
notif, ctx, notif_fd, sockfd, buf_ptr, buf_len, flags, &path,
),
_ => {
// Non-IP destination with no fs-path gate: an abstract unix
// address, a named unix address with the fs-gate off, a non-unix
Expand Down Expand Up @@ -475,29 +456,9 @@ pub(super) async fn sendmmsg_on_behalf(
}
}
if named_unix {
if ctx.policy.chroot_root.is_some() {
// Chroot: lexical check; deny the whole call if any named-unix
// entry is outside the (virtual) write grants.
for i in 0..vlen {
let entry_ptr = mmsg_entry_ptr(msgvec_ptr, i);
if let Some(path) = mmsg_entry_named_unix_path(notif, notif_fd, entry_ptr) {
if !path_under_any(&path, &ctx.policy.chroot_writable) {
return NotifAction::Errno(libc::EACCES);
}
}
}
// All granted: fall through to the existing path.
} else {
return sendmmsg_named_unix_on_behalf(
notif,
notif_fd,
sockfd,
msgvec_ptr,
vlen,
flags,
&ctx.policy.chroot_writable,
);
}
return sendmmsg_named_unix_on_behalf(
notif, ctx, notif_fd, sockfd, msgvec_ptr, vlen, flags,
);
}
}

Expand Down
Loading
Loading