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
2 changes: 2 additions & 0 deletions engine/crates/layerfs-api/examples/sandbox_kernel_mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ fn setup(private: [u8; 32], peer: [u8; 32]) -> DaemonSetup {
idle_exit_s: 0,
},
existing_store: None,
private_mount_ns: false,
}
}
type Running = (
Expand All @@ -124,6 +125,7 @@ fn start(
vec![],
directory.into(),
false,
None,
),
}
.map_err(|f| format!("create {f:?}"))?;
Expand Down
2 changes: 2 additions & 0 deletions engine/crates/layerfs-api/examples/sandbox_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn setup(private: [u8; 32], peer: [u8; 32]) -> DaemonSetup {
idle_exit_s: 0,
},
existing_store: None,
private_mount_ns: false,
}
}
fn ordinary(owner: &ManagedSandbox) -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -48,6 +49,7 @@ fn ordinary(owner: &ManagedSandbox) -> Result<(), Box<dyn std::error::Error>> {
vec![],
"/".into(),
false,
None,
)
.map_err(|f| format!("create {f:?}"))?;
let attached = created.start().map_err(|f| format!("start {f:?}"))?;
Expand Down
16 changes: 15 additions & 1 deletion engine/crates/layerfs-api/src/base/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,30 @@ pub fn parts(

/// The same ordinary launcher for external host commands and disjoint concurrent proof borrows.
#[cfg(unix)]
pub use crate::sandbox::exec::{exec_arguments, MountNamespace};

/// `namespace` names a daemon that serves from a private mount namespace: the
/// command then runs through that daemon's `ns-exec` helper. `None` keeps the
/// command as it is.
pub fn exec(
sandbox: &layerfs_sandbox::backend::docker::Sandbox,
identity: layerfs_sandbox::CommandIdentity,
arguments: Vec<String>,
environment: Vec<String>,
directory: String,
stdin: bool,
namespace: Option<MountNamespace<'_>>,
) -> Result<
layerfs_sandbox::backend::docker::ExecCreated,
Box<layerfs_sandbox::backend::docker::ExecCreateFailure>,
> {
crate::sandbox::exec::launch(sandbox, identity, arguments, environment, directory, stdin)
crate::sandbox::exec::launch(
sandbox,
identity,
arguments,
environment,
directory,
stdin,
namespace,
)
}
62 changes: 60 additions & 2 deletions engine/crates/layerfs-api/src/sandbox/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@
use crate::{ManagedSandbox, MountedWorkspace, WorkspaceApi};
use layerfs_sandbox::{
backend::docker::{ExecCreateFailure, ExecCreated, ExecRequest, Sandbox},
CommandIdentity,
CommandIdentity, DAEMON_ENTRY,
};

/// The daemon socket a command gets its private mount namespace from. Only a
/// daemon that serves from a private namespace names one, so `Some` always
/// wraps the command and `None` never does: there is no empty middle state.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MountNamespace<'a> {
/// The daemon's socket. It sends the descriptor of its own mount
/// namespace; the daemon is not dumpable, so the helper cannot open
/// `/proc/<pid>/ns/mnt` itself.
pub socket: &'a str,
}

impl WorkspaceApi<'_> {
/// Creates one ordinary command at this mount using the deployment's admitted identity.
/// The returned Sandbox runtime owner owns Start, streams, exit status and cancellation.
Expand All @@ -16,29 +27,76 @@ impl WorkspaceApi<'_> {
environment: Vec<String>,
stdin: bool,
) -> Result<ExecCreated, Box<ExecCreateFailure>> {
// The owner's own daemon setup says whether this Sandbox serves from a
// private namespace. When it does, the command must enter it through
// `ns-exec`; running it in the initial namespace would find only the
// empty directory under the mount.
let socket = sandbox
.request
.setup
.mount_ns_socket()
.map(|path| path.to_string_lossy().into_owned());
launch(
&sandbox.runtime,
sandbox.command_identity,
arguments,
environment,
workspace.ready.directory.clone(),
stdin,
socket.as_deref().map(|socket| MountNamespace { socket }),
)
}
}
/// Creates the Engine Exec for one command. `namespace` names a daemon that
/// serves from a private mount namespace: the command then runs through that
/// daemon's `ns-exec` helper, because the Workspace mounts are absent from the
/// namespace the Engine enters. `None` keeps the command as it is.
pub(crate) fn launch(
sandbox: &Sandbox,
identity: CommandIdentity,
arguments: Vec<String>,
environment: Vec<String>,
directory: String,
stdin: bool,
namespace: Option<MountNamespace<'_>>,
) -> Result<ExecCreated, Box<ExecCreateFailure>> {
sandbox.create_exec(ExecRequest {
identity,
arguments,
arguments: exec_arguments(namespace, identity, &directory, arguments),
environment,
directory,
stdin,
root_launcher: namespace.is_some(),
})
}
/// The Engine argv for one command. Without a mount-namespace socket it is
/// exactly the caller's argv. With one it names the daemon's helper, the
/// socket, the mount directory the Engine starts in (only the directory under
/// the mount exists in its namespace) and the ordinary identity the helper
/// drops to, then the same argv after `--`: the helper execs it, so the Engine
/// still has one process tree, the same streams and the same exit code.
pub fn exec_arguments(
namespace: Option<MountNamespace<'_>>,
identity: CommandIdentity,
directory: &str,
arguments: Vec<String>,
) -> Vec<String> {
let Some(namespace) = namespace else {
return arguments;
};
let mut wrapped = vec![
DAEMON_ENTRY.to_owned(),
"ns-exec".to_owned(),
"--socket".to_owned(),
namespace.socket.to_owned(),
"--directory".to_owned(),
directory.to_owned(),
"--uid".to_owned(),
identity.uid.to_string(),
"--gid".to_owned(),
identity.gid.to_string(),
"--".to_owned(),
];
wrapped.extend(arguments);
wrapped
}
1 change: 1 addition & 0 deletions engine/crates/layerfs-api/tests/kernel_application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ fn actual_daemon_install_hello_bind_status_no_constructor_and_session_end() {
idle_exit_s: 0,
},
existing_store: None,
private_mount_ns: false,
};
let config = f.directory.join("daemon.config");
fs::write(&config, setup.encode().unwrap()).unwrap();
Expand Down
1 change: 1 addition & 0 deletions engine/crates/layerfs-api/tests/observed_application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ fn configure(f: &support::Fixture, deployment: &Deployment) -> PathBuf {
idle_exit_s: deployment.idle_exit_s,
},
existing_store: deployment.existing_store.clone(),
private_mount_ns: false,
};
let config = f.directory.join("daemon.config");
fs::write(&config, setup.encode().unwrap()).unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ fn start(f: &support::Fixture, mounts: &Path) -> (Process, SocketAddr) {
idle_exit_s: 0,
},
existing_store: None,
private_mount_ns: false,
};
let config = f.directory.join("daemon.config");
fs::write(&config, setup.encode().unwrap()).unwrap();
Expand Down
55 changes: 55 additions & 0 deletions engine/crates/layerfs-api/tests/sandbox_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ fn setup(private: [u8; 32], peer: [u8; 32]) -> DaemonSetup {
idle_exit_s: 0,
},
existing_store: None,
private_mount_ns: false,
}
}

Expand All @@ -61,7 +62,18 @@ impl Fixture {
fn new(tail: Vec<Vec<u8>>) -> Self {
Self::new_for(None, tail)
}
/// A Sandbox whose daemon serves from a private mount namespace.
fn new_private(tail: Vec<Vec<u8>>) -> Self {
Self::build(None, tail, true)
}
fn new_for(project: Option<&layerfs_api::SealedProject>, tail: Vec<Vec<u8>>) -> Self {
Self::build(project, tail, false)
}
fn build(
project: Option<&layerfs_api::SealedProject>,
tail: Vec<Vec<u8>>,
private_mount_ns: bool,
) -> Self {
let existing = project.is_some();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
Expand Down Expand Up @@ -132,6 +144,7 @@ impl Fixture {
));
fs::write(&binary, b"original executable input").unwrap();
let mut deployment_setup = setup([32; 32], duplex::public_key(&[31; 32]).unwrap());
deployment_setup.private_mount_ns = private_mount_ns;
if let Some(project) = project {
let mut manifest = project.manifest.clone();
manifest.daemon_sqlite = Some("3.51.0".into());
Expand Down Expand Up @@ -334,6 +347,48 @@ fn workspace_exec_returns_original_runtime_owner_at_the_mount_with_no_daemon_reg
.any(|call| call.windows(12).any(|bytes| bytes == b"/exec/start ")));
}
#[test]
fn workspace_exec_wraps_the_command_for_a_private_mount_namespace_sandbox() {
const EID: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
let mut fixture = Fixture::new_private(vec![
fixed(201, format!("{{\"Id\":\"{EID}\"}}").as_bytes()),
fixed(204, b""),
fixed(204, b""),
]);
let created = WorkspaceApi::exec(
fixture.owner.as_ref().unwrap(),
&mounted(),
vec!["/bin/pwd".into()],
vec![],
false,
)
.unwrap();
assert_eq!(created.request().directory, "/workspaces/selected-mount");
assert_eq!(
created.request().arguments,
[
"/usr/local/bin/layerfs-daemon",
"ns-exec",
"--socket",
"/layerfs-local/ns-exec.sock",
"--directory",
"/workspaces/selected-mount",
"--uid",
"501",
"--gid",
"20",
"--",
"/bin/pwd",
]
);
// Only root holds the capability the helper's setns needs.
assert!(created.request().root_launcher);
fixture.api.close(fixture.owner.take().unwrap()).unwrap();
let calls = fixture.calls();
let request = std::str::from_utf8(&calls[6]).unwrap();
assert!(request.starts_with(&format!("POST /v1.54/containers/{CID}/exec ")));
assert!(request.contains(r#""User":"0:0""#), "request: {request}");
}
#[test]
fn workspace_exec_failure_keeps_exact_original_request_and_container() {
let mut fixture = Fixture::new(vec![
fixed(409, b"original exec refusal"),
Expand Down
40 changes: 39 additions & 1 deletion engine/crates/layerfs-bridge/src/daemon_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::{
wire::{Reader, Writer},
};
use std::fmt;
const MAGIC: &[u8] = b"LFSD\x05";
use std::path::{Path, PathBuf};
const MAGIC: &[u8] = b"LFSD\x06";
/// Explicit daemon startup resources; these are windows/admission, not file caps.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DaemonLimits {
Expand Down Expand Up @@ -51,6 +52,12 @@ pub struct DaemonSetup {
/// None selects the original one-time installer. Some selects explicit existing
/// Store open using retained authority; never selected after an install error.
pub existing_store: Option<StoreManifest>,
/// Start in a mount namespace of this process's own (prototype for #3/#499),
/// serve its descriptor on [`DaemonSetup::mount_ns_socket`] and write the
/// diagnostic pid file [`DaemonSetup::mount_ns_pid_file`] names, so a
/// command reaches the Workspace mounts only through the daemon's
/// `ns-exec` helper. Off by default; the flag is opt-in.
pub private_mount_ns: bool,
}
impl DaemonSetup {
/// Encodes one bounded private config. No operation/flow byte limit is implied.
Expand All @@ -77,6 +84,7 @@ impl DaemonSetup {
if let Some(value) = &self.existing_store {
out.blob(&value.encode()?)?;
}
out.byte(u8::from(self.private_mount_ns))?;
Ok(out.0)
}
/// Decodes exactly one config without fallback or profile selection.
Expand Down Expand Up @@ -104,6 +112,11 @@ impl DaemonSetup {
1 => Some(StoreManifest::decode(input.blob(crate::wire::LIMIT)?)?),
_ => return Err(ControlError("daemon config boolean")),
};
let private_mount_ns = match input.byte()? {
0 => false,
1 => true,
_ => return Err(ControlError("daemon config boolean")),
};
input.finish()?;
let value = Self {
listen,
Expand All @@ -116,6 +129,7 @@ impl DaemonSetup {
command_gid,
limits,
existing_store,
private_mount_ns,
};
value.check()?;
Ok(value)
Expand Down Expand Up @@ -154,8 +168,31 @@ impl DaemonSetup {
return Err(ControlError("explicit existing Disposable Store"));
}
}
if self.private_mount_ns && self.mount_ns_socket().is_none() {
return Err(ControlError("private mount namespace socket path"));
}
Ok(())
}
/// The diagnostic pid file this daemon writes when
/// [`Self::private_mount_ns`] is on: `daemon.pid` in the local directory
/// above the Overlay's own directory (in a Sandbox that is
/// `/layerfs-local`). It is evidence, not an exec route. `None` when the
/// flag is off or the Overlay path has no such local directory.
pub fn mount_ns_pid_file(&self) -> Option<PathBuf> {
self.local_path("daemon.pid")
}
/// The socket this daemon serves its mount-namespace descriptor on when
/// [`Self::private_mount_ns`] is on, beside the pid file.
pub fn mount_ns_socket(&self) -> Option<PathBuf> {
self.local_path("ns-exec.sock")
}
fn local_path(&self, name: &str) -> Option<PathBuf> {
if !self.private_mount_ns {
return None;
}
let home = Path::new(&self.overlay).parent()?.parent()?;
Some(home.join(name))
}
}
impl fmt::Debug for DaemonSetup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand All @@ -170,6 +207,7 @@ impl fmt::Debug for DaemonSetup {
.field("command_gid", &self.command_gid)
.field("limits", &self.limits)
.field("existing_store", &self.existing_store)
.field("private_mount_ns", &self.private_mount_ns)
.finish()
}
}
Loading