Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
31a6482
fix: unlink inline symlinks without following targets
Jun 5, 2026
b91b080
fix: preserve instrumented wasm file permissions
Jun 15, 2026
e380ecd
fix: preserve omitted host utimens timestamps
Jun 15, 2026
0708ad4
fix: align host-backed mount path resolution
brandonpayton Jun 19, 2026
fc48786
fix: carry host mount and worker resource policy into Node
Jun 16, 2026
28149dd
fix: reject invalid synthetic browser DNS names
Jun 16, 2026
83a32cb
fix: update sharedfs timestamps on content changes
Jun 17, 2026
023111a
fix: align browser network peeking and proxy fetches
Jun 17, 2026
a996514
fix: precompile exec and spawn modules before launch
Jun 17, 2026
0a49631
fix: honor exclusive creates in SharedFS
Jun 17, 2026
6ea06d0
fix: keep virtual interface name APIs consistent
Jun 17, 2026
c3ea58e
test: keep MemoryFS inode numbers representable
Jun 17, 2026
5dcd4b0
sharedfs: add indexed directory bookkeeping
Jun 18, 2026
e657398
fix: align SharedFS directory reads and append offsets
Jun 18, 2026
fbfe975
fix: update SharedFS directory mutation times
Jun 18, 2026
ae05120
fix: tighten SharedFS rename semantics
Jun 18, 2026
32a5202
fix: reject trailing-slash unlink on files
Jun 18, 2026
7540902
fix: include service aliases in browser VFS
Jun 18, 2026
d81b80d
fix: arm browser VM interrupt timers from kernel workers
Jun 18, 2026
c5711c3
fix: align clock, timer, and PHP runner behavior across hosts
brandonpayton Jun 19, 2026
e8b6d31
fix: preserve instrumented Wasm permissions in place
brandonpayton Jul 11, 2026
085a027
fix: align Node host mounts and worker resources
brandonpayton Jul 11, 2026
928b087
fix: enforce host runtime contracts across Node and browser
brandonpayton Jul 11, 2026
04b60ae
fix: make SharedFS persistence and lazy backing race-safe
brandonpayton Jul 11, 2026
348da0d
fix: make rootfs services authoritative in derived PHP images
brandonpayton Jul 11, 2026
583bcec
fix: give PHP shutdown a fresh ordinary timeout phase
brandonpayton Jul 11, 2026
74a5765
fix(netcat): avoid unsupported abortive close on Kandelo
brandonpayton Jul 11, 2026
15e0e74
fix: support Linux coarse clock requests
brandonpayton Jul 11, 2026
bb5a0b1
docs: describe VM interrupt flag stores accurately
brandonpayton Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions MANIFEST
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@
/etc/os-release f 0644 0 0
/etc/profile f 0644 0 0
/etc/motd f 0644 0 0
# services matches the ad-hoc table in
# apps/browser-demos/lib/browser-kernel.ts byte-for-byte so the cutover
# is invisible to getservbyname/getservbyport callers.
# The rootfs file is the authoritative services database. Rootfs-derived
# images inherit it rather than rebuilding or synthesizing a second table.
/etc/services f 0644 0 0

# ── Shells ─────────────────────────────────────────────────────────
Expand Down
9 changes: 9 additions & 0 deletions apps/browser-demos/pages/kandelo/kernel-host/live-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,15 @@ async function bootProfile(
maxMemoryPages: profile.init?.maxMemoryPages,
onStdout: (data) => recordProcessOutput(data, "stdout"),
onStderr: (data) => recordProcessOutput(data, "stderr"),
onHostDiagnostic: (diagnostic) => {
if (!isCurrent()) return;
host.pushDmesg({
t: bootElapsedMs(bootStartedAt),
level: "warn",
facility: "kernel",
msg: diagnostic.message,
});
},
onProcessEvent: (event) => { if (isCurrent()) host.emitProcessEvent(event); },
onHttpBridgePendingRequests: (count) => {
if (isCurrent()) host.setWebPreviewPendingRequests(count);
Expand Down
36 changes: 35 additions & 1 deletion crates/fork-instrument/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
use anyhow::{Context, Result};
use clap::Parser;
use std::fs;
use std::path::PathBuf;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use fork_instrument::{Options, analyze, instrument};

Expand Down Expand Up @@ -65,16 +67,48 @@ fn main() -> Result<()> {
let output_path = cli.output.as_ref().ok_or_else(|| {
anyhow::anyhow!("--output is required unless --discover-only is set")
})?;
// Capture this before writing: `--output` is allowed to name the input
// file, and output creation/truncation must not become the source of truth
// for the executable mode we are preserving.
let input_mode = input_mode(&cli.input)?;

let output = instrument(&input, &opts)
.with_context(|| format!("instrumenting {}", cli.input.display()))?;

fs::write(output_path, &output)
.with_context(|| format!("writing output: {}", output_path.display()))?;
preserve_input_mode(input_mode, output_path)?;

Ok(())
}

#[cfg(unix)]
fn input_mode(input_path: &Path) -> Result<u32> {
let mode = fs::metadata(input_path)
.with_context(|| format!("stat input for permissions: {}", input_path.display()))?
.permissions()
.mode();
Ok(mode)
}

#[cfg(not(unix))]
fn input_mode(_input_path: &Path) -> Result<()> {
Ok(())
}

#[cfg(unix)]
fn preserve_input_mode(input_mode: u32, output_path: &Path) -> Result<()> {
let permissions = fs::Permissions::from_mode(input_mode);
fs::set_permissions(output_path, permissions)
.with_context(|| format!("setting output permissions: {}", output_path.display()))?;
Ok(())
}

#[cfg(not(unix))]
fn preserve_input_mode(_input_mode: (), _output_path: &Path) -> Result<()> {
Ok(())
}

fn print_analysis_json(analysis: &fork_instrument::Analysis) {
// Hand-rolled JSON to avoid a serde dependency for a tiny output.
// Format is one-entry-per-line array of `{name, is_import}` objects.
Expand Down
95 changes: 95 additions & 0 deletions crates/fork-instrument/tests/cli_permissions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#![cfg(unix)]

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};

static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);

const INPUT_WAT: &str = r#"
(module
(import "kernel" "kernel_fork" (func $fork (result i32)))
(memory 1)
(func (export "_start")
(drop (call $fork))))
"#;

struct TempDir(PathBuf);

impl TempDir {
fn new(test_name: &str) -> Self {
let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"kandelo-fork-instrument-{test_name}-{}-{id}",
std::process::id()
));
fs::create_dir(&path).expect("create test directory");
Self(path)
}

fn join(&self, path: impl AsRef<Path>) -> PathBuf {
self.0.join(path)
}
}

impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}

fn write_input(path: &Path, mode: u32) {
fs::write(path, wat::parse_str(INPUT_WAT).expect("compile input WAT"))
.expect("write input Wasm");
fs::set_permissions(path, fs::Permissions::from_mode(mode)).expect("set input mode");
}

fn run_instrumenter(input: &Path, output: &Path) {
let status = Command::new(env!("CARGO_BIN_EXE_wasm-fork-instrument"))
.arg(input)
.arg("--output")
.arg(output)
.status()
.expect("run wasm-fork-instrument");
assert!(status.success(), "instrumenter exited with {status}");
}

fn mode(path: &Path) -> u32 {
fs::metadata(path)
.expect("stat output")
.permissions()
.mode()
& 0o7777
}

#[test]
fn new_output_preserves_input_mode() {
let dir = TempDir::new("new-output");
let input = dir.join("input.wasm");
let output = dir.join("output.wasm");
write_input(&input, 0o751);

run_instrumenter(&input, &output);

assert_eq!(mode(&input), 0o751);
assert_eq!(mode(&output), 0o751);
wasmparser::Validator::new()
.validate_all(&fs::read(output).expect("read output"))
.expect("instrumented output validates");
}

#[test]
fn in_place_output_preserves_input_mode() {
let dir = TempDir::new("in-place");
let input = dir.join("program.wasm");
write_input(&input, 0o711);

run_instrumenter(&input, &input);

assert_eq!(mode(&input), 0o711);
wasmparser::Validator::new()
.validate_all(&fs::read(input).expect("read output"))
.expect("instrumented output validates");
}
141 changes: 129 additions & 12 deletions crates/kernel/src/syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5546,13 +5546,42 @@ pub fn sys_exit(proc: &mut Process, host: &mut dyn HostIO, status: i32) {
proc.exit_signal = 0;
}

/// Get the current time from the specified clock.
fn is_encoded_process_cpu_clock_id(clock_id: u32) -> bool {
// musl/Linux encode process CPU clocks as (-pid-1)*8 + 2. Real encoded
// IDs are negative clockid_t values; checking only the low three bits
// would incorrectly accept positive IDs such as 10 (produced when an
// invalid negative pid is passed to clock_getcpuclockid()).
(clock_id as i32) < 0 && (clock_id & 7) == 2
}

fn host_clock_id(clock_id: u32) -> Result<u32, Errno> {
use wasm_posix_shared::clock::*;

match clock_id {
CLOCK_REALTIME | CLOCK_MONOTONIC | CLOCK_PROCESS_CPUTIME_ID
| CLOCK_THREAD_CPUTIME_ID | CLOCK_BOOTTIME => Ok(clock_id),
// Linux exposes coarse variants to libc consumers such as MariaDB.
// Kandelo's hosts do not maintain separate coarse clock sources, so
// preserve the clock domain while using the corresponding canonical
// source. Returning EINVAL here leaves MariaDB spinning before its
// first filesystem operation.
CLOCK_REALTIME_COARSE => Ok(CLOCK_REALTIME),
CLOCK_MONOTONIC_COARSE => Ok(CLOCK_MONOTONIC),
// clock_getcpuclockid() encodes a process clock as (-pid-1)*8 + 2.
// Kandelo does not yet account CPU time per process, so preserve the
// documented elapsed-time approximation without letting the host
// mistake an encoded ID for CLOCK_REALTIME.
id if is_encoded_process_cpu_clock_id(id) => Ok(CLOCK_PROCESS_CPUTIME_ID),
_ => Err(Errno::EINVAL),
}
}

pub fn sys_clock_gettime(
_proc: &Process,
host: &mut dyn HostIO,
clock_id: u32,
) -> Result<WasmTimespec, Errno> {
let (sec, nsec) = host.host_clock_gettime(clock_id)?;
let (sec, nsec) = host.host_clock_gettime(host_clock_id(clock_id)?)?;
Ok(WasmTimespec {
tv_sec: sec,
tv_nsec: nsec,
Expand All @@ -5576,13 +5605,17 @@ pub fn sys_nanosleep(
pub fn sys_clock_getres(_proc: &Process, clock_id: u32) -> Result<WasmTimespec, Errno> {
use wasm_posix_shared::clock::*;
match clock_id {
CLOCK_REALTIME | CLOCK_MONOTONIC | CLOCK_PROCESS_CPUTIME_ID | CLOCK_THREAD_CPUTIME_ID => {
Ok(WasmTimespec {
tv_sec: 0,
tv_nsec: 1_000_000,
}) // 1ms
}
id if (id & 7) == 2 => {
CLOCK_REALTIME
| CLOCK_MONOTONIC
| CLOCK_PROCESS_CPUTIME_ID
| CLOCK_THREAD_CPUTIME_ID
| CLOCK_REALTIME_COARSE
| CLOCK_MONOTONIC_COARSE
| CLOCK_BOOTTIME => Ok(WasmTimespec {
tv_sec: 0,
tv_nsec: 1_000_000,
}), // 1ms
id if is_encoded_process_cpu_clock_id(id) => {
// Per-process CPU clock: clock_getcpuclockid encodes as (-pid-1)*8 + 2
Ok(WasmTimespec {
tv_sec: 0,
Expand All @@ -5593,8 +5626,8 @@ pub fn sys_clock_getres(_proc: &Process, clock_id: u32) -> Result<WasmTimespec,
}
}

/// Sleep using a specific clock. Only relative (TIMER_RELTIME=0) mode
/// is supported — absolute mode returns ENOTSUP.
/// Sleep using a specific clock. For TIMER_ABSTIME, rewrite the requested
/// deadline to the relative duration consumed by the host channel.
pub fn sys_clock_nanosleep(
_proc: &Process,
host: &mut dyn HostIO,
Expand All @@ -5605,7 +5638,7 @@ pub fn sys_clock_nanosleep(
) -> Result<(), Errno> {
use wasm_posix_shared::clock::*;
// Validate clock_id
if clock_id != CLOCK_REALTIME && clock_id != CLOCK_MONOTONIC {
if clock_id != CLOCK_REALTIME && clock_id != CLOCK_MONOTONIC && clock_id != CLOCK_BOOTTIME {
return Err(Errno::EINVAL);
}
// Validate timespec
Expand Down Expand Up @@ -15824,6 +15857,65 @@ mod tests {
assert_eq!(ts.tv_nsec, 123456789);
}

#[test]
fn test_clock_gettime_supports_boottime_and_encoded_process_clock() {
let proc = Process::new(1);
let mut host = MockHostIO::new();

assert!(sys_clock_gettime(
&proc,
&mut host,
wasm_posix_shared::clock::CLOCK_BOOTTIME,
)
.is_ok());
// Linux's clock_getcpuclockid() encoding for PID 1.
assert!(sys_clock_gettime(&proc, &mut host, (-2_i32 * 8 + 2) as u32).is_ok());
}

#[test]
fn test_clock_calls_map_linux_coarse_clocks_to_supported_sources() {
use wasm_posix_shared::clock::{
CLOCK_MONOTONIC, CLOCK_MONOTONIC_COARSE, CLOCK_REALTIME, CLOCK_REALTIME_COARSE,
};

let proc = Process::new(1);
let mut host = MockHostIO::new();

assert_eq!(host_clock_id(CLOCK_REALTIME_COARSE), Ok(CLOCK_REALTIME));
assert_eq!(host_clock_id(CLOCK_MONOTONIC_COARSE), Ok(CLOCK_MONOTONIC),);
for clock_id in [CLOCK_REALTIME_COARSE, CLOCK_MONOTONIC_COARSE] {
assert!(sys_clock_gettime(&proc, &mut host, clock_id).is_ok());
assert!(sys_clock_getres(&proc, clock_id).is_ok());
}
}

#[test]
fn test_clock_gettime_rejects_unknown_clock() {
let proc = Process::new(1);
let mut host = MockHostIO::new();
assert!(matches!(
sys_clock_gettime(&proc, &mut host, 0x7fff_ffff),
Err(Errno::EINVAL),
));
}

#[test]
fn test_clock_calls_reject_positive_cpu_clock_bit_pattern() {
let proc = Process::new(1);
let mut host = MockHostIO::new();

// musl computes 10 for clock_getcpuclockid(-2). The kernel must return
// EINVAL so musl can translate that API result to ESRCH.
assert!(matches!(
sys_clock_gettime(&proc, &mut host, 10),
Err(Errno::EINVAL),
));
assert!(matches!(
sys_clock_getres(&proc, 10),
Err(Errno::EINVAL),
));
}

#[test]
fn test_nanosleep_rejects_negative() {
let proc = Process::new(1);
Expand Down Expand Up @@ -15857,6 +15949,31 @@ mod tests {
assert_eq!(sys_nanosleep(&proc, &mut host, &req), Ok(()));
}

#[test]
fn test_clock_nanosleep_boottime_absolute_uses_monotonic_equivalent() {
let proc = Process::new(1);
let mut host = MockHostIO::new();
let req = WasmTimespec {
tv_sec: 1_234_567_891,
tv_nsec: 123_456_789,
};
let mut relative = [0_u8; 16];

assert_eq!(
sys_clock_nanosleep(
&proc,
&mut host,
wasm_posix_shared::clock::CLOCK_BOOTTIME,
1,
&req,
relative.as_mut_ptr(),
),
Ok(()),
);
assert_eq!(i64::from_le_bytes(relative[0..8].try_into().unwrap()), 1);
assert_eq!(i64::from_le_bytes(relative[8..16].try_into().unwrap()), 0);
}

#[test]
fn test_isatty_stdin() {
let proc = terminal_process(1);
Expand Down
Loading