Skip to content

VFS: Verify lazy downloads before running programs#1024

Draft
brandonpayton wants to merge 1 commit into
mainfrom
vfs/lazy-artifact-integrity-current
Draft

VFS: Verify lazy downloads before running programs#1024
brandonpayton wants to merge 1 commit into
mainfrom
vfs/lazy-artifact-integrity-current

Conversation

@brandonpayton

Copy link
Copy Markdown
Member

Why

Kandelo is moving toward a main shell that can fetch Homebrew-provided Perl,
Python, and Erlang closures only when they are first used. A lazy executable
must not turn an HTTP 200 response, a truncated file, or a changed ZIP into
guest-visible program bytes.

The old lazy path checked neither the complete response length nor archive
identity. Node also returned lazy executable bytes directly instead of
materializing the kernel-owned VFS, so Node and browser state diverged.

What changes

  • Treat each standalone lazy size as an exact decoded-body byte count, capped
    at 1 GiB, and leave its stub retryable after a short, long, or failed fetch.
  • Let new lazy ZIP declarations bind the exact decoded ZIP byte count and
    lowercase SHA-256. Verify both before parsing or writing members.
  • Validate every declared ZIP member and expanded size before replacing any
    archive stub, bound deflate expansion, and share concurrent materialization
    work per inode or archive.
  • Materialize successful Node lazy executables and closures into the same VFS
    that browser exec uses.
  • Validate serialized lazy metadata and image section boundaries while safely
    ignoring live metadata made stale by a concurrent guest unlink or write.
  • Bind the existing Vim and NetHack shell archives to their build-time digests,
    require exact sizes in mkrootfs manifests, and remove benchmark size guesses.

Relationship to the Homebrew work

This is the consumer-side complement to draft #1023. That PR publishes a
deterministic browser-fetchable Homebrew closure and descriptor; this PR gives
the runtime a bounded, digest-checked way to consume such a closure. The two
PRs do not edit the same files.

A follow-up can wire the #1023 descriptor into the main shell for lazy Perl,
Python, and Erlang. This PR does not add those runtime entries by itself.

This is a clean successor to stale #897, rebuilt on current main rather than a
merge of its old ABI-era branch.

Compatibility and ABI

The archive byte-count and digest fields are optional when reading existing VFS
images, so older images remain loadable. Current builders provide both fields
for newly produced remote ZIPs.

No kernel ABI shape or syscall behavior changes. ABI version 41 and the
committed snapshot remain unchanged.

Validation

  • Focused VFS, image, archive, and real Node-kernel integration: 103 tests
    passed.
  • Browser lazy-file and lazy-ZIP retry tests: 6 passed across Chromium,
    Firefox, and WebKit.
  • mkrootfs typecheck and suite: 179 tests passed.
  • Host declaration build: passed.
  • Shell VFS build module load through the repository's tsx tool: passed.
  • ABI snapshot/header/TypeScript binding check: passed.
  • git diff whitespace check: passed.

The broader default host run reached 1,227 passing tests. Its remaining 34
failures were fixture setup failures because this isolated worktree does not
contain sysroot64, rootfs.vfs, or the full generated program-binary set; no
touched lazy/VFS test remained failing.

@github-actions

Copy link
Copy Markdown

Phase B-1 matrix build status — pr-1024-staging

ABI v41. 6 built, 0 failed, 6 total.

Package Arch Status Sha
kandelo-sdk wasm32 built b0e75273
rootfs wasm32 built 93cf7c62
shell wasm32 built a73d0e0c
lamp wasm32 built 089ad416
node-vfs wasm32 built bf36f02d
wordpress wasm32 built 3e02f290

Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.

brandonpayton added a commit that referenced this pull request Jul 21, 2026
…1037)

## Why

The shell image records lazy ZIP metadata for Vim and NetHack. It
previously
rebuilt those ZIPs even though Kandelo already publishes
`vim-browser-bundle` and `nethack-browser-bundle` outputs. Separate ZIP
builds
can contain the same files but still produce different bytes because
path
order, timestamps, permissions, environment options, or ZIP metadata
differ.

That is what the new lazy-archive integrity check in #1024 exposed: the
shell
recorded one Vim digest while Chromium fetched a different,
independently
generated Vim ZIP. The integrity check correctly rejected the mismatch.

## What changes

### Use one declared producer

- Make `shell` depend on the Vim and NetHack browser-bundle packages
instead
  of the underlying program packages.
- Resolve the exact declared `programs/wasm32/vim.zip` and `nethack.zip`
outputs. The shell no longer rebuilds either archive or falls back to a
  public-directory copy.
- Read each output once, validate its central directory and required
executable, and derive its byte length and SHA-256 from those exact
bytes.

### Make independent bundle builds byte-reproducible

- Add one shared deterministic ZIP helper used by both bundle builders.
- Build in a private mirror so source trees are not modified.
- Sort entry paths bytewise and normalize file modes, link modes,
timestamps,
  compression level, locale, timezone, and host-specific ZIP fields.
- Preserve package-owned symlinks and validate their targets before VFS
  registration.
- Declare every byte-producing helper as a bundle cache-key input.
- Keep the in-flight Vim bundle at revision 3, bump NetHack from
revision 3 to
  4, and keep the shell bump from revision 13 to 14.

### Protect the lazy-archive mount boundary

- Preflight every member before creating directories, stubs, symlinks,
inode
  mappings, or group metadata.
- Reject absolute, traversal, backslash, non-canonical, duplicate,
colliding,
  and non-directory-ancestor member paths.
- Validate mount prefixes and required symlink targets in the same
preflight.

Missing, corrupt, wrong, non-reproducible, or path-escaping bundle
outputs now
fail at the producer/consumer contract instead of being hidden by a
lookalike
rebuild or partially mutating the VFS. This is a packaging, host-VFS,
and VFS
build change; it does not change the kernel or weaken #1024.

## Validation

- Focused bundle coverage: 10 tests passed for both bundle specs, exact
bytes/digests, deterministic ZIP bytes, canonical entry order,
permissions,
  local and central timestamps/extra fields, DEFLATE bytes, symlink
preservation and VFS registration, resolver calls, fail-loudly stat and
newline boundaries, missing/corrupt/wrong outputs, no fallback, and
package
  declarations.
- Lazy-archive API coverage: 37 tests passed, including regular-file and
  symlink traversal proofs plus absolute, backslash, empty-component,
  duplicate, and non-directory-ancestor rejection before any VFS/group
  mutation.
- The deterministic fixture produces canonical SHA-256
  `67b198037eb2b47e6d5acadbdfc94f25ec83c469c5374d719342392b08ef0bd0`
from source trees with different creation order, mtimes, permission
bits,
timezone, `SOURCE_DATE_EPOCH`, and `ZIPOPT`. It passed with both the
Darwin
and GNU command sets in the repository dev shell and asserts that the
large
  fixture member uses DEFLATE.
- Strict TypeScript compilation passed for the host VFS, lazy-archive
helper,
  shell builder, and both focused test files.
- `bash -n` passed for the shared ZIP helper and both bundle builders.
- Package parsing and cache-key calculation passed in the dev shell:
- `vim-browser-bundle`:
`cf5f9141bfc5047f7548121cafb993708a8da25b2639967cc603e4d0347a4347`
- `nethack-browser-bundle`:
`c50cb85a33c8e4dd8157998a3c7fde468d33193054b7f0ce6cef111ccb26b011`
- `shell`:
`802b871c980a8d8e44b28ab25a0a6947e6b350f93e2b4825ad4345553f5ede7d`
- Declared output-path checks resolve `vim.zip` and `nethack.zip`.
- `git diff --check` passed.

The full repository test path is left to CI because a fresh isolated
checkout
does not have the prebuilt sysroot needed by the unrelated global
C-fixture
setup.

## #1024 handoff

Current main does not yet accept integrity metadata in
`registerLazyArchiveFromEntries`, so this PR returns the exact computed
integrity without pretending an ignored argument is enforced. After this
producer fix lands, #1024 must rebase onto it, pass `archive.integrity`
into
the real integrity-aware registration API, and rerun the exact Chromium
proof.
Both halves are required.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant