Skip to content

Commit 8d0b2c2

Browse files
authored
Merge pull request #149 from bleggett/bleggett/workload-console-uid
Properly handle /dev/console ownership on CreateRequest if UID is set
2 parents 59a3575 + 35ef5f1 commit 8d0b2c2

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

src/wrap.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::config::{
1616
use crate::namespace::Namespace;
1717
use crate::signal;
1818
use crate::unshare::{setns, unshare};
19+
use anyhow::Context;
1920
use anyhow::{Result, anyhow, bail};
2021
use libc::{
2122
self, PR_CAP_AMBIENT, PR_CAP_AMBIENT_LOWER, PR_CAP_AMBIENT_RAISE, PR_CAPBSET_DROP,
@@ -644,6 +645,11 @@ impl Wrappable for CreateRequest {
644645
fs::write("/proc/self/oom_score_adj", score.to_string())?;
645646
}
646647

648+
// Bind the workload's terminal over /dev/console and hand the
649+
// workload uid ownership of it. We must do this here, after we have moved into
650+
// the mount/userns, but before we drop CAP_SYS_ADMIN/CAP_CHOWN.
651+
setup_console(self.exec.uid)?;
652+
647653
preexec_prep(&self.exec, self.capabilities.as_ref())?;
648654

649655
debug!("ready to launch workload");
@@ -901,6 +907,62 @@ fn apply_gid_uid(
901907
Ok(())
902908
}
903909

910+
/// Similar to what runc and others do: if we have an exec UID override, bind a pty to /dev/console
911+
/// with the correct UID ownership, so non-root stuff with that UID can open/write to console.
912+
/// Note that we intentionally only do this for CreateRequest, where we control/create the mount namespace.
913+
fn setup_console(uid: Option<u32>) -> Result<()> {
914+
// Only do the setup if we have an exec UID, otherwise there's no point.
915+
if let Some(exec_uid) = uid {
916+
let Some(tty_fd) = [0, 1, 2]
917+
.into_iter()
918+
.find(|fd| unsafe { libc::isatty(*fd) } == 1)
919+
else {
920+
return Ok(());
921+
};
922+
923+
let pts_path =
924+
fs::read_link(format!("/proc/self/fd/{tty_fd}")).context("could not read TTY FD")?;
925+
926+
// If /dev/console isn't here, we should be fine to create it and bind over it,
927+
// rather than bind over the existing one.
928+
if !std::path::Path::new("/dev/console").exists() {
929+
fs::File::create("/dev/console").context("could not create /dev/console stub")?;
930+
}
931+
932+
let console_mount = MountSpec {
933+
source: Some(pts_path.to_string_lossy().into_owned()),
934+
target: "/dev/console".to_string(),
935+
fstype: None,
936+
bind: true,
937+
recurse: false,
938+
unshare: false,
939+
safe: false,
940+
create_mountpoint: false,
941+
read_only: false,
942+
data: None,
943+
};
944+
console_mount
945+
.mount()
946+
.context("failed to bind-mount /dev/console")?;
947+
948+
// Flag the cases runc does:
949+
// - a uid not mapped into our userns (EPERM)
950+
// - a read-only /dev
951+
let rc = unsafe { libc::fchown(tty_fd, exec_uid as libc::uid_t, u32::MAX as libc::gid_t) };
952+
if rc < 0 {
953+
let err = Error::last_os_error();
954+
match err.raw_os_error() {
955+
Some(libc::EPERM) | Some(libc::EROFS) => {
956+
warn!("refusing to chown workload console to uid {exec_uid}: {err}");
957+
}
958+
_ => bail!("failed to chown workload console to uid {exec_uid}: {err}"),
959+
}
960+
}
961+
}
962+
963+
Ok(())
964+
}
965+
904966
fn apply_capabilities(capabilities: Option<&Capabilities>) -> Result<()> {
905967
let Some(caps) = capabilities else {
906968
return Ok(());

0 commit comments

Comments
 (0)