#![allow(unused)]
use std::{
env,
io::{Read, Seek, SeekFrom, Stdin, Write},
os::fd::{AsFd, AsRawFd, BorrowedFd},
process::{exit, ExitCode},
};
use dur::Duration;
use linefeed::{Interface, ReadResult};
use nix::{
errno::Errno,
unistd::{isatty, Gid, Uid},
};
use syd::{
compat::MFdFlags,
config::*,
cookie::safe_memfd_create,
debug,
fd::{seal_memfd_all, set_cloexec},
human_size,
io::ReadFd,
lookup::safe_copy_if_exists,
path::XPathBuf,
syslog::LogLevel,
};
#[cfg(all(
not(coverage),
not(feature = "prof"),
not(target_os = "android"),
not(target_arch = "riscv64"),
target_page_size_4k,
target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;
#[cfg(not(target_os = "android"))]
#[expect(clippy::disallowed_types)]
enum Input {
File(std::fs::File),
Stdin(Stdin),
}
#[cfg(not(target_os = "android"))]
impl Read for Input {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Input::File(f) => f.read(buf),
Input::Stdin(s) => s.read(buf),
}
}
}
#[cfg(not(target_os = "android"))]
impl AsFd for Input {
fn as_fd(&self) -> BorrowedFd<'_> {
match self {
Input::File(f) => f.as_fd(),
Input::Stdin(s) => s.as_fd(),
}
}
}
#[cfg(not(target_os = "android"))]
impl ReadFd for Input {}
#[cfg(target_os = "android")]
fn main() {
eprintln!("syd-sh: bionic libc doesn't support wordexp(3)!");
std::process::exit(libc::ENOSYS);
}
#[cfg(not(target_os = "android"))]
syd::main! {
use lexopt::prelude::*;
use syd::wordexp::*;
syd::set_sigpipe_dfl()?;
syd::log::log_init_simple(LogLevel::Warn)?;
let mut optc = false;
let mut opte = false;
let mut optl = false;
let mut optx = false;
let mut args = Vec::new();
let mut aend = false;
for (idx, arg) in env::args().enumerate() {
match arg.chars().next() {
Some('-') if idx == 0 => {
optl = true;
continue;
}
_ if idx == 0 => continue,
Some('+') if !aend => continue,
Some('-') if arg == "--" => aend = true,
_ => aend = true,
}
args.push(arg);
}
let mut parser = lexopt::Parser::from_args(&args);
let mut args = Vec::new();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Short('c') => optc = true,
Short('e') => opte = true,
Short('l') => optl = true,
Short('x') => optx = true,
Short(_) | Long(_) => {}
Value(prog) => {
args.push(prog);
args.extend(parser.raw_args()?);
}
}
}
#[expect(clippy::disallowed_types)]
let mut file = safe_memfd_create(
c"syd-sh",
MFdFlags::MFD_ALLOW_SEALING | MFdFlags::MFD_CLOEXEC).map(std::fs::File::from)?;
debug!("ctx": "sh",
"msg": format!("created memory-file {} with close-on-exec flag set",
file.as_raw_fd()));
if opte {
file.write_all(b"set -e\n")?;
}
if optx {
file.write_all(b"set -x\n")?;
}
file.write_all(ESYD_SH.as_bytes())?;
file.write_all(b"\n")?;
if optl {
safe_copy_if_exists(&mut file, "/etc/syd/init_login.sh")?;
file.write_all(b"\n")?;
}
safe_copy_if_exists(&mut file, "/etc/syd/init.sh")?;
file.write_all(b"\n")?;
let uid = Uid::effective();
let name = env::var_os("USER")
.map(XPathBuf::from)
.unwrap_or_else(|| "nobody".into());
let home = env::var_os("HOME")
.map(XPathBuf::from)
.unwrap_or_else(|| "/var/empty".into());
if optl {
let init = home.join(b".config").join(b"syd").join(b"init_login.sh");
safe_copy_if_exists(&mut file, &init)?;
file.write_all(b"\n")?;
}
let init = home.join(b".config").join(b"syd").join(b"init.sh");
safe_copy_if_exists(&mut file, &init)?;
file.write_all(b"\n")?;
let mut args = args.into_iter().peekable();
if optc {
if args.peek().is_none() {
eprintln!("syd-sh: -c requires an argument!");
return Ok(ExitCode::FAILURE);
}
let mut argc = 0;
let mut input = String::new();
for arg in args {
argc += 1;
let arg = arg.to_str().ok_or(Errno::EINVAL)?;
file.write_all(quote(arg).as_bytes())?;
file.write_all(b" ")?;
if optx {
input.push_str(arg);
input.push(' ');
}
}
file.write_all(b"\n")?;
debug!("ctx": "sh",
"msg": format!("written {argc} argument{} into memory-file {}",
if argc > 1 { "s" } else { "" },
file.as_raw_fd()));
if optx {
eprintln!("+ {input}");
}
seal_memfd_all(&file)?;
debug!("ctx": "sh",
"msg": format!("sealed memory-file {} against grows, shrinks and writes",
file.as_raw_fd()));
set_cloexec(&file, false)?;
debug!("ctx": "sh",
"msg": format!("set close-on-exec flag to off for memory-file {}",
file.as_raw_fd()));
let shell = format!("`. /proc/self/fd/{}`", file.as_raw_fd());
debug!("ctx": "sh",
"msg": format!("passing memory file {} to WordExp::expand with 3 seconds timeout...",
file.as_raw_fd()));
match WordExp::expand(&shell, true, Duration::from_secs(3)) {
Ok(out) => {
println!("{out}");
return Ok(ExitCode::SUCCESS);
}
Err(err) => {
let err = err.into();
if opte {
eprintln!("syd-sh: 1: {}", wrde2str(err));
}
exit(err);
}
};
}
#[expect(clippy::disallowed_methods)]
#[expect(clippy::disallowed_types)]
let input: Option<(Input, String)> = if let Some(path) = args.next() {
Some((
Input::File(std::fs::File::open(&path)?),
XPathBuf::from(path).to_string(),
))
} else if isatty(std::io::stdin()).unwrap_or(false) {
None
} else {
Some((Input::Stdin(std::io::stdin()), "standard input".to_string()))
};
if let Some((mut input_file, input_name)) = input {
debug!("ctx": "sh",
"msg": format!("copying from {input_name} to memory-file {}...",
file.as_raw_fd()));
let copylen = syd::io::copy(&mut input_file, &mut file)?;
debug!("ctx": "sh",
"msg": format!("copied {} from {input_name} to memory-file {}",
human_size(copylen.try_into()?),
file.as_raw_fd()));
seal_memfd_all(&file)?;
debug!("ctx": "sh",
"msg": format!("sealed memory-file {} against grows, shrinks and writes",
file.as_raw_fd()));
set_cloexec(&file, false)?;
debug!("ctx": "sh",
"msg": format!("set close-on-exec flag to off for memory-file {}",
file.as_raw_fd()));
let shell = format!("`. /proc/self/fd/{}`", file.as_raw_fd());
debug!("ctx": "sh",
"msg": format!("passing memory file {} to WordExp::expand with 3 seconds timeout...",
file.as_raw_fd()));
match WordExp::expand(&shell, true, Duration::from_secs(3)) {
Ok(val) => {
println!("{val}");
return Ok(ExitCode::SUCCESS);
}
Err(err) => {
let err = err.into();
if opte {
eprintln!("syd-sh: {err}");
}
exit(err);
}
}
}
assert_eq!(
Uid::current(),
Uid::effective(),
"real user ID must match effective user ID in interactive mode!",
);
assert_eq!(
Gid::current(),
Gid::effective(),
"real group ID must match effective group ID in interactive mode!",
);
let reader = Interface::new("syd-sh")?;
reader.set_prompt("; ")?;
while let ReadResult::Input(input) = reader.read_line()? {
if matches!(input.chars().next(), Some('>')) {
let histlen = file.seek(SeekFrom::End(0))?;
file.write_all(&input.as_bytes()[1..])?;
file.write_all(b"\n")?;
let len = input.len();
reader.set_prompt("OKHIST; ")?;
debug!("ctx": "sh",
"msg": format!("pushed {} into memory-file of {}",
human_size(len),
human_size(histlen.try_into()?)));
continue;
} else if matches!(input.trim().chars().next(), None | Some('#')) {
reader.set_prompt("; ")?;
continue;
} else if optx {
eprintln!("+ {input}");
}
#[expect(clippy::disallowed_types)]
let mut fdup = safe_memfd_create(
c"syd-sh",
MFdFlags::MFD_ALLOW_SEALING | MFdFlags::MFD_CLOEXEC).map(std::fs::File::from)?;
debug!("ctx": "sh",
"msg": format!("created memory-file {} with sealing allowed",
fdup.as_raw_fd()));
file.seek(SeekFrom::Start(0))?;
let copylen = syd::io::copy(&mut file, &mut fdup)?;
debug!("ctx": "sh",
"msg": format!("copied {} from memory-file {} to {}",
human_size(copylen.try_into()?),
file.as_raw_fd(),
fdup.as_raw_fd()));
fdup.write_all(input.as_bytes())?;
debug!("ctx": "sh",
"msg": format!("written {} of input to memory-file {}",
human_size(input.len()),
fdup.as_raw_fd()));
seal_memfd_all(&fdup)?;
debug!("ctx": "sh",
"msg": format!("sealed memory-file {} against grows, shrinks and writes",
fdup.as_raw_fd()));
set_cloexec(&fdup, false)?;
debug!("ctx": "sh",
"msg": format!("set close-on-exec flag to off for memory-file {}",
fdup.as_raw_fd()));
let shell = format!("`. /proc/self/fd/{} 2>&1`", fdup.as_raw_fd());
debug!("ctx": "sh",
"msg": format!("passing memory-file {} to WordExp::expand with 3 seconds timeout...",
fdup.as_raw_fd()));
let result = WordExp::expand(&shell, true, Duration::from_secs(3));
let fdup_fd = fdup.as_raw_fd();
drop(fdup);
match result {
Ok(ref val) => {
debug!("ctx": "sh",
"msg": format!("closed memory-file {fdup_fd} after WordExp::expand returned {} of output",
human_size(val.len())));
}
Err(ref err) => {
debug!("ctx": "sh",
"msg": format!("closed memory-file {fdup_fd} after WordExp::expand error {err}"));
}
}
match result {
Ok(val) => {
reader.set_prompt("; ")?;
println!("{val}");
}
Err(WordExpError::BadValue) if !input.contains(';') => {
reader.set_prompt("; ")?;
if let Some(cmd) = input.split_whitespace().next() {
for builtin in SHELL_BUILTINS {
if cmd == *builtin {
let histlen = file.seek(SeekFrom::End(0))?;
file.write_all(input.as_bytes())?;
file.write_all(b"\n")?;
debug!("ctx": "sh",
"msg": format!("pushed {} into memory-file of {}",
human_size(input.len() + 1),
human_size(histlen.try_into()?)));
break;
}
}
}
}
Err(err) => {
let prompt = format!("{}; ", wrde2str(err.into()));
reader.set_prompt(&prompt)?;
}
}
}
Ok(ExitCode::SUCCESS)
}
#[cfg(not(target_os = "android"))]
fn help() {
println!("Usage:");
println!(" syd-sh [-helsx] [--] [_command_file_ [argument...]]");
println!(" syd-sh [-helx] -c _command_string_ [_command_name_ [argument...]]");
println!("Simple confined shell based on wordexp(3)");
println!("Given no arguments, enter read-eval-print loop.");
println!("Given -c with an argument, evaluate and print the result.");
}
#[cfg(not(target_os = "android"))]
fn wrde2str(err: i32) -> String {
use syd::wordexp::*;
match err {
0 => "".to_string(),
128 => "ERR?".to_string(),
WRDE_NOSPACE => "NOSPACE".to_string(),
WRDE_BADCHAR => "BADCHAR".to_string(),
WRDE_BADVAL => "BADVAL".to_string(),
WRDE_CMDSUB => "CMDSUB".to_string(),
WRDE_SYNTAX => "SYNTAX".to_string(),
WRDE_SECCOMP => "SECCOMP".to_string(),
WRDE_TIMEOUT => "TIMEOUT".to_string(),
_ => format!("ERR{}", 128 - err),
}
}
#[cfg(not(target_os = "android"))]
fn quote(input: &str) -> String {
format!("'{}'", input.replace("'", "'\\''"))
}
#[cfg(not(target_os = "android"))]
const SHELL_BUILTINS: &[&str] = &[
".", "alias", "cd", "export", "hash", "readonly", "set", "shift", "source", "umask", "unalias",
"unset",
];