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
38 changes: 4 additions & 34 deletions apps/browser-demos/lib/init/vfs-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,10 @@
* and are used by demo build scripts that construct VFS images.
*/
import type { MemoryFileSystem } from "../../../../host/src/vfs/memory-fs";

const encoder = new TextEncoder();

/**
* Write a text file to the VFS. Opens with O_WRONLY|O_CREAT|O_TRUNC,
* writes the encoded content, and closes the fd.
*/
export function writeVfsFile(
fs: MemoryFileSystem,
path: string,
content: string,
mode = 0o644,
): void {
const data = encoder.encode(content);
const fd = fs.open(path, 0o1101, mode); // O_WRONLY | O_CREAT | O_TRUNC
fs.write(fd, data, 0, data.length);
fs.close(fd);
}

/**
* Write a binary file to the VFS. Opens with O_WRONLY|O_CREAT|O_TRUNC,
* writes the raw bytes, and closes the fd.
*/
export function writeVfsBinary(
fs: MemoryFileSystem,
path: string,
data: Uint8Array,
mode = 0o755,
): void {
const fd = fs.open(path, 0o1101, mode); // O_WRONLY | O_CREAT | O_TRUNC
fs.write(fd, data, 0, data.length);
fs.close(fd);
}
export {
writeVfsBinary,
writeVfsFile,
} from "../../../../host/src/vfs/image-helpers";

/**
* Create a directory, ignoring EEXIST errors.
Expand Down Expand Up @@ -79,4 +50,3 @@ export function ensureDirRecursive(
ensureDir(fs, current, mode);
}
}

27 changes: 26 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,26 @@ five-export fallback, while ABI 18 and later require role claims
and reject stale call-graph artifacts. ABI 19 combines that contract with live
main's concurrent/nested pthread continuation-buffer repair. Dlopen replay
records both the parent's memory base and exact table base, including null gaps
left by failed loads. The supported
left by failed loads. TLS-bearing side modules additionally record their live,
positive `__tls_base`. A child restores the pointer-width-correct mutable
global without calling `__wasm_init_tls`, because copied memory already holds
the parent's live TLS bytes and reinitialization would reset C++ unwinder state
and application `thread_local` values. C++ exceptions and longjmp use one
canonical pointer-width tag identity across the main image and all side
modules; a main-exported tag wins over the host-created fallback.

The dlopen replay list and its atomic pthread-fork lock live in a transient,
host-private control record. The same host build writes and reads that record
during one process lifetime; guest code and persisted artifacts never
interpret it. Changing that record's size is therefore not a guest ABI change,
while the public ABI snapshot/classifier remains authoritative.

Pthread workers have separate Wasm instances, tables, and exception tags, none
of which can be structured-cloned from the process worker. `dlopen()` from a
pthread therefore fails normally with `dlerror()`. If the process has loaded a
side module, `fork()` from a pthread returns `ENOTSUP`; an atomic process lock
excludes a racing main-worker dlopen across the pthread's archive check,
unwind, memory copy, and parent rewind. The supported
direct-main-to-side boundary and the remaining opaque cross-side callback
limitation are specified in
[fork-instrumentation.md](fork-instrumentation.md#fork-from-a-dlopened-side-module).
Expand Down Expand Up @@ -596,6 +615,12 @@ const restored = MemoryFileSystem.fromImage(image, { maxByteLength: 1024 * 1024

The image must also have been built with a large enough filesystem maximum, for example `MemoryFileSystem.create(sab, 1024 * 1024 * 1024)`. `fromImage(..., { maxByteLength })` only controls the restored buffer's runtime growth ceiling; `statfs`/`df` and allocation remain capped by the image superblock maximum.

A consumer that must stage files larger than the image's recorded allocation
ceiling first calls `rebaseToNewFileSystem(requiredMaxBytes)`. Shared image
helpers never treat a partial file as complete: `writeVfsBinary` advances over
positive short writes and throws on zero/negative progress or an underlying
filesystem error, while still closing the descriptor.

Kandelo browser UI presets use this approach. Each image builder pre-populates a VFS with runtime files, directory structure, configs, and symlinks, then saves it as a `.vfs.zst` file (zstd-compressed; `saveImage()` compresses on write). At runtime, the UI fetches the file and `MemoryFileSystem.fromImage` decompresses transparently - restoring the image replaces thousands of individual file writes with a single buffer copy. The empty regions of the SharedFS allocator compress to almost nothing, so a 32 MB filesystem with a few MB of real content typically ships as a 1-3 MB download.

There are two consumption patterns for VFS images, depending on whether the demo wants the kernel worker to fully own the filesystem:
Expand Down
8 changes: 5 additions & 3 deletions docs/binary-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,11 @@ For each declared arch in the package's `arches = [...]` (default
- `abi_versions` must contain the in-tree `ABI_VERSION`.
- `cache_key_sha` must match the resolver's locally-computed
cache-key sha (catches recipe drift).
6. Places `binaries/programs/<arch>/<output>.wasm` symlinks pointing
into the cache, so browser/Node demos can load by relative path
without re-fetching.
6. Places each program output under `binaries/programs/<arch>/` using the
manifest's output layout, and places declared non-Wasm runtime files under
`binaries/programs/<arch>/<package>/<artifact>`. Both are symlinks into the
validated cache, so browser/Node image builders load the same bytes without
re-fetching. Local builds use the identical layout under `local-binaries/`.

On any verification failure, the resolver logs a warning and falls
through to a source build (the package's build script). This
Expand Down
20 changes: 17 additions & 3 deletions docs/fork-instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,23 @@ function pointers passed through main-module memory or the shared table cannot
currently be attributed to their originating module; using such a pointer to
create a side A -> side B -> fork path is unsupported and is not yet guaranteed
to fail before control-flow corruption. A future module-activation protocol is
required to close that residual. Fork from a pthread into a dlopened side
module is also unsupported; pthread workers do not install the side-module
coordinator.
required to close that residual.

Pthread workers do not own the process worker's side-module instances, table,
or exception-tag identities. `dlopen()` from a pthread consequently returns
NULL with a precise `dlerror()`. Once the process main worker has published a
dlopen archive entry, `fork()` from a pthread returns `ENOTSUP` without
creating a child. A host-private atomic lock prevents main-worker dlopen from
racing the pthread's archive check and is held through unwind, SYS_FORK/memory
copy, and parent rewind; the child clears its copied lock before replay. Fork
from a pthread remains supported while that process-wide archive is empty.

For TLS-bearing side modules, each archive entry also preserves the live
positive `__tls_base`. Replay restores only that mutable global using the
process pointer type. It does not call `__wasm_init_tls`: the child memory copy
already contains live TLS, and reinitialization would overwrite C++ landing-pad
and application `thread_local` state. TLS-relative exports relocate from that
base, while `__tls_size` and `__tls_align` remain scalar constants.

Every participating module still uses the fixed 16 KiB save-buffer limit
described below. A dynamically allocated side buffer avoids overlap with the
Expand Down
51 changes: 47 additions & 4 deletions docs/package-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ The split is load-bearing post the
Required fields:

```toml
name = "zlib" # logical library name
version = "1.3.1" # upstream version
name = "zlib" # logical library name; one safe path component
version = "1.3.1" # upstream version; one safe path component
depends_on = [] # ["zlib@1.3.1", ...] — exact versions, no ranges

[source]
Expand All @@ -176,8 +176,51 @@ script_path = "packages/registry/zlib/build-zlib.sh"
libs = ["lib/libz.a"] # must exist post-build
headers = ["include/zlib.h", "include/zconf.h"]
pkgconfig = ["lib/pkgconfig/zlib.pc"]
files = ["share/runtime-data.bin"] # other runtime data
```

Program packages use `[[outputs]]` for executable/side-module artifacts. A
non-Wasm file required at runtime is declared separately so it remains part of
the same reproducible archive and cache key:

```toml
[[outputs]]
name = "php"
wasm = "php.wasm"

[[runtime_files]]
artifact = "icu.dat" # relative to the package cache/archive
guest_path = "/usr/lib/php/icu.dat" # installation path in a VFS image
mode = 420 # optional decimal TOML; default 0644
```

`[[runtime_files]]` is program-only. Artifact paths are normalized portable
relative paths; guest paths are normalized absolute POSIX paths; files,
ancestor paths, and resolver-mirror destinations may not collide. The resolver
requires regular non-symlink runtime files after fresh builds, cache hits, and
remote fetches. It mirrors them at
`{local-,}binaries/programs/<arch>/<package>/<artifact>` independently of the
number of `[[outputs]]` entries. Missing fetched files make the archive stale
and trigger source fallback (or a hard failure in fetch-only mode).

Repo-side VFS/test builders query the authoritative path and mode with
`xtask build-deps runtime-file-metadata <package> <artifact>`; they must not
scan library caches or invent environment-only guest paths. Published VFS
images contain the installed bytes already, so this query is a build-tool
contract rather than a runtime host API. The structured metadata also lists
the package's complete mirror closure (every `[[outputs]]` artifact plus every
`[[runtime_files]]` file). Repo-side consumers resolve that set from one
complete provenance root: a partial local override may fall back wholesale to
a complete fetched package, but local, fetched, and installed-package tiers
are never combined. If artifacts exist but no tier has the complete accepted
closure, resolution fails loudly.

Top-level keys are closed-schema: misspellings such as `[[runtime_file]]`
(singular) are rejected instead of silently dropping a runtime dependency.
Package names, versions, dependency names, and exact dependency-version tokens
must each be safe single filesystem components; `/`, `\`, NUL, `.` and `..`
spellings are rejected before cache, archive, or registry path construction.

`package.toml` **must NOT** carry `revision`, `[binary]`,
`[build].repo_url`, or `[build].commit`. Those moved to `build.toml`
during the binary-resolution-via-index-ledger migration;
Expand Down Expand Up @@ -353,7 +396,7 @@ that doesn't respect them cannot be cached safely.

| Variable | Meaning |
|---|---|
| `WASM_POSIX_DEP_OUT_DIR` | Temp dir the script must install into. Layout matches `outputs.libs` / `outputs.headers` / `outputs.pkgconfig` relative paths. |
| `WASM_POSIX_DEP_OUT_DIR` | Temp dir the script must install into. Layout matches `outputs.libs` / `outputs.headers` / `outputs.pkgconfig` / `outputs.files` relative paths. |
| `WASM_POSIX_DEP_NAME` | `name` from package.toml. |
| `WASM_POSIX_DEP_VERSION` | `version` from package.toml. |
| `WASM_POSIX_DEP_REVISION` | Effective package revision after `build.toml` is overlaid. |
Expand All @@ -371,7 +414,7 @@ from source; it does not restrict normal SDK users compiling against a
published sysroot/libc++ artifact.

After the script exits 0, the resolver verifies every path in
`outputs.{libs,headers,pkgconfig}` exists under `$WASM_POSIX_DEP_OUT_DIR`.
`outputs.{libs,headers,pkgconfig,files}` exists under `$WASM_POSIX_DEP_OUT_DIR`.
A missing output fails the build (and the temp dir is cleaned up,
so a retry starts clean).

Expand Down
10 changes: 8 additions & 2 deletions docs/posix-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,10 @@ These PHP needs are well-handled by the current kernel:
- Memory: anonymous mmap, munmap, brk
- Multi-process: fork (kernel syscall), exec (host-initiated), waitpid (kernel syscall)
- Networking: AF_INET TCP (connect, bind, listen, accept, send, recv), getaddrinfo
- Dynamic linking: dlopen, dlsym, dlclose, dlerror (Wasm dylink)
- Dynamic linking: dlopen, dlsym, dlclose, dlerror (Wasm dylink on the process
worker). Pthread workers cannot share the process's Wasm table/tag graph, so
pthread `dlopen` fails and pthread `fork` after a process dlopen returns
`ENOTSUP`.
- POSIX timers: partial `SIGEV_SIGNAL`/`SIGEV_NONE` timer_create, timer_settime,
timer_gettime, timer_delete. The PHP package imports a cooperative Wasm host
hook because native `SIGEV_THREAD_ID` delivery is unavailable; that package
Expand All @@ -639,7 +642,10 @@ These require features fundamentally unavailable in the Wasm architecture:

- **Wasm FP exceptions (110 math tests):** WebAssembly has no floating-point exception flags (`fenv.h`). All `fe*` math tests fail. `long double` variants pass because they use software fp128.
- **No pthread_cancel:** Wasm has no async cancellation mechanism or cancel-point assembly. `pthread_create` works; `pthread_cancel` does not.
- **No dlopen/TLS:** `tls_get_new-dtv_dso` requires loading a shared library with TLS at runtime.
- **No musl DTV expansion for arbitrary DSOs:** `tls_get_new-dtv_dso` requires
native-style per-thread dynamic TLS-vector growth. Kandelo can replay the
fixed TLS reservation of a Wasm side module across process `fork`, but that
does not implement musl's general pthread DTV contract.

### Linker Requirements for Signal Handlers

Expand Down
Loading
Loading