update docs

This commit is contained in:
2026-09-22 13:23:34 +08:00
parent 99bc3d15c5
commit 8a4788fca8
126 changed files with 7198 additions and 2425 deletions
+42 -2
View File
@@ -69,11 +69,36 @@ pub fn start(
) -> Result<Child> {
start_group(&format!("bay{bay}"), arguments, working, environment)
}
pub fn foreground(
bay: Bay,
arguments: &[String],
working: &str,
environment: &[(String, String)],
descriptors: [OwnedFd; 3],
terminal: bool,
) -> Result<Child> {
spawn(
&format!("bay{bay}"),
arguments,
working,
environment,
Some((descriptors, terminal)),
)
}
pub fn start_group(
name: &str,
arguments: &[String],
working: &str,
environment: &[(String, String)],
) -> Result<Child> {
spawn(name, arguments, working, environment, None)
}
fn spawn(
name: &str,
arguments: &[String],
working: &str,
environment: &[(String, String)],
io: Option<([OwnedFd; 3], bool)>,
) -> Result<Child> {
if !fds_common::manifest::identifier(name) {
return Err(Error("Invalid process group".into()));
@@ -95,11 +120,11 @@ pub fn start_group(
.custom_flags(libc::O_CLOEXEC)
.open(path.join("cgroup.procs"))?;
let mut command = Command::new(&arguments[0]);
let working = c(working)?;
command
.args(&arguments[1..])
.current_dir(working)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("PATH", "/usr/bin:/bin:/run/fds/bin")
.env("HOME", "/home/fds")
.env("USER", "fds")
.env("LOGNAME", "fds")
@@ -109,6 +134,14 @@ pub fn start_group(
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
command.envs(environment.iter().cloned());
let mut terminal = false;
if let Some(([input, output, errors], tty)) = io {
command
.stdin(Stdio::from(input))
.stdout(Stdio::from(output))
.stderr(Stdio::from(errors));
terminal = tty;
}
// Only async-signal-safe syscalls are used in the forked child. Writing 0
// moves the child itself, avoiding PID reuse and parent/child migration races.
unsafe {
@@ -119,6 +152,9 @@ pub fn start_group(
if libc::setsid() < 0 {
return Err(io::Error::last_os_error());
}
if terminal && libc::ioctl(0, libc::TIOCSCTTY, 0) < 0 {
return Err(io::Error::last_os_error());
}
if libc::setgroups(0, std::ptr::null()) < 0
|| libc::setgid(1000) < 0
|| libc::setuid(1000) < 0
@@ -128,6 +164,10 @@ pub fn start_group(
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0 {
return Err(io::Error::last_os_error());
}
// Resolve client-selected working directories only as the user.
if libc::chdir(working.as_ptr()) < 0 {
return Err(io::Error::last_os_error());
}
let mut mask: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut mask);
if libc::sigprocmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut()) < 0 {
+1
View File
@@ -4,6 +4,7 @@ mod data_sessions;
mod media;
mod power;
mod profiles;
mod programs;
mod recovery;
mod server;
mod software;
+1 -1
View File
@@ -228,7 +228,7 @@ impl Mounted {
("FDS_APP".into(), app.display().to_string()),
(
"PATH".into(),
format!("{}/bin:/usr/bin:/bin", app.display()),
format!("{}/bin:/usr/bin:/bin:/run/fds/bin", app.display()),
),
(
"LD_LIBRARY_PATH".into(),
+88
View File
@@ -0,0 +1,88 @@
//! A stable PATH directory is updated as validated cartridges appear/disappear.
use crate::media::Mounted;
use fds_common::{
Bay, Error, Result,
manifest::{Class, identifier},
};
use std::{
collections::BTreeMap,
fs,
os::unix::fs::{PermissionsExt, symlink},
path::Path,
};
pub const BIN: &str = "/run/fds/bin";
pub type Commands = BTreeMap<String, (Bay, String)>;
pub fn collect(mounts: &BTreeMap<Bay, Mounted>) -> Result<Commands> {
let mut result = BTreeMap::new();
for (&bay, mount) in mounts {
if mount.manifest.cartridge.class != Class::Program || mount.fault.is_some() {
continue;
}
let mut commands = Vec::new();
if let Some(software) = &mount.software {
for entry in &software.catalogue.software {
for name in entry.commands.keys() {
commands.push((
name.clone(),
format!("{}:{name}", entry.id),
format!("b{bay}:{}:{name}", entry.id),
));
}
}
} else {
let root = Path::new(&mount.path).join("app/bin");
if root.is_dir() {
for entry in fs::read_dir(root)? {
let entry = entry?;
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
if identifier(&name)
&& entry.path().is_file()
&& entry
.path()
.canonicalize()?
.starts_with(Path::new(&mount.path).join("app"))
&& entry.path().metadata()?.permissions().mode() & 0o111 != 0
{
commands.push((name.clone(), name.clone(), format!("b{bay}:{name}")));
}
}
}
}
commands.sort();
for (name, selector, qualified) in commands {
result.entry(name).or_insert((bay, selector.clone()));
// A fully qualified spelling always identifies this cartridge.
result.insert(qualified, (bay, selector));
}
}
Ok(result)
}
pub fn publish(commands: &Commands) -> Result<()> {
fs::create_dir_all(BIN)?;
fs::set_permissions(BIN, fs::Permissions::from_mode(0o755))?;
for entry in fs::read_dir(BIN)? {
let entry = entry?;
if !commands.contains_key(&entry.file_name().to_string_lossy().into_owned()) {
if !entry.file_type()?.is_symlink() {
return Err(Error(
"Unexpected file in cartridge command directory".into(),
));
}
fs::remove_file(entry.path())?;
}
}
for name in commands.keys() {
let path = Path::new(BIN).join(name);
if path.symlink_metadata().is_ok() {
if fs::read_link(&path)? != Path::new("/usr/bin/fds-program") {
return Err(Error("Unexpected cartridge command link".into()));
}
} else {
symlink("/usr/bin/fds-program", path)?;
}
}
Ok(())
}
+166 -9
View File
@@ -32,6 +32,7 @@ struct State {
profiles: profiles::Manager,
burning: burning::Manager,
power: power::Manager,
commands: crate::programs::Commands,
}
impl State {
fn scan(&mut self) -> Result<()> {
@@ -63,6 +64,7 @@ impl State {
software: None,
mount: None,
consumers: consumers::count(bay)?,
commands: Vec::new(),
};
// A hub in a bay may contain several functions, but multiple actual
// devices are ambiguous until an explicit composite policy exists.
@@ -109,8 +111,113 @@ impl State {
if !self.power.frozen() {
self.profiles.reconcile(&self.mounts, &devices)?;
}
self.commands = crate::programs::collect(&self.mounts)?;
crate::programs::publish(&self.commands)?;
for entry in &mut self.bays {
entry.commands = self
.commands
.iter()
.filter_map(|(alias, (bay, selector))| {
(*bay == entry.bay && alias.starts_with(&format!("b{bay}:"))).then(|| {
fds_common::control::PublishedCommand {
selector: selector.clone(),
alias: alias.clone(),
}
})
})
.collect();
}
Ok(())
}
fn program(&mut self, name: &str) -> Result<(Bay, fds_common::launch::Program)> {
if self.recovery || self.power.frozen() {
return Err(Error(
"Program launches are unavailable during recovery or shutdown".into(),
));
}
let (bay, selector) = self
.commands
.get(name)
.cloned()
.ok_or_else(|| Error(format!("Cartridge command {name} is no longer available")))?;
let mount = self
.mounts
.get_mut(&bay)
.ok_or_else(|| Error("Cartridge was removed".into()))?;
let (arguments, environment) = mount.program(&[selector])?;
Ok((
bay,
fds_common::launch::Program {
arguments,
environment,
},
))
}
fn foreground(
&mut self,
name: &str,
arguments: &[String],
terminal: bool,
working: &str,
term: &str,
client: &mut Client,
) -> Result<Response> {
self.program(name)?;
if arguments.len() > 120
|| arguments.iter().any(|a| a.contains('\0'))
|| !working.starts_with('/')
|| working.len() > 4096
|| working.contains('\0')
|| term.len() > 128
|| term.chars().any(char::is_control)
{
return Err(Error(
"Invalid foreground program arguments or environment".into(),
));
}
client.socket.set_nonblocking(false)?;
client
.socket
.set_read_timeout(Some(Duration::from_secs(5)))?;
client
.socket
.set_write_timeout(Some(Duration::from_secs(5)))?;
let result = (|| {
client.socket.write_all(b"R")?;
let descriptors: [OwnedFd; 3] = fds_common::launch::receive_fds(&mut client.socket, 3)?
.try_into()
.map_err(|_| Error("Expected three program I/O descriptors".into()))?;
let (bay, mut program) = self.program(name)?;
program.arguments.extend_from_slice(arguments);
program.environment.push(("TERM".into(), term.into()));
let mut child = consumers::foreground(
bay,
&program.arguments,
working,
&program.environment,
descriptors,
terminal,
)?;
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, child.id(), 0) } as i32;
if fd < 0 {
let _ = child.kill();
self.children.push(child);
return Err(std::io::Error::last_os_error().into());
}
let pidfd = unsafe { OwnedFd::from_raw_fd(fd) };
if let Err(error) = fds_common::launch::send_fds(&client.socket, &[pidfd.as_raw_fd()]) {
let _ = child.kill();
self.children.push(child);
return Err(error);
}
client.foreground = Some(child);
let mut reply = Response::failure("");
reply.error = None;
Ok(reply)
})();
client.socket.set_nonblocking(true)?;
result
}
fn mapped_devices(&self) -> Vec<(Bay, topology::UsbDevice)> {
self.bays
.iter()
@@ -628,6 +735,7 @@ impl State {
},
media_job: None,
recovery: None,
exit_status: None,
disk: None,
power: None,
})
@@ -796,6 +904,7 @@ struct Client {
offset: usize,
deadline: Instant,
uid: u32,
foreground: Option<std::process::Child>,
waiting: Option<(String, u64)>,
}
fn peer_uid(socket: &UnixStream) -> Result<Option<u32>> {
@@ -910,6 +1019,7 @@ pub fn run(notify: bool) -> Result<()> {
profiles: profiles::Manager::new(!recovery),
burning: burning::Manager::load()?,
power: power::Manager::load()?,
commands: BTreeMap::new(),
};
for n in 1..=12 {
let bay = Bay::try_from(n)?;
@@ -965,6 +1075,7 @@ pub fn run(notify: bool) -> Result<()> {
state.profiles.shutdown()?;
}
cleanup_stale_mounts()?;
crate::programs::publish(&BTreeMap::new())?;
state.scan()?;
let mut clients: Vec<Client> = Vec::new();
loop {
@@ -1000,7 +1111,7 @@ pub fn run(notify: bool) -> Result<()> {
fd: client.socket.as_raw_fd(),
events: if client.output.is_some() {
libc::POLLOUT
} else if client.waiting.is_some() {
} else if client.waiting.is_some() || client.foreground.is_some() {
0
} else {
libc::POLLIN
@@ -1010,6 +1121,7 @@ pub fn run(notify: bool) -> Result<()> {
}
let timeout = clients
.iter()
.filter(|c| c.foreground.is_none())
.map(|c| {
c.deadline
.saturating_duration_since(Instant::now())
@@ -1071,9 +1183,25 @@ pub fn run(notify: bool) -> Result<()> {
if (fds[0].revents != 0 && consume_events(&events)?) || console_changed {
state.scan()?;
}
// Reserve connection capacity for eject/status even with many foreground jobs.
let foreground_count = clients.iter().filter(|c| c.foreground.is_some()).count();
// Process existing clients before accepting more; vectors stay aligned.
for index in (0..clients.len()).rev() {
let client = &mut clients[index];
if let Some(child) = &mut client.foreground {
if let Some(status) = child.try_wait()? {
use std::os::unix::process::ExitStatusExt;
let mut reply = Response::failure("");
reply.error = None;
reply.exit_status = Some(status.into_raw());
let mut output =
serde_json::to_vec(&reply).map_err(|e| Error(e.to_string()))?;
output.push(b'\n');
client.output = Some(output);
client.foreground = None;
client.deadline = Instant::now() + Duration::from_secs(5);
}
}
let ready = fds[index + 5].revents;
if let Some((id, sequence)) = &client.waiting {
let job = state.burning.status(id);
@@ -1097,8 +1225,8 @@ pub fn run(notify: bool) -> Result<()> {
client.deadline = Instant::now() + Duration::from_secs(5);
}
}
let mut remove =
Instant::now() >= client.deadline || ready & (libc::POLLERR | libc::POLLNVAL) != 0;
let mut remove = (client.foreground.is_none() && Instant::now() >= client.deadline)
|| ready & (libc::POLLERR | libc::POLLNVAL) != 0;
if !remove && ready & libc::POLLIN != 0 && client.output.is_none() {
let mut chunk = [0u8; 4096];
match client.socket.read(&mut chunk) {
@@ -1124,9 +1252,31 @@ pub fn run(notify: bool) -> Result<()> {
client.waiting = Some((id.clone(), *sequence));
}
}
state
.reply(request, client.uid)
.unwrap_or_else(Response::failure)
if let Request::Program {
name,
arguments,
terminal,
working,
term,
} = request
{
if foreground_count >= 64 {
Response::failure(
"Too many foreground programs; close a program and retry",
)
} else {
state
.foreground(
&name, &arguments, terminal, &working,
&term, client,
)
.unwrap_or_else(Response::failure)
}
} else {
state
.reply(request, client.uid)
.unwrap_or_else(Response::failure)
}
}
Err(_) => Response::failure("Invalid control request"),
}
@@ -1140,7 +1290,9 @@ pub fn run(notify: bool) -> Result<()> {
.unwrap();
}
output.push(b'\n');
if client.waiting.is_some() {
if client.foreground.is_some() {
// SIGCHLD wakes the loop when this foreground command exits.
} else if client.waiting.is_some() {
client.deadline = Instant::now() + Duration::from_secs(90);
} else {
client.output = Some(output);
@@ -1173,7 +1325,11 @@ pub fn run(notify: bool) -> Result<()> {
remove = true;
}
if remove {
clients.swap_remove(index);
let mut removed = clients.swap_remove(index);
if let Some(mut child) = removed.foreground.take() {
let _ = child.kill();
state.children.push(child);
}
}
}
if fds[1].revents & libc::POLLIN != 0 {
@@ -1181,7 +1337,7 @@ pub fn run(notify: bool) -> Result<()> {
match listener.accept() {
Ok((socket, _)) => {
let uid = peer_uid(&socket)?;
if clients.len() >= 16 || uid.is_none() {
if clients.len() >= 128 || uid.is_none() {
continue;
}
socket.set_nonblocking(true)?;
@@ -1192,6 +1348,7 @@ pub fn run(notify: bool) -> Result<()> {
offset: 0,
deadline: Instant::now() + Duration::from_secs(5),
uid: uid.unwrap(),
foreground: None,
waiting: None,
});
}
+53 -6
View File
@@ -2,7 +2,7 @@
use crate::media::{self, c, checked};
use fds_burn::{device::Disk, image};
use fds_common::{Bay, Error, Result, read_text, sysfs::BlockPartition};
use fds_software::{Catalogue, archive};
use fds_software::{Catalogue, archive, tree};
use std::{
collections::{BTreeMap, BTreeSet},
fs::{self, File, OpenOptions},
@@ -136,7 +136,14 @@ impl Mounted {
&format!("/proc/self/fd/{}", source.as_raw_fd()),
&path,
"erofs",
libc::MS_RDONLY | libc::MS_NOEXEC | libc::MS_NOSUID | libc::MS_NODEV,
libc::MS_RDONLY
| libc::MS_NOSUID
| libc::MS_NODEV
| if result.catalogue.format == 1 {
libc::MS_NOEXEC
} else {
0
},
"",
)?;
result.payloads.insert(
@@ -148,7 +155,11 @@ impl Mounted {
key,
},
);
let bundles = Path::new(&path).join("bundles");
let bundles = Path::new(&path).join(if result.catalogue.format == 2 {
"programs"
} else {
"bundles"
});
if !fs::symlink_metadata(&bundles)?.is_dir() {
return Err(Error("Payload bundles must be a real directory".into()));
}
@@ -157,14 +168,20 @@ impl Mounted {
.software
.iter()
.filter(|s| s.partition == spec.number)
.map(|s| format!("{}.tar.xz", s.id))
.map(|s| {
if s.installed {
s.id.clone()
} else {
format!("{}.tar.xz", s.id)
}
})
.collect();
let actual: BTreeSet<_> = fs::read_dir(&bundles)?
.map(|e| Ok(e?.file_name().to_string_lossy().into_owned()))
.collect::<Result<_>>()?;
if actual != expected {
if actual != expected || fs::read_dir(&path)?.count() != 1 {
return Err(Error(
"Payload archive inventory disagrees with catalogue".into(),
"Payload software inventory disagrees with catalogue".into(),
));
}
for software in result
@@ -173,6 +190,10 @@ impl Mounted {
.iter()
.filter(|s| s.partition == spec.number)
{
if software.installed {
tree::verify(&Path::new(&path).join(software.root_path()), software)?;
continue;
}
if archive::open(&Path::new(&path).join(software.archive_path()))?
.metadata()?
.len()
@@ -226,6 +247,32 @@ impl Mounted {
if media::key(&payload.partition)? != payload.key {
return Err(Error("Software payload was removed".into()));
}
if software.installed {
let root = Path::new(&payload.path).join(software.root_path());
let mut args = tree::executable(&root, executable)?;
args.extend_from_slice(&arguments[1..]);
let root = root.display().to_string();
return Ok((
args,
vec![
("FDS_APP".into(), root.clone()),
(
"PATH".into(),
format!("{root}/usr/bin:{root}/bin:/usr/bin:/bin:/run/fds/bin"),
),
(
"LD_LIBRARY_PATH".into(),
format!("{root}/usr/lib:{root}/lib"),
),
(
"XDG_DATA_DIRS".into(),
format!("{root}/usr/share:/usr/share"),
),
("DISPLAY".into(), ":0".into()),
("XAUTHORITY".into(), "/run/fds/x11/authority".into()),
],
));
}
if !self.caches.contains_key(id) {
// Each executable tree receives its own bounded, read-only tmpfs.
// Root owns every path; the consumer only receives ordinary UID 1000.