#![allow(unused)]
use std::{
env,
io::{Read, Seek, SeekFrom, Stdin, Write},
os::fd::{AsFd, AsRawFd, BorrowedFd},
process::{exit, ExitCode},
};
use dur::Duration;
use memchr::{arch::all::is_equal, memchr_iter};
use nix::{
errno::Errno,
unistd::{isatty, Gid, Uid},
};
use syd::{
compat::MFdFlags,
config::*,
cookie::safe_memfd_create,
debug, eprintfln,
err::err2no,
fd::{seal_memfd_all, set_cloexec},
human_size,
io::ReadFd,
lookup::safe_copy_if_exists,
path::{XPath, XPathBuf, XPathCow},
printfln,
syslog::LogLevel,
xfmt, xpath,
};
#[cfg(all(
not(target_os = "android"),
not(target_arch = "loongarch64"),
not(target_arch = "riscv64"),
target_page_size_4k,
target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;
#[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() {
let _ = eprintfln!("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": xfmt!("created memory-file {} with close-on-exec flag set",
file.as_raw_fd()));
if opte {
file.write_all(b"set -e\n").map_err(|error| err2no(&error))?;
}
if optx {
file.write_all(b"set -x\n").map_err(|error| err2no(&error))?;
}
file.write_all(ESYD_SH.as_bytes()).map_err(|error| err2no(&error))?;
file.write_all(b"\n").map_err(|error| err2no(&error))?;
if optl {
safe_copy_if_exists(&mut file, "/etc/syd/init_login.sh")?;
file.write_all(b"\n").map_err(|error| err2no(&error))?;
}
safe_copy_if_exists(&mut file, "/etc/syd/init.sh")?;
file.write_all(b"\n").map_err(|error| err2no(&error))?;
let uid = Uid::effective();
let name = match env::var_os("USER") {
Some(name) => XPathCow::Owned(XPathBuf::from(name)),
None => XPathCow::Borrowed(XPath::from_bytes(b"nobody")),
};
let home = match env::var_os("HOME") {
Some(home) => XPathCow::Owned(XPathBuf::from(home)),
None => XPathCow::Borrowed(XPath::from_bytes(b"/var/empty")),
};
if optl {
let init = home
.try_join(b".config")?
.try_join(b"syd")?
.try_join(b"init_login.sh")?;
safe_copy_if_exists(&mut file, &init)?;
file.write_all(b"\n").map_err(|error| err2no(&error))?;
}
let init = home
.try_join(b".config")?
.try_join(b"syd")?
.try_join(b"init.sh")?;
safe_copy_if_exists(&mut file, &init)?;
file.write_all(b"\n").map_err(|error| err2no(&error))?;
let mut args = args.into_iter().peekable();
if optc {
if args.peek().is_none() {
eprintfln!("syd-sh: -c requires an argument!")?;
return Ok(ExitCode::FAILURE);
}
let mut argc = 0;
let mut trace = XPathBuf::new();
for arg in args {
argc += 1;
let arg = arg.to_str().ok_or(Errno::EINVAL)?;
file.write_all(quote(arg)?.as_bytes()).map_err(|error| err2no(&error))?;
file.write_all(b" ").map_err(|error| err2no(&error))?;
if optx {
trace.try_append_bytes(arg.as_bytes())?;
trace.try_append_byte(b' ')?;
}
}
file.write_all(b"\n").map_err(|error| err2no(&error))?;
debug!("ctx": "sh",
"msg": xfmt!("written {argc} argument{} into memory-file {}",
if argc > 1 { "s" } else { "" },
file.as_raw_fd()));
if optx {
let mut stderr = std::io::stderr();
stderr.write_all(b"+ ").map_err(|error| err2no(&error))?;
stderr.write_all(trace.as_bytes()).map_err(|error| err2no(&error))?;
stderr.write_all(b"\n").map_err(|error| err2no(&error))?;
}
seal_memfd_all(&file)?;
debug!("ctx": "sh",
"msg": xfmt!("sealed memory-file {} against grows, shrinks and writes",
file.as_raw_fd()));
set_cloexec(&file, false)?;
debug!("ctx": "sh",
"msg": xfmt!("set close-on-exec flag to off for memory-file {}",
file.as_raw_fd()));
let shell = xpath!("`. /proc/self/fd/{}`", file.as_raw_fd())?;
let shell = std::str::from_utf8(shell.as_bytes()).or(Err(Errno::EILSEQ))?;
debug!("ctx": "sh",
"msg": xfmt!("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) => {
let mut stdout = std::io::stdout();
stdout.write_all(out.as_bytes()).map_err(|error| err2no(&error))?;
stdout.write_all(b"\n").map_err(|error| err2no(&error))?;
return Ok(ExitCode::SUCCESS);
}
Err(err) => {
let err = err.into();
if opte {
eprintfln!("syd-sh: 1: {}", wrde2str(err))?;
}
exit(err);
}
};
}
#[expect(clippy::disallowed_methods)]
#[expect(clippy::disallowed_types)]
let input: Option<(Input, XPathCow)> = if let Some(path) = args.next() {
Some((
Input::File(std::fs::File::open(&path)?),
XPathCow::Owned(XPathBuf::from(path)),
))
} else if isatty(std::io::stdin()).unwrap_or(false) {
None
} else {
Some((
Input::Stdin(std::io::stdin()),
XPathCow::Borrowed(XPath::from_bytes(b"standard input")),
))
};
if let Some((mut input_file, input_name)) = input {
debug!("ctx": "sh",
"msg": xfmt!("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": xfmt!("copied {} from {input_name} to memory-file {}",
human_size(copylen.try_into()?),
file.as_raw_fd()));
seal_memfd_all(&file)?;
debug!("ctx": "sh",
"msg": xfmt!("sealed memory-file {} against grows, shrinks and writes",
file.as_raw_fd()));
set_cloexec(&file, false)?;
debug!("ctx": "sh",
"msg": xfmt!("set close-on-exec flag to off for memory-file {}",
file.as_raw_fd()));
let shell = xpath!("`. /proc/self/fd/{}`", file.as_raw_fd())?;
let shell = std::str::from_utf8(shell.as_bytes()).or(Err(Errno::EILSEQ))?;
debug!("ctx": "sh",
"msg": xfmt!("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) => {
let mut stdout = std::io::stdout();
stdout.write_all(val.as_bytes()).map_err(|error| err2no(&error))?;
stdout.write_all(b"\n").map_err(|error| err2no(&error))?;
return Ok(ExitCode::SUCCESS);
}
Err(err) => {
let err = err.into();
if opte {
eprintfln!("syd-sh: {err}")?;
}
exit(err);
}
}
}
if Uid::current() != Uid::effective() || Gid::current() != Gid::effective() {
eprintfln!("syd-sh: real and effective IDs must match in interactive mode!")?;
return Ok(ExitCode::FAILURE);
}
let mut stdin = std::io::stdin();
let mut line = XPathBuf::new();
let mut prompt = XPathCow::Borrowed(XPath::from_bytes(b"; "));
loop {
std::io::stderr().write_all(prompt.as_bytes()).map_err(|error| err2no(&error))?;
line.clear();
let mut read_any = false;
loop {
let mut byte = [0u8; 1];
if stdin.read(&mut byte).map_err(|error| err2no(&error))? == 0 {
break;
}
read_any = true;
if byte[0] == b'\n' {
break;
}
if byte[0] != b'\r' {
line.try_append_byte(byte[0])?;
}
}
if !read_any {
eprintfln!()?;
break;
}
let input = line.as_bytes();
if matches!(input.first(), Some(&b'>')) {
let histlen = file.seek(SeekFrom::End(0)).map_err(|error| err2no(&error))?;
if let Some(input) = input.get(1..) {
file.write_all(input).map_err(|error| err2no(&error))?;
}
file.write_all(b"\n").map_err(|error| err2no(&error))?;
let len = input.len();
prompt = XPathCow::Borrowed(XPath::from_bytes(b"OKHIST; "));
debug!("ctx": "sh",
"msg": xfmt!("pushed {} into memory-file of {}",
human_size(len),
human_size(histlen.try_into()?)));
continue;
} else if matches!(input.trim_ascii().first(), None | Some(&b'#')) {
prompt = XPathCow::Borrowed(XPath::from_bytes(b"; "));
continue;
} else if optx {
let mut stderr = std::io::stderr();
stderr.write_all(b"+ ").map_err(|error| err2no(&error))?;
stderr.write_all(input).map_err(|error| err2no(&error))?;
stderr.write_all(b"\n").map_err(|error| err2no(&error))?;
}
#[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": xfmt!("created memory-file {} with sealing allowed",
fdup.as_raw_fd()));
file.seek(SeekFrom::Start(0)).map_err(|error| err2no(&error))?;
let copylen = syd::io::copy(&mut file, &mut fdup)?;
debug!("ctx": "sh",
"msg": xfmt!("copied {} from memory-file {} to {}",
human_size(copylen.try_into()?),
file.as_raw_fd(),
fdup.as_raw_fd()));
fdup.write_all(input).map_err(|error| err2no(&error))?;
debug!("ctx": "sh",
"msg": xfmt!("written {} of input to memory-file {}",
human_size(input.len()),
fdup.as_raw_fd()));
seal_memfd_all(&fdup)?;
debug!("ctx": "sh",
"msg": xfmt!("sealed memory-file {} against grows, shrinks and writes",
fdup.as_raw_fd()));
set_cloexec(&fdup, false)?;
debug!("ctx": "sh",
"msg": xfmt!("set close-on-exec flag to off for memory-file {}",
fdup.as_raw_fd()));
let shell = xpath!("`. /proc/self/fd/{} 2>&1`", fdup.as_raw_fd())?;
let shell = std::str::from_utf8(shell.as_bytes()).or(Err(Errno::EILSEQ))?;
debug!("ctx": "sh",
"msg": xfmt!("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": xfmt!("closed memory-file {fdup_fd} after WordExp::expand returned {} of output",
human_size(val.len())));
}
Err(ref err) => {
debug!("ctx": "sh",
"msg": xfmt!("closed memory-file {fdup_fd} after WordExp::expand error {err}"));
}
}
match result {
Ok(val) => {
prompt = XPathCow::Borrowed(XPath::from_bytes(b"; "));
let mut stdout = std::io::stdout();
stdout.write_all(val.as_bytes()).map_err(|error| err2no(&error))?;
stdout.write_all(b"\n").map_err(|error| err2no(&error))?;
}
Err(WordExpError::BadValue) if !input.contains(&b';') => {
prompt = XPathCow::Borrowed(XPath::from_bytes(b"; "));
if let Some(cmd) = input.trim_ascii_start().split(|c| c.is_ascii_whitespace()).next() {
for builtin in SHELL_BUILTINS {
if is_equal(cmd, builtin.as_bytes()) {
let histlen = file.seek(SeekFrom::End(0)).map_err(|error| err2no(&error))?;
file.write_all(input).map_err(|error| err2no(&error))?;
file.write_all(b"\n").map_err(|error| err2no(&error))?;
debug!("ctx": "sh",
"msg": xfmt!("pushed {} into memory-file of {}",
human_size(input.len() + 1),
human_size(histlen.try_into()?)));
break;
}
}
}
}
Err(err) => {
prompt = XPathCow::Owned(xpath!("{}; ", wrde2str(err.into()))?);
}
}
}
Ok(ExitCode::SUCCESS)
}
#[cfg(not(target_os = "android"))]
fn help() -> Result<(), Errno> {
printfln!("Usage:")?;
printfln!(" syd-sh [-helsx] [--] [_command_file_ [argument...]]")?;
printfln!(" syd-sh [-helx] -c _command_string_ [_command_name_ [argument...]]")?;
printfln!("Simple confined shell based on wordexp(3)")?;
printfln!("Given no arguments, enter read-eval-print loop.")?;
printfln!("Given -c with an argument, evaluate and print the result.")?;
Ok(())
}
#[cfg(not(target_os = "android"))]
fn wrde2str(err: i32) -> &'static str {
use syd::wordexp::*;
match err {
0 => "",
128 => "ERR?",
WRDE_NOSPACE => "NOSPACE",
WRDE_BADCHAR => "BADCHAR",
WRDE_BADVAL => "BADVAL",
WRDE_CMDSUB => "CMDSUB",
WRDE_SYNTAX => "SYNTAX",
WRDE_SECCOMP => "SECCOMP",
WRDE_TIMEOUT => "TIMEOUT",
_ => "ERR?",
}
}
#[cfg(not(target_os = "android"))]
fn quote(input: &str) -> Result<XPathBuf, Errno> {
let bytes = input.as_bytes();
let mut out = XPathBuf::new();
out.try_append_byte(b'\'')?;
let mut start = 0;
for pos in memchr_iter(b'\'', bytes) {
out.try_append_bytes(&bytes[start..pos])?;
out.try_append_bytes(b"'\\''")?;
start = pos.checked_add(1).ok_or(Errno::EOVERFLOW)?;
}
out.try_append_bytes(&bytes[start..])?;
out.try_append_byte(b'\'')?;
Ok(out)
}
#[cfg(not(target_os = "android"))]
const SHELL_BUILTINS: &[&str] = &[
".", "alias", "cd", "export", "hash", "readonly", "set", "shift", "source", "umask", "unalias",
"unset",
];