Skip to content

Commit a7e9bbd

Browse files
vmagrometa-codesync[bot]
authored andcommitted
[antlir2][container_subtarget] support booted rootless containers
Summary: Finally support `--boot` on `[container]` subtargets with rootless images. Test Plan: ``` ❯ buck2 test fbcode//antlir/antlir2/container_subtarget/tests:test -- test_rooted_boot_exit_code test_rootless_boot_exit_code Buck UI: https://www.internalfb.com/buck2/97729d52-cef4-4b10-b06f-f4460618a6d7 Tests finished: Pass 2. Fail 0. Timeout 0. Fatal 0. Skip 0. Omit 0. Infra Failure 0. Build failure 0 ``` https://www.internalfb.com/intern/testinfra/testrun/18014398697563779 Reviewed By: sergeyfd Differential Revision: D113495962 fbshipit-source-id: d059f16cece7a72c1c1e9703597b1a40fae7511d
1 parent 7cd9ae5 commit a7e9bbd

5 files changed

Lines changed: 89 additions & 16 deletions

File tree

antlir/antlir2/antlir2_isolate/isolate_unshare/isolate_unshare_preexec/src/isolation.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use anyhow::Result;
1818
use cap_std::fs::Dir;
1919
use cap_std::fs::OpenOptionsExt as _;
2020
use isolate_cfg::Ephemeral;
21+
use isolate_cfg::InvocationType;
2122
use isolate_cfg::IsolationContext;
2223
use nix::mount::MsFlags;
2324
use nix::mount::mount;
@@ -233,6 +234,30 @@ pub(crate) fn setup_isolation(isol: &IsolationContext) -> Result<()> {
233234
.with_context(|| format!("while mounting device node '{devname}'"))?;
234235
}
235236

237+
// For an interactive booted container, systemd (as PID 1) provides
238+
// a login shell bound to /dev/console. Bind the controlling
239+
// terminal (inherited as stdin) onto /dev/console so console I/O
240+
// flows back to the caller, mirroring what
241+
// `systemd-nspawn --console=interactive` does.
242+
// SAFETY: `isatty` merely queries whether the given fd refers to a
243+
// terminal and has no preconditions or memory safety concerns.
244+
let stdin_is_tty = unsafe { libc::isatty(0) } == 1;
245+
if matches!(invocation_type, InvocationType::BootInteractive) && stdin_is_tty {
246+
let tty = std::fs::read_link("/proc/self/fd/0")
247+
.context("while resolving controlling terminal for /dev/console")?;
248+
let console = tmpfs
249+
.create("console")
250+
.context("while creating device file 'console'")?;
251+
nix::mount::mount(
252+
Some(tty.as_path()),
253+
&console.abspath(),
254+
None::<&str>,
255+
MsFlags::MS_BIND | MS_NOSYMFOLLOW,
256+
None::<&str>,
257+
)
258+
.context("while mounting device node 'console'")?;
259+
}
260+
236261
// Things like `sem_open` requires a usable `/dev/shm`.
237262
tmpfs
238263
.create_dir("shm")

antlir/antlir2/antlir2_isolate/isolate_unshare/isolate_unshare_preexec/src/main.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use anyhow::Result;
1717
use anyhow::anyhow;
1818
use clap::Parser;
1919
use isolate_cfg::Ephemeral;
20+
use isolate_cfg::InvocationType;
2021
use isolate_cfg::IsolationContext;
2122
use json_arg::Json;
2223
use nix::sched::CloneFlags;
@@ -100,6 +101,25 @@ fn do_main(args: Main) -> Result<()> {
100101
ctx.ephemeral = None;
101102
}
102103

104+
// For an interactive booted container, the login shell launched by systemd
105+
// wants to acquire the container's console (/dev/console, which we bind to
106+
// our inherited controlling terminal) as its controlling terminal. That
107+
// terminal is owned by the parent user namespace, so the container cannot
108+
// *steal* it (TIOCSCTTY force requires CAP_SYS_ADMIN in the owning
109+
// namespace). Release it from our session here — while it is unowned, the
110+
// container can acquire it cleanly without any capability.
111+
//
112+
// SAFETY: these libc calls only manipulate this process's controlling
113+
// terminal / signal disposition and have no memory safety implications.
114+
if ctx.invocation_type == InvocationType::BootInteractive && unsafe { libc::isatty(0) } == 1 {
115+
unsafe {
116+
// Ignore the SIGHUP that releasing the controlling terminal sends to
117+
// our (foreground) process group, so it does not kill us.
118+
libc::signal(libc::SIGHUP, libc::SIG_IGN);
119+
libc::ioctl(0, libc::TIOCNOTTY);
120+
}
121+
}
122+
103123
let mut pid1 = Command::new(std::env::current_exe().context("while getting current exe")?);
104124
pid1.arg("pid1")
105125
.arg(serde_json::to_string(&ctx).context("while serializing isolation info")?);

antlir/antlir2/antlir2_isolate/isolate_unshare/src/lib.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ compile_error!("only supported on linux");
1111
use std::ffi::OsStr;
1212
use std::process::Command;
1313

14-
use isolate_cfg::InvocationType;
1514
use isolate_cfg::IsolationContext;
1615

1716
pub mod mount;
@@ -36,12 +35,6 @@ impl<'a> IsolatedContext<'a> {
3635
if self.0.register {
3736
return Err(Error::UnsupportedSetting("register"));
3837
}
39-
// TODO: support this when we can bind the controlling terminal to
40-
// /dev/console, otherwise don't lie about providing an interactive
41-
// console
42-
if self.0.invocation_type == InvocationType::BootInteractive {
43-
return Err(Error::UnsupportedSetting("invocation_type=BootInteractive"));
44-
}
4538

4639
let mut cmd = Command::new(
4740
buck_resources::get("antlir/antlir2/antlir2_isolate/isolate_unshare/preexec")

antlir/antlir2/container_subtarget/src/main.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ struct Args {
4040
user: String,
4141
#[clap(long, conflicts_with_all = ["boot"])]
4242
pipe: bool,
43-
#[clap(long, conflicts_with_all = ["pipe", "rootless"])]
43+
#[clap(long, conflicts_with_all = ["pipe"])]
4444
boot: bool,
4545
#[clap(long)]
4646
chdir: Option<PathBuf>,
@@ -101,14 +101,13 @@ fn main() -> anyhow::Result<()> {
101101
.ephemeral(true)
102102
.tmpfs(Path::new("/tmp"))
103103
.enable_network(args.enable_network);
104-
if !args.rootless {
105-
cmd_builder.invocation_type(match (args.boot, args.pipe) {
106-
(true, false) => InvocationType::BootInteractive,
107-
(true, true) => unreachable!("--boot and --pipe are mutually exclusive"),
108-
(false, true) => InvocationType::Pid2Pipe,
109-
(false, false) => InvocationType::Pid2Interactive,
110-
});
111-
} else {
104+
cmd_builder.invocation_type(match (args.boot, args.pipe) {
105+
(true, false) => InvocationType::BootInteractive,
106+
(true, true) => unreachable!("--boot and --pipe are mutually exclusive"),
107+
(false, true) => InvocationType::Pid2Pipe,
108+
(false, false) => InvocationType::Pid2Interactive,
109+
});
110+
if args.rootless {
112111
cmd_builder.devtmpfs(Path::new("/dev"));
113112
}
114113
if args.artifacts_require_repo {
@@ -133,6 +132,22 @@ fn main() -> anyhow::Result<()> {
133132
PathBuf::from("/run/systemd/system/container-subtarget.service"),
134133
container_subtarget_service,
135134
));
135+
// In rootless mode there is no `systemd-nspawn --boot` to locate and
136+
// exec the init binary, so it must be invoked explicitly. In rooted
137+
// mode systemd-nspawn interprets the trailing argument as a kernel
138+
// command line and boots init itself.
139+
if args.rootless {
140+
// systemd mounts a fresh tmpfs over /run early in boot, which would
141+
// otherwise mask the unit file bind-mounted above.
142+
cmd_builder.tmpfs(Path::new("/run"));
143+
// Provide a fake sysfs (with a cgroup2 hierarchy mounted underneath)
144+
// so systemd does not mount the host's real sysfs. Otherwise
145+
// systemd resolves /dev/console via /sys/class/tty/console/active to
146+
// the host's console device (e.g. /dev/hvc0), which does not exist
147+
// in the container.
148+
cmd_builder.sysfs(Path::new("/sys"));
149+
cmd.insert(0, "/usr/lib/systemd/systemd".into());
150+
}
136151
cmd.push("systemd.unit=container-subtarget.service".into());
137152
}
138153
let mut cmd = cmd.into_iter();

antlir/antlir2/container_subtarget/tests/test.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,23 @@ fn test_rooted_boot_exit_code() {
7777
status => panic!("unexpected exit status: {status:?}"),
7878
}
7979
}
80+
81+
#[test]
82+
fn test_rootless_boot_exit_code() {
83+
let exe = std::env::var("ROOTLESS").expect("missing env var");
84+
let mut p = rexpect::spawn(&format!("{exe} --boot --no-register"), TIMEOUT_MS)
85+
.expect("failed to spawn");
86+
p.exp_regex("[^\n\r](.*?)# ")
87+
.expect("didn't get bash prompt");
88+
p.send_line("systemctl is-system-running")
89+
.expect("failed to write shell command line");
90+
p.exp_regex("running")
91+
.expect("didn't get 'running' response from systemctl is-system-running");
92+
p.send_line("exit 42")
93+
.expect("failed to write shell command line");
94+
let status = p.process.wait().expect("failed to wait for process");
95+
match status {
96+
WaitStatus::Exited(_, real_code) => assert_eq!(42, real_code),
97+
status => panic!("unexpected exit status: {status:?}"),
98+
}
99+
}

0 commit comments

Comments
 (0)