diff --git a/Cargo.lock b/Cargo.lock index 1fbddde057..920ed0df7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ansi_term" @@ -150,9 +150,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libredox" @@ -211,6 +211,16 @@ dependencies = [ "libredox", ] +[[package]] +name = "redox-scheme" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96b9cfb034251dfb0aaa66a67059a7f0ea344039904d1d70cd36266af9c8a2f" +dependencies = [ + "libredox", + "redox_syscall", +] + [[package]] name = "redox_event" version = "0.4.1" @@ -236,9 +246,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags 2.6.0", ] @@ -382,11 +392,14 @@ version = "0.1.0" dependencies = [ "clap", "extra", + "libc", "libredox", "orbclient", "redox-daemon", + "redox-scheme", "redox_event", "redox_liner", + "redox_syscall", "redox_termios", "redox_users", "termion", diff --git a/Cargo.toml b/Cargo.toml index ef04651fa3..a9ec5d7caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,11 +55,14 @@ path = "src/bin/usermod.rs" clap = "2.33.0" extra = { git = "https://gitlab.redox-os.org/redox-os/libextra.git" } orbclient = "0.3.47" -redox-daemon = "0.1.2" +redox-daemon = "0.1.3" redox_liner = "0.5.2" +libc = "0.2.171" libredox = "0.1.3" redox_termios = "0.1.3" redox_event = "0.4" +redox-scheme = "0.5.0" +redox_syscall = "0.5" redox_users = "0.4.6" termion = "4" diff --git a/src/bin/sudo.rs b/src/bin/sudo.rs index 2f81f4fd3a..3114a74f8a 100644 --- a/src/bin/sudo.rs +++ b/src/bin/sudo.rs @@ -1,10 +1,20 @@ +use std::collections::HashMap; use std::env; use std::io::{self, Write}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::os::unix::process::CommandExt; use std::process::{Command, exit}; use extra::option::OptionalExt; +use libredox::flag::O_CLOEXEC; +use redox_scheme::scheme::SchemeSync; +use redox_scheme::{ + CallerCtx, OpenResult, RequestKind, Response, SendFdRequest, SignalBehavior, Socket, +}; use redox_users::{All, AllGroups, AllUsers, Config, get_uid}; +use syscall::error::*; +use syscall::flag::*; +use syscall::schemev2::NewFdFlags; use termion::input::TermRead; const MAX_ATTEMPTS: u16 = 3; @@ -27,42 +37,30 @@ EXIT STATUS the exit status will be >0. AUTHOR - Written by Jeremy Soller, Jose Narvaez. + Written by Jeremy Soller, Jose Narvaez, bjorn3. "#; /* @MANEND */ pub fn main() { + if env::args().nth(1).as_deref() == Some("--daemon") { + daemon_main(); + } + let mut args = env::args().skip(1); let cmd = args.next().unwrap_or_else(|| { eprintln!("sudo: no command provided"); exit(1); }); - let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); - let groups = AllGroups::new(Config::default()).unwrap_or_exit(1); - + let users = AllUsers::basic(Config::default()).unwrap_or_exit(1); let uid = get_uid().unwrap_or_exit(1); - let user = users.get_by_id(uid).unwrap_or_exit(1); if uid == 0 { - run_command_as_root(&cmd, &args.collect::>()); - exit(0); + // We are root already. No need to elevate privileges + run_command_as_root(&cmd, &args.collect()); } - let sudo_group = groups.get_by_name("sudo").unwrap_or_exit(1); - if !sudo_group.users.iter().any(|name| name == &user.user) { - eprintln!("sudo: '{}' not in sudo group", user.user); - exit(1); - } - - if user.is_passwd_blank() { - // FIXME: We should not be doing this as provides access to any - // user w/o auth to run stuff as root. We should be doing something like: - // eprintln!("sudo: '{}' is in sudo group but does not have a password set", user.user); - // exit(1); - run_command_as_root(&cmd, &args.collect::>()); - exit(0); - } + let file = libredox::call::open("/scheme/sudo", O_CLOEXEC, 0).unwrap(); let mut attempts = 0; @@ -74,14 +72,19 @@ pub fn main() { Some(password) => { println!(); - if user.verify_passwd(&password) { - break; - } else { - attempts += 1; - eprintln!("sudo: incorrect password ({}/{})", attempts, MAX_ATTEMPTS); - if attempts >= MAX_ATTEMPTS { - exit(1); + match libredox::call::write(file, password.as_bytes()) { + Ok(_) => break, + Err(err) if err.errno() == EPERM => { + attempts += 1; + eprintln!( + "sudo: incorrect password or not in sudo group ({}/{})", + attempts, MAX_ATTEMPTS, + ); + if attempts >= MAX_ATTEMPTS { + exit(1); + } } + Err(err) => panic!("{err}"), } } None => { @@ -91,11 +94,33 @@ pub fn main() { } } - run_command_as_root(&cmd, &args.collect::>()); - exit(0); + // Elevate privileges of our own process with help from the sudo daemon + let self_proc = syscall::open("/scheme/thisproc/current/open_via_dup", 0).unwrap(); + syscall::sendfd(file, self_proc, 0, 0).unwrap(); + + run_command_as_root(&cmd, &args.collect()); } -fn run_command_as_root(cmd: &str, args: &Vec) { +enum Policy { + Deny, + Authenticate, +} + +fn policy_for_user(uid: u32) -> Policy { + let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); + let groups = AllGroups::new(Config::default()).unwrap_or_exit(1); + + let user = users.get_by_id(uid as usize).unwrap_or_exit(1); + + let sudo_group = groups.get_by_name("sudo").unwrap_or_exit(1); + if !sudo_group.users.iter().any(|name| name == &user.user) { + return Policy::Deny; + } + + Policy::Authenticate +} + +fn run_command_as_root(cmd: &str, args: &Vec) -> ! { let mut command = Command::new(&cmd); for arg in args { command.arg(&arg); @@ -107,17 +132,163 @@ fn run_command_as_root(cmd: &str, args: &Vec) { command.env("UID", "0"); command.env("GROUPS", "0"); - match command.spawn() { - Ok(mut child) => match child.wait() { - Ok(status) => exit(status.code().unwrap_or(0)), - Err(err) => { - eprintln!("sudo: failed to wait for {}: {}", cmd, err); - exit(1); - } - }, - Err(err) => { - eprintln!("sudo: failed to execute {}: {}", cmd, err); - exit(1); + let err = command.exec(); + + eprintln!("sudo: failed to execute {}: {}", cmd, err); + exit(1); +} + +struct Scheme { + next_fd: usize, + handles: HashMap, +} +enum Handle { + AwaitingPassword { uid: u32 }, + AwaitingContextFd, + Placeholder, +} + +impl SchemeSync for Scheme { + fn open(&mut self, path: &str, _flags: usize, ctx: &CallerCtx) -> Result { + // Path must be empty + if path.trim_start_matches('/') != "" { + return Err(Error::new(ENOENT)); } + let fd = self.next_fd; + self.next_fd = self.next_fd.checked_add(1).ok_or(Error::new(EMFILE))?; + self.handles + .insert(fd, Handle::AwaitingPassword { uid: ctx.uid }); + + Ok(OpenResult::ThisScheme { + number: fd, + flags: NewFdFlags::empty(), + }) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + _off: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?; + + let validate_utf8 = |buf| std::str::from_utf8(buf).map_err(|_| Error::new(EINVAL)); + + match std::mem::replace(handle, Handle::Placeholder) { + Handle::AwaitingPassword { uid } => { + let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); + let user = users.get_by_id(uid as usize).unwrap_or_exit(1); + + match policy_for_user(uid) { + Policy::Deny => { + *handle = Handle::AwaitingPassword { uid }; + return Err(Error::new(EPERM)); + } + Policy::Authenticate => { + let password = validate_utf8(buf)?; + if user.verify_passwd(&password) { + *handle = Handle::AwaitingContextFd + } else { + *handle = Handle::AwaitingPassword { uid }; + return Err(Error::new(EPERM)); + } + } + } + } + Handle::AwaitingContextFd => { + *handle = Handle::AwaitingContextFd; + return Err(Error::new(EINVAL)); + } + + Handle::Placeholder => { + eprintln!("sudo: found placeholder handle with ID {id}"); + return Err(Error::new(EBADFD)); + } + } + Ok(buf.len()) } } +impl Scheme { + fn on_close(&mut self, id: usize) { + self.handles.remove(&id); + } + + fn on_sendfd(&mut self, socket: &Socket, req: &SendFdRequest) -> Result { + let handle = self.handles.get_mut(&req.id()).ok_or(Error::new(EBADF))?; + match std::mem::replace(handle, Handle::Placeholder) { + Handle::AwaitingContextFd => { + let mut context_file = usize::MAX; + req.obtain_fd(socket, FobtainFdFlags::empty(), Err(&mut context_file))?; + let context_file = unsafe { OwnedFd::from_raw_fd(context_file as RawFd) }; + + let context_uid_fd = unsafe { + OwnedFd::from_raw_fd( + syscall::dup(context_file.as_raw_fd() as usize, b"uid")? as RawFd + ) + }; + let context_gid_fd = unsafe { + OwnedFd::from_raw_fd( + syscall::dup(context_file.as_raw_fd() as usize, b"gid")? as RawFd + ) + }; + + // The caller was previously authenticated as member of the sudo group. Allow it to + // elevate privileges of a single process (likely itself). + syscall::write(context_uid_fd.as_raw_fd() as usize, b"0") + .map_err(|_| Error::new(EIO))?; + + syscall::write(context_gid_fd.as_raw_fd() as usize, b"0") + .map_err(|_| Error::new(EIO))?; + } + old => { + *handle = old; + return Err(Error::new(EBADF)); + } + } + Ok(0) + } +} + +fn daemon_main() -> ! { + redox_daemon::Daemon::new(move |daemon| { + // TODO: Linux kernel audit-like logging? + let socket = Socket::create("sudo").expect("failed to open scheme socket"); + + let mut scheme = Scheme { + next_fd: 1, + handles: HashMap::new(), + }; + + daemon + .ready() + .expect("failed to signal sudo scheme readiness"); + + loop { + let Some(req) = socket + .next_request(SignalBehavior::Restart) + .expect("failed to get request") + else { + break; + }; + + let response = match req.kind() { + RequestKind::Call(call) => call.handle_sync(&mut scheme), + RequestKind::SendFd(req) => Response::new(scheme.on_sendfd(&socket, &req), req), + RequestKind::OnClose { id } => { + scheme.on_close(id); + continue; + } + _ => continue, + }; + + socket + .write_response(response, SignalBehavior::Restart) + .expect("sudo: scheme write failed"); + } + std::process::exit(0) + }) + .expect("failed to start sudo daemon"); +}