Skip to content
Closed
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
113 changes: 3 additions & 110 deletions apps/browser-demos/lib/kernel-owned-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
// left is the small, transient per-boot image-build FS; these helpers track it
// and nudge WebKit's collector to reclaim it between boots.
import { MemoryFileSystem } from "@host/vfs/memory-fs";
import { overlayEtcFromRootfs } from "@host/vfs/rootfs-overlay";
// @ts-expect-error — vite ?url virtual module (resolved by the kernel-artifacts plugin)
import rootfsVfsUrl from "@rootfs-vfs?url";

export { overlayEtcFromRootfs };

const WEBKIT_RECLAIM_TIMEOUT_MS = 1_500;
const WEBKIT_RECLAIM_STEP_MS = 150;
const WEBKIT_RECLAIM_PRESSURE_BYTES = 32 * 1024 * 1024;
Expand Down Expand Up @@ -91,116 +94,6 @@ export function createEmptyBuildFs(maxByteLength = 64 * 1024 * 1024): MemoryFile
return MemoryFileSystem.create(sab, maxByteLength);
}

const S_IFMT = 0xf000;
const S_IFREG = 0x8000;
const S_IFDIR = 0x4000;
const S_IFLNK = 0xa000;

function copyMissingRootfsPath(
source: MemoryFileSystem,
target: MemoryFileSystem,
path: string,
): void {
const sourceStat = source.lstat(path);
const sourceKind = sourceStat.mode & S_IFMT;
let targetKind: number | null = null;
try {
targetKind = target.lstat(path).mode & S_IFMT;
} catch {
// Missing in the caller's image: copy it from the canonical rootfs.
}

// Existing caller-owned leaves always win. Existing directories merge with
// canonical directories so a demo can override one file without losing the
// rest of that subtree.
if (targetKind !== null && (sourceKind !== S_IFDIR || targetKind !== S_IFDIR)) {
return;
}

if (sourceKind === S_IFDIR) {
if (targetKind === null) {
target.mkdirWithOwner(
path,
sourceStat.mode & 0o7777,
sourceStat.uid,
sourceStat.gid,
);
}
const dh = source.opendir(path);
try {
for (;;) {
const entry = source.readdir(dh);
if (entry === null) break;
if (entry.name === "." || entry.name === "..") continue;
copyMissingRootfsPath(
source,
target,
path === "/" ? `/${entry.name}` : `${path}/${entry.name}`,
);
}
} finally {
source.closedir(dh);
}
return;
}

if (sourceKind === S_IFLNK) {
target.symlinkWithOwner(
source.readlink(path),
path,
sourceStat.uid,
sourceStat.gid,
);
return;
}

if (sourceKind !== S_IFREG) {
throw new Error(`Unsupported canonical /etc file type at ${path}`);
}

const bytes = new Uint8Array(sourceStat.size);
const fd = source.open(path, 0, 0);
let offset = 0;
try {
while (offset < bytes.length) {
const count = source.read(
fd,
bytes.subarray(offset),
null,
bytes.length - offset,
);
if (count <= 0) {
throw new Error(
`Short read while copying canonical rootfs path ${path}: ` +
`${offset}/${bytes.length} bytes`,
);
}
offset += count;
}
} finally {
source.close(fd);
}
target.createFileWithOwner(
path,
sourceStat.mode & 0o7777,
sourceStat.uid,
sourceStat.gid,
bytes,
);
}

/**
* Recursively merge canonical `/etc` state into `target`, without overwriting
* files or symlinks the caller already wrote. This replaces the legacy
* worker-side `/etc` overlay that `kernel.init()` performed: demos that start
* from a small custom image still inherit rootfs-owned account, resolver, TLS,
* and other configuration through the normal VFS path.
*/
export function overlayEtcFromRootfs(target: MemoryFileSystem, rootfsImage: Uint8Array): void {
const source = MemoryFileSystem.fromImage(rootfsImage);
copyMissingRootfsPath(source, target, "/etc");
}

/**
* Convenience: an empty build FS pre-seeded with `/etc` from the canonical
* rootfs — the kernel-owned equivalent of the legacy empty-FS + init()-overlay
Expand Down
11 changes: 10 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,16 @@ VFS images can also carry image-level metadata outside the guest file tree. The

`BrowserKernel.boot({ vfsImage, ... })` is the kernel-owned VFS path. The worker restores the supplied image (per-demo `.vfs.zst`, typically built on top of the canonical rootfs as a base layer) into a `MemoryFileSystem`, applies `DEFAULT_MOUNT_SPEC` via `resolveForBrowser` (the image becomes the `/` mount; the seven scratch mounts come up empty), and layers `/dev/shm` + `/dev` on top. Browser networking then replaces `/etc/ssl/certs/ca-certificates.crt` with its generated per-session MITM root; the image-owned OpenSSL configuration and compiled-in `/etc/ssl/cert.pem` trust path remain unchanged.

The legacy `kernel.spawn(programBytes, argv, { fsSab })` path is still supported for demos that own a single `MemoryFileSystem` SAB at `/` (used by `benchmark`, `erlang`, `shell`). To keep NSS and other static system policy available on that path, the browser kernel worker recursively merges `/etc/**` from `rootfs.vfs` into the demo SAB at boot (`overlayEtcFromRootfs` in `host/src/vfs/rootfs-overlay.ts`). Existing leaf files and symlinks remain demo-owned, while existing directories are traversed so missing canonical descendants such as `/etc/ssl/openssl.cnf` are still installed. This is a temporary bridge until those demos move to the `vfsImage` boot path.
The browser test runner and Git test assemble small kernel-owned VFS images with
`createBuildFsWithEtc` in `apps/browser-demos/lib/kernel-owned-boot.ts`, then
serialize them with `finalizeKernelOwnedImage` and boot them through
`BrowserKernel.boot`. Before serialization, the shared host helper
`overlayEtcFromRootfs` in `host/src/vfs/rootfs-overlay.ts` recursively merges
`/etc/**` from the canonical `rootfs.vfs`. Existing leaves and directory
metadata remain caller-owned, while missing canonical descendants such as
`/etc/ssl/openssl.cnf` retain their source modes and ownership. Missing
canonical `/etc` state, short reads, and target capacity failures abort image
assembly instead of producing an incomplete filesystem.

### Lazy Files

Expand Down
1 change: 1 addition & 0 deletions host/src/vfs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export {
} from "./default-mounts";
export type { MountSpec, BrowserResolverOptions } from "./default-mounts";
export { resolveForNode } from "./default-mounts-node";
export { overlayEtcFromRootfs } from "./rootfs-overlay";
171 changes: 171 additions & 0 deletions host/src/vfs/rootfs-overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { MemoryFileSystem } from "./memory-fs";
import {
ENOENT,
ENOSPC,
O_CREAT,
O_RDONLY,
O_TRUNC,
O_WRONLY,
S_IFDIR,
S_IFLNK,
S_IFMT,
S_IFREG,
SFSError,
} from "./sharedfs-vendor";

function lstatIfPresent(fs: MemoryFileSystem, path: string) {
try {
return fs.lstat(path);
} catch (error) {
if (error instanceof SFSError && error.code === ENOENT) return null;
throw error;
}
}

function readFile(
fs: MemoryFileSystem,
path: string,
size: number,
): Uint8Array {
const bytes = new Uint8Array(size);
const fd = fs.open(path, O_RDONLY, 0);
let offset = 0;
try {
while (offset < bytes.length) {
const count = fs.read(
fd,
bytes.subarray(offset),
null,
bytes.length - offset,
);
if (count <= 0) break;
offset += count;
}
} finally {
fs.close(fd);
}

if (offset !== bytes.length) {
throw new Error(
`Short read while copying canonical rootfs path ${path}: ` +
`${offset}/${bytes.length} bytes`,
);
}
return bytes;
}

function writeFile(
fs: MemoryFileSystem,
path: string,
bytes: Uint8Array,
mode: number,
uid: number,
gid: number,
): void {
const fd = fs.open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
let offset = 0;
try {
while (offset < bytes.length) {
const count = fs.write(
fd,
bytes.subarray(offset),
null,
bytes.length - offset,
);
if (count <= 0) {
throw new SFSError(
ENOSPC,
`No space left on device while copying canonical rootfs path ${path}: ` +
`${offset}/${bytes.length} bytes`,
);
}
offset += count;
}
} finally {
fs.close(fd);
}
fs.chown(path, uid, gid);
fs.chmod(path, mode);
}

/**
* Merge one canonical rootfs path into a caller-owned filesystem without
* overwriting an existing leaf. Existing directories are traversed so missing
* canonical descendants can still be added below caller-owned directory trees.
*/
function copyMissingRootfsPath(
source: MemoryFileSystem,
target: MemoryFileSystem,
path: string,
): void {
const sourceStat = source.lstat(path);
const sourceKind = sourceStat.mode & S_IFMT;
const targetStat = lstatIfPresent(target, path);

if (sourceKind === S_IFDIR) {
if (targetStat) {
if ((targetStat.mode & S_IFMT) !== S_IFDIR) return;
} else {
target.mkdirWithOwner(
path,
sourceStat.mode & 0o7777,
sourceStat.uid,
sourceStat.gid,
);
}

const dh = source.opendir(path);
try {
for (;;) {
const entry = source.readdir(dh);
if (entry === null) break;
if (entry.name === "." || entry.name === "..") continue;
const child = path === "/" ? `/${entry.name}` : `${path}/${entry.name}`;
copyMissingRootfsPath(source, target, child);
}
} finally {
source.closedir(dh);
}
return;
}

// A caller-owned file or symlink is authoritative for that exact leaf.
if (targetStat) return;

if (sourceKind === S_IFLNK) {
target.symlinkWithOwner(
source.readlink(path),
path,
sourceStat.uid,
sourceStat.gid,
);
return;
}

if (sourceKind !== S_IFREG) {
throw new Error(`Unsupported canonical /etc file type at ${path}`);
}

writeFile(
target,
path,
readFile(source, path, sourceStat.size),
sourceStat.mode & 0o7777,
sourceStat.uid,
sourceStat.gid,
);
}

/**
* Recursively merge canonical `/etc` image state into an image under
* construction. Existing leaves and directory metadata remain caller-owned;
* missing canonical directories, regular files, and symlinks retain their
* source ownership and modes.
*/
export function overlayEtcFromRootfs(
target: MemoryFileSystem,
rootfsImage: Uint8Array,
): void {
const source = MemoryFileSystem.fromImage(rootfsImage);
copyMissingRootfsPath(source, target, "/etc");
}
74 changes: 0 additions & 74 deletions host/test/kernel-owned-boot.test.ts

This file was deleted.

Loading