Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor c3d14faab0 fix(engine): retry a leased file with a plain open; the FIFO test uses its own folder 2026-11-11 22:37:47 +00:00
Dylan M. Taylor 57d900b207 fix(engine): open native files non-blocking so a FIFO cannot stall the reader 2026-11-11 20:44:14 +00:00
Dylan M. Taylor 763816d27c ci: run Harden-Runner on every job (#3786)
Adds step-security/harden-runner (audit, pinned to v2.22.4 like
craft-common-actions) as the first step of each job that did not have it,
so every workflow in the org's public repos reports its egress to
StepSecurity. Audit, not block: block-mode allow lists come from each
job's observed traffic.
2026-11-11 12:11:59 -05:00
FredApps 90180f4d87 Sandbox: build on riscv64, where libc names no kexec_file_load (#3777)
The seccomp list named libc::SYS_kexec_file_load, which libc (0.2.190) defines for x86_64 and
aarch64 but not riscv64, so the release's Linux riscv64 build stopped with E0425. riscv64 takes
the number from the kernel's generic table, 294, as aarch64 does; the test checks libc's aarch64
value against it.
2026-11-11 17:35:44 +01:00
FredApps d69a285efb Release: GridCraft v0.19.0 (#3770) 2026-11-11 16:38:52 +01:00
4 changed files with 98 additions and 5 deletions
+4
View File
@@ -27,6 +27,10 @@ jobs:
name: Windows installation policy (no app build)
runs-on: windows-latest
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
+43 -4
View File
@@ -924,17 +924,56 @@ fn script_sandbox_check(path: &str) -> Result<()> {
#[cfg(not(target_arch = "wasm32"))]
const MAX_READ_BYTES: u64 = 2 * 1024 * 1024 * 1024;
/// `O_NONBLOCK` as the platform defines it (std offers no constant and the crate takes no libc
/// dependency); `None` on a Unix not listed, where the pathname check alone applies.
#[cfg(all(unix, not(target_arch = "wasm32")))]
const O_NONBLOCK: Option<i32> =
if cfg!(any(target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly"))
{
Some(0x4)
} else if cfg!(all(
any(target_os = "linux", target_os = "android"),
any(target_arch = "mips", target_arch = "mips64", target_arch = "mips32r6", target_arch = "mips64r6")
)) {
Some(0x80)
} else if cfg!(all(any(target_os = "linux", target_os = "android"), any(target_arch = "sparc", target_arch = "sparc64"))) {
Some(0x4000)
} else if cfg!(any(target_os = "linux", target_os = "android")) {
Some(0o4000)
} else {
None
};
/// Opens `path` for reading without blocking on a FIFO or device that has no writer.
#[cfg(not(target_arch = "wasm32"))]
fn open_nonblocking(path: &std::path::Path) -> std::io::Result<std::fs::File> {
let mut opts = std::fs::OpenOptions::new();
opts.read(true);
#[cfg(unix)]
if let Some(flag) = O_NONBLOCK {
use std::os::unix::fs::OpenOptionsExt;
opts.custom_flags(flag);
}
opts.open(path)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn read_file(path: &str) -> Result<Vec<u8>> {
use std::io::Read;
script_sandbox_check(path)?;
// Checked before opening: opening a FIFO nobody writes to blocks the calling (GUI) thread.
// shortcut: a FIFO swapped in between this check and the open still blocks; close that with
// O_NONBLOCK when the crate takes a libc dependency.
// Checked before opening: opening a FIFO nobody writes to blocks the calling (GUI) thread. The
// open itself is non-blocking where the platform's flag is known, so a FIFO swapped in after
// this check cannot block it either; the opened handle is checked again below.
if !std::fs::metadata(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?.is_file() {
return Err(EngineError::Other(format!("{path}: only regular files can be opened")));
}
let f = std::fs::File::open(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let f = match open_nonblocking(std::path::Path::new(path)) {
Ok(f) => f,
// A conflicting lease (Samba, NFS) makes a non-blocking open of a regular file wait to be retried; the
// path was just checked to be a regular file, so open it the plain way, as before.
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => std::fs::File::open(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?,
Err(e) => return Err(EngineError::Other(format!("{path}: {e}"))),
};
// A symlink to an endless device reports length 0 but streams forever, so metadata
// alone never bounds the read: require a real file and cap the bytes actually taken.
if !f.metadata().map_err(|e| EngineError::Other(format!("{path}: {e}")))?.is_file() {
+41
View File
@@ -810,3 +810,44 @@ fn reading_a_fifo_is_refused_without_blocking() {
assert!(err.contains("only regular files"), "{err}");
let _ = std::fs::remove_dir_all(&dir);
}
/// Opening a FIFO nobody writes to returns at once (it is not a regular file), where a plain open
/// blocks the calling thread for good.
#[cfg(unix)]
#[test]
fn a_fifo_without_a_writer_does_not_block_the_open() {
if O_NONBLOCK.is_none() {
return;
}
/// Removes the folder when the test ends, however it ends.
struct Cleanup(std::path::PathBuf);
impl Drop for Cleanup {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
// Created exclusively (a fresh name per try), so no other process shares it.
let dir = (0..100)
.map(|n| {
std::env::temp_dir().join(format!(
"gridcraft-fifo-{}-{n}-{}",
std::process::id(),
std::time::UNIX_EPOCH.elapsed().map_or(0, |t| t.as_nanos())
))
})
.find(|d| std::fs::create_dir(d).is_ok())
.expect("a fresh temporary folder");
let _cleanup = Cleanup(dir.clone());
let fifo = dir.join("pipe");
if !std::process::Command::new("mkfifo").arg(&fifo).status().is_ok_and(|s| s.success()) {
return; // no mkfifo here
}
let (tx, rx) = std::sync::mpsc::channel();
let path = fifo.clone();
std::thread::spawn(move || {
let _ = tx.send(open_nonblocking(&path).and_then(|f| f.metadata()).map(|m| m.is_file()));
});
let opened = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("the open blocked");
assert!(!opened.unwrap());
assert!(read_file(&fifo.to_string_lossy()).is_err());
}
+10 -1
View File
@@ -31,6 +31,12 @@ const SYS_SETXATTRAT: libc::c_long = 463;
const SYS_REMOVEXATTRAT: libc::c_long = 466;
const SYS_OPEN_TREE_ATTR: libc::c_long = 467;
const SYS_FILE_SETATTR: libc::c_long = 469;
// `libc` names no kexec_file_load on riscv64 (0.2.190). It takes 294 from the kernel's generic
// table there, as on aarch64, where `libc` names it.
#[cfg(target_arch = "riscv64")]
const SYS_KEXEC_FILE_LOAD: libc::c_long = 294;
#[cfg(not(target_arch = "riscv64"))]
const SYS_KEXEC_FILE_LOAD: libc::c_long = libc::SYS_kexec_file_load;
pub fn support() -> Support {
match arch() {
@@ -223,7 +229,7 @@ fn denied_calls() -> Vec<libc::c_long> {
libc::SYS_finit_module,
libc::SYS_delete_module,
libc::SYS_kexec_load,
libc::SYS_kexec_file_load,
SYS_KEXEC_FILE_LOAD,
libc::SYS_swapon,
libc::SYS_swapoff,
libc::SYS_reboot,
@@ -455,6 +461,9 @@ mod tests {
#[cfg(target_arch = "x86_64")]
assert_eq!(SYS_FCHMODAT2, libc::SYS_fchmodat2);
assert_eq!((SYS_SETXATTRAT, SYS_REMOVEXATTRAT, SYS_OPEN_TREE_ATTR, SYS_FILE_SETATTR), (463, 466, 467, 469));
// The generic table's number, which riscv64 writes here, is the one `libc` gives aarch64.
#[cfg(target_arch = "aarch64")]
assert_eq!(libc::SYS_kexec_file_load, 294);
}
#[test]