Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ descendant teardown, or fresh evidence on the other five targets.
Until 1.0, the four first-party clients use an exact Cargo requirement:

```toml
kernal-api = { version = "=0.1.19", features = ["..."] }
kernal-api = { version = "=0.1.20", features = ["..."] }

[profile.dev.package.kernal-api]
codegen-units = 1
Expand All @@ -107,7 +107,7 @@ codegen-units = 1
codegen-units = 1
```

The Python companion is likewise pinned with `kernal-api==0.1.19` when used by
The Python companion is likewise pinned with `kernal-api==0.1.20` when used by
first-party Python tooling. A source checkout may temporarily use a path patch
only on an explicit migration branch; release branches must resolve the exact
registry version. There is no `optional = true` legacy implementation behind
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kernal-api"
version = "0.1.19"
version = "0.1.20"
build = "build.rs"
edition = "2021"
rust-version = "1.95.0"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ adds `kernal-api` a second time, as a build-dependency with only this feature:

```toml
[dependencies]
kernal-api = { version = "=0.1.19", features = ["window-icon"] }
kernal-api = { version = "=0.1.20", features = ["window-icon"] }

[build-dependencies]
kernal-api = { version = "=0.1.19", default-features = false, features = ["build-resources"] }
kernal-api = { version = "=0.1.20", default-features = false, features = ["build-resources"] }
```

Features enabled in the `[dependencies]` entry never reach the build script:
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/compiler-guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/compiler-guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ required-features = ["guest-proof"]
# Independently runnable Core compiler/cache and public-facade hash proofs for #13.
zccache-compiler = { git = "https://github.com/zackees/zccache", rev = "c6ddfa974a4920a127eac81773db6a5c56cd30a7", default-features = false }
zccache-hash = { git = "https://github.com/zackees/zccache", rev = "2543136ea8b648b295d2f7115a19656ff0854531", default-features = false }
kernal-api = { version = "=0.1.19", path = "../../..", default-features = false }
kernal-api = { version = "=0.1.20", path = "../../..", default-features = false }
base64 = "=0.22.1"
serde = { version = "=1.0.229", features = ["derive"] }
serde_json = "=1.0.151"
2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/component-guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/component-guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ compiler-artifact-output = []
zccache-compiler = { git = "https://github.com/zackees/zccache", rev = "c6ddfa974a4920a127eac81773db6a5c56cd30a7", default-features = false }
# Exact source-only migration fixture; not published-pin acceptance.
zccache-hash = { git = "https://github.com/zackees/zccache", rev = "2543136ea8b648b295d2f7115a19656ff0854531", default-features = false }
kernal-api = { version = "=0.1.19", path = "../../..", default-features = false, features = ["wasm-component-hash-experiment"] }
kernal-api = { version = "=0.1.20", path = "../../..", default-features = false, features = ["wasm-component-hash-experiment"] }
wit-bindgen = { version = "=0.58.0", default-features = false, features = ["macros", "realloc", "async", "std"] }

[profile.release]
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/component-tools/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/component-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ execution-probe = ["engine-probe", "dep:kernal-api"]
anyhow = "=1.0.104"
wit-component = "=0.251.0"
wasmparser = "=0.251.0"
kernal-api = { version = "=0.1.19", path = "../../..", default-features = false, optional = true }
kernal-api = { version = "=0.1.20", path = "../../..", default-features = false, optional = true }
wasmtime = { version = "=45.0.0", optional = true, default-features = false, features = ["cranelift", "runtime", "component-model", "component-model-async"] }

[dev-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion examples/wasm-tauri-screenshot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ the screenshot lifecycle starts. The map uses a deterministic standard hasher
and the channel reads are non-blocking after join: the closed profile therefore
adds neither ambient `random_get` nor `poll_oneoff` imports.

The source fixture uses an exact `=0.1.19` version plus a **migration-only local
The source fixture uses an exact `=0.1.20` version plus a **migration-only local
path**; it must switch to an actually published guest-capable release before
release acceptance. The packaged facade has separately passed a Wasm check,
but this is not evidence that the guest-capable package has been published.
Expand Down
2 changes: 1 addition & 1 deletion examples/wasm-tauri-screenshot/guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/wasm-tauri-screenshot/guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ proof-block-after-capture = []
[dependencies]
# Migration-only source fixture. Replace the path with the exact release pin
# after the guest-capable facade is published; this is not release acceptance.
kernal-api = { version = "=0.1.19", path = "../../..", default-features = false }
kernal-api = { version = "=0.1.20", path = "../../..", default-features = false }
dashmap = "6.1.0"
crossbeam-channel = "0.5.15"
2 changes: 1 addition & 1 deletion guests/threaded-smoke/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion guests/threaded-smoke/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ warnings = "deny"
[dependencies]
# Migration-only source pin for the public streaming proof. The generated
# dependency below remains solely for deliberate low-level ABI probes.
kernal-api = { version = "=0.1.19", path = "../..", default-features = false }
kernal-api = { version = "=0.1.20", path = "../..", default-features = false }
kernal-api-v1-bindings = { path = "../../src/wasm/generated/v1/guest" }
dashmap = "=6.1.0"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "kernal-api"
version = "0.1.19"
version = "0.1.20"
description = "Async OS HAL, profiling, symbolization, and allocator instrumentation"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion python/kernal_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import sys
from dataclasses import dataclass

__version__ = "0.1.19"
__version__ = "0.1.20"
RUST_MSRV = "1.95.0"
SUPPORTED_SYSTEMS = frozenset({"Linux", "Darwin", "Windows"})
SUPPORTED_MACHINES = frozenset({"x86_64", "amd64", "aarch64", "arm64"})
Expand Down
2 changes: 1 addition & 1 deletion python/tests/test_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


def test_python_and_rust_versions_are_explicit() -> None:
assert kernal_api.__version__ == "0.1.19"
assert kernal_api.__version__ == "0.1.20"
assert kernal_api.RUST_MSRV == "1.95.0"


Expand Down
4 changes: 2 additions & 2 deletions src/platform/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ pub use async_io::AsyncFileIo;
mod materialize;
#[cfg(feature = "fs")]
pub use materialize::{
allocated_bytes, apply_metadata_mode, classify, file_change_marker, file_id_width,
allocated_bytes, apply_metadata_mode, await_no_writers, classify, file_change_marker, file_id_width,
hard_link_count, make_executable, metadata_mode, native_call_path, path_from_raw_bytes,
set_readonly, symlink_file, sync_directory_if_supported, volume_identity_u128,
FileChangeMarker, LinkKind,
FileChangeMarker, LinkKind, WriterWait,
};
/// Path-observed file identity with the native identifier width.
#[cfg(feature = "fs")]
Expand Down
34 changes: 34 additions & 0 deletions src/platform/fs/materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,40 @@ pub fn hard_link_count(path: &Path) -> io::Result<u64> {
native::hard_link_count(path)
}

/// Result of [`await_no_writers`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WriterWait {
/// No process held the file open for writing when last observed.
/// `waited` is how long other writers kept it open (zero when none did).
Clear {
/// Time spent waiting for other writers to close.
waited: std::time::Duration,
},
/// Some process still held the file open for writing at the deadline.
TimedOut,
/// This host or file system cannot report writers. Hosts that execute a
/// file regardless of writers report this without waiting.
Unobservable,
}

/// Wait up to `timeout` until no process (this one or any other) holds the
/// file at `path` open for writing.
///
/// Publishing an executable is only safe to `exec` once every write
/// descriptor on its inode is closed. A child forked while the publisher's
/// write descriptor was open inherits that descriptor until its own `exec`,
/// so closing the publisher's copy is not enough: Linux refuses to execute
/// the file (`ETXTBSY`) for the child's fork-to-exec window. Call this after
/// closing the file and before handing it to anything that will execute it.
/// The file is opened read-only and no handle is retained.
///
/// # Errors
///
/// Returns the error from opening `path`.
pub fn await_no_writers(path: &Path, timeout: std::time::Duration) -> io::Result<WriterWait> {
native::await_no_writers(path, timeout)
}

/// Create a symbolic link at `link` to the file `target`, which may not
/// exist yet.
///
Expand Down
47 changes: 46 additions & 1 deletion src/platform_linux/fs_materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::io;
use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
use std::path::{Path, PathBuf};

use crate::platform::fs::LinkKind;
use crate::platform::fs::{LinkKind, WriterWait};

// ---------------------------------------------------------------------------
// Replacement
Expand Down Expand Up @@ -112,6 +112,47 @@ pub fn file_change_marker(_path: &Path) -> Option<i128> {
None
}

// ---------------------------------------------------------------------------
// Writers
// ---------------------------------------------------------------------------

/// A read lease is refused (`EAGAIN`) while any process holds the inode open
/// for writing: the same condition under which `execve` fails with `ETXTBSY`.
pub fn await_no_writers(path: &Path, timeout: std::time::Duration) -> io::Result<WriterWait> {
use std::os::fd::AsRawFd as _;
use std::os::unix::fs::OpenOptionsExt as _;

let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)?;
let fd = file.as_raw_fd();
let started = std::time::Instant::now();
let mut pause = std::time::Duration::from_micros(50);
loop {
// SAFETY: `fd` is a valid descriptor owned by `file` for this call.
if unsafe { libc::fcntl(fd, libc::F_SETLEASE, libc::F_RDLCK) } == 0 {
// SAFETY: as above; releasing the lease just taken.
unsafe { libc::fcntl(fd, libc::F_SETLEASE, libc::F_UNLCK) };
return Ok(WriterWait::Clear {
waited: started.elapsed(),
});
}
match io::Error::last_os_error().raw_os_error() {
Some(libc::EAGAIN) => {}
// Leases disabled, unsupported by the file system, or a file this
// user may not lease: writers cannot be observed.
_ => return Ok(WriterWait::Unobservable),
}
let elapsed = started.elapsed();
if elapsed >= timeout {
return Ok(WriterWait::TimedOut);
}
std::thread::sleep(pause.min(timeout - elapsed));
pause = (pause * 2).min(std::time::Duration::from_millis(5));
}
}

// ---------------------------------------------------------------------------
// Links
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -198,3 +239,7 @@ pub fn native_call_path(path: &Path) -> io::Result<PathBuf> {
pub fn sync_directory_if_supported(directory: &Path) -> io::Result<()> {
std::fs::File::open(directory)?.sync_all()
}

#[cfg(test)]
#[path = "fs_materialize_tests.rs"]
mod tests;
77 changes: 77 additions & 0 deletions src/platform_linux/fs_materialize_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! Linux-native behavior of `await_no_writers`: the inherited-descriptor
//! `ETXTBSY` race it exists to close (zackees/soldr#3350).

use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
use std::os::unix::process::CommandExt as _;
use std::time::Duration;

use super::await_no_writers;
use crate::platform::fs::WriterWait;

fn executable_script(dir: &std::path::Path) -> (std::path::PathBuf, std::fs::File) {
let path = dir.join("build-script-build");
let mut file = std::fs::File::create(&path).expect("create script");
file.write_all(b"#!/bin/sh\nexit 0\n").expect("write script");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).expect("chmod");
(path, file)
}

#[test]
fn an_unwritten_file_is_clear_at_once() {
let dir = tempfile::tempdir().expect("tempdir");
let (path, file) = executable_script(dir.path());
drop(file);
match await_no_writers(&path, Duration::ZERO).expect("observe") {
WriterWait::Clear { .. } => {}
other => panic!("expected Clear, got {other:?}"),
}
}

#[test]
fn an_open_writer_in_this_process_times_out() {
let dir = tempfile::tempdir().expect("tempdir");
let (path, _writer) = executable_script(dir.path());
assert_eq!(
await_no_writers(&path, Duration::from_millis(20)).expect("observe"),
WriterWait::TimedOut
);
}

/// A child forked while the publisher's write descriptor is open keeps that
/// descriptor until its own exec. The publisher has closed its copy, yet
/// executing the file fails with `ETXTBSY` until the child execs; waiting for
/// writers makes the exec succeed.
#[test]
fn a_descriptor_inherited_across_fork_is_waited_out() {
let dir = tempfile::tempdir().expect("tempdir");
let (path, writer) = executable_script(dir.path());

let child = std::thread::spawn(|| {
let mut command = std::process::Command::new("true");
// SAFETY: the hook only sleeps, which is async-signal-safe.
unsafe {
command.pre_exec(|| {
std::thread::sleep(Duration::from_millis(400));
Ok(())
});
}
command.status().expect("run lingering child")
});
// Let the fork happen while `writer` is open, then close the publisher's copy.
std::thread::sleep(Duration::from_millis(100));
drop(writer);

let busy = std::process::Command::new(&path).status();
assert_eq!(
busy.as_ref().err().and_then(std::io::Error::raw_os_error),
Some(libc::ETXTBSY),
"the inherited descriptor must make exec fail: {busy:?}"
);
match await_no_writers(&path, Duration::from_secs(10)).expect("observe") {
WriterWait::Clear { waited } => assert!(waited > Duration::ZERO),
other => panic!("expected Clear after the child exec'd, got {other:?}"),
}
assert!(std::process::Command::new(&path).status().expect("exec after waiting").success());
assert!(child.join().expect("child thread").success());
}
13 changes: 12 additions & 1 deletion src/platform_macos/fs_materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::io;
use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
use std::path::{Path, PathBuf};

use crate::platform::fs::LinkKind;
use crate::platform::fs::{LinkKind, WriterWait};

// ---------------------------------------------------------------------------
// Replacement
Expand Down Expand Up @@ -103,6 +103,17 @@ pub fn file_change_marker(_path: &Path) -> Option<i128> {
None
}

// ---------------------------------------------------------------------------
// Writers
// ---------------------------------------------------------------------------

/// This host executes a file regardless of other processes' write
/// descriptors, so there is nothing to wait for.
pub fn await_no_writers(path: &Path, _timeout: std::time::Duration) -> io::Result<WriterWait> {
std::fs::metadata(path)?;
Ok(WriterWait::Unobservable)
}

// ---------------------------------------------------------------------------
// Links
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading