Merge nucleic/olive-ember-seal-q7vk into dev
This commit is contained in:
Generated
+7
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user