Merge nucleic/olive-ember-seal-q7vk into dev

This commit is contained in:
2026-07-18 15:14:54 -07:00
parent 19218c7dae
commit 5ff6fd8631
5 changed files with 192 additions and 2 deletions
Generated
+7
View File
@@ -1366,6 +1366,13 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "naros-init"
version = "0.1.0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "nash" name = "nash"
version = "0.1.0" version = "0.1.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["nash", "nash-observe"] members = ["nash", "nash-observe", "naros-init"]
[profile.release] [profile.release]
strip = true strip = true
+21 -1
View File
@@ -6,7 +6,27 @@ stays open until fixed in the fork (or upstream) and re-verified by the corpus.
## Open ## Open
(none) ### D2 — non-ASCII bytes re-encoded through `read`/`echo` under C/empty locale
- **Found**: narOS N0 rootfs validation (first real-world install run under the
divert): with `/bin/sh → nash`, `ca-certificates`' postinst
(`update-ca-certificates`, a `while read`-over-conf loop) mangled the UTF-8
filename `NetLock_Arany_=Class_Gold=_Főtanúsítvány.crt` and failed the
whole image build. Worked around structurally in `os/mkimage/build-rootfs.sh`
(the divert is now always the image's last configure step), but any
*runtime* `apt install` inside narOS still runs postinsts under nash, so this
blocks narOS/M3 forcing gates until fixed.
- **Repro** (nash `0.4.0` musl arm64, empty locale):
`echo 'Főtanúsítvány' | nash -c 'while read x; do echo "$x"; done'` —
bash emits the input bytes unchanged (`F \305\221 t a n \303\272 …`); nash
emits each byte Latin-1→UTF-8 double-encoded (`F \303\205 \302\221 …`).
- **Suspected root cause**: brush decodes input bytes to `String` with a lossy/
Latin-1 assumption on the `read` path (or at word-splitting) instead of
keeping raw bytes; on output the char sequence is re-encoded as UTF-8.
POSIX shells treat variable values as byte strings.
- **Severity**: silent data corruption (not a parse failure, so the §4.1
bash-fallback cannot catch it). Needs a corpus case (`utf8-bytes-passthru`)
and a byte-preservation sweep of read/expansion/heredoc paths.
## Closed ## Closed
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "naros-init"
version = "0.1.0"
edition = "2021"
description = "narOS PID-1 supervisor for container surfaces (docs/NAROS.md §5)"
license = "MIT"
[dependencies]
libc = "0.2"
+154
View File
@@ -0,0 +1,154 @@
//! naros-init — narOS PID-1 supervisor for container surfaces (docs/NAROS.md §5).
//!
//! Supervision and plumbing only, no policy:
//! - reaps zombies (the historical job of PID 1);
//! - forwards SIGTERM/SIGINT to supervised children;
//! - `naros-init -- CMD ARGS…` supervises a primary command and exits with its status
//! (129+signum on signal death), replacing `sh -c` wrappers as the container entrypoint;
//! - with no primary command it is the keepalive that replaces ContainerEngine's
//! sleep loop, staying alive until signalled;
//! - NAROS_BRIDGE=1 additionally supervises the control bridge
//! (`node /opt/nucleic/control-bridge.js`), restarting it with backoff — the bridge is
//! best-effort transport, so its failures never affect the primary command or init.
//!
//! In the VM desktop flavor systemd stays PID 1 and this binary runs as a role unit.
use std::env;
use std::process::{exit, Command};
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::{Duration, Instant};
static PENDING_SIGNAL: AtomicI32 = AtomicI32::new(0);
extern "C" fn on_signal(sig: libc::c_int) {
PENDING_SIGNAL.store(sig, Ordering::SeqCst);
}
fn install_handlers() {
unsafe {
let handler = on_signal as *const () as usize;
for sig in [libc::SIGTERM, libc::SIGINT] {
libc::signal(sig, handler);
}
// SIG_DFL for SIGCHLD keeps children reapable via waitpid below.
}
}
/// Reap every exited child; returns the primary's status when it is among them.
fn reap(primary: Option<libc::pid_t>, bridge: Option<libc::pid_t>) -> (Option<i32>, bool) {
let mut primary_status = None;
let mut bridge_died = false;
loop {
let mut status: libc::c_int = 0;
let pid = unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG) };
if pid <= 0 {
break;
}
let code = if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else if libc::WIFSIGNALED(status) {
128 + libc::WTERMSIG(status)
} else {
1
};
if Some(pid) == primary {
primary_status = Some(code);
} else if Some(pid) == bridge {
bridge_died = true;
}
}
(primary_status, bridge_died)
}
fn forward(sig: i32, pids: &[Option<libc::pid_t>]) {
for pid in pids.iter().flatten() {
unsafe {
libc::kill(*pid, sig);
}
}
}
fn spawn_bridge() -> Option<libc::pid_t> {
const BRIDGE: &str = "/opt/nucleic/control-bridge.js";
if !std::path::Path::new(BRIDGE).exists() {
return None;
}
match Command::new("node").arg(BRIDGE).spawn() {
Ok(child) => Some(child.id() as libc::pid_t),
Err(err) => {
eprintln!("naros-init: bridge spawn failed: {err}");
None
}
}
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.get(1).map(String::as_str) == Some("--version") {
println!("naros-init {}", env!("CARGO_PKG_VERSION"));
return;
}
install_handlers();
let cmd: Vec<&String> = match args.iter().position(|a| a == "--") {
Some(i) => args[i + 1..].iter().collect(),
None => Vec::new(),
};
let mut primary: Option<libc::pid_t> = None;
if let Some((prog, rest)) = cmd.split_first() {
match Command::new(prog).args(rest).spawn() {
Ok(child) => primary = Some(child.id() as libc::pid_t),
Err(err) => {
eprintln!("naros-init: exec {prog}: {err}");
exit(127);
}
}
}
let want_bridge = env::var("NAROS_BRIDGE").ok().as_deref() == Some("1");
let mut bridge = if want_bridge { spawn_bridge() } else { None };
let mut bridge_backoff = Duration::from_millis(500);
let mut bridge_retry_at: Option<Instant> = None;
loop {
let sig = PENDING_SIGNAL.swap(0, Ordering::SeqCst);
if sig != 0 {
forward(sig, &[primary, bridge]);
if primary.is_none() {
// Keepalive role: the signal is our own shutdown request.
exit(128 + sig);
}
}
let (primary_status, bridge_died) = reap(primary, bridge);
if let Some(code) = primary_status {
forward(libc::SIGTERM, &[bridge]);
reap(None, bridge);
exit(code);
}
if bridge_died {
bridge = None;
bridge_retry_at = Some(Instant::now() + bridge_backoff);
bridge_backoff = (bridge_backoff * 2).min(Duration::from_secs(30));
}
if want_bridge && bridge.is_none() {
if let Some(at) = bridge_retry_at {
if Instant::now() >= at {
bridge = spawn_bridge();
bridge_retry_at = None;
if bridge.is_some() {
bridge_backoff = Duration::from_millis(500);
} else {
bridge_retry_at = Some(Instant::now() + bridge_backoff);
}
}
} else {
bridge_retry_at = Some(Instant::now());
}
}
std::thread::sleep(Duration::from_millis(100));
}
}