@@ -16,6 +16,7 @@ use crate::config::{
1616use crate :: namespace:: Namespace ;
1717use crate :: signal;
1818use crate :: unshare:: { setns, unshare} ;
19+ use anyhow:: Context ;
1920use anyhow:: { Result , anyhow, bail} ;
2021use 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+
904966fn apply_capabilities ( capabilities : Option < & Capabilities > ) -> Result < ( ) > {
905967 let Some ( caps) = capabilities else {
906968 return Ok ( ( ) ) ;
0 commit comments