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
1 change: 1 addition & 0 deletions apps/browser-demos/pages/test-runner/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<title>POSIX Test Runner</title>
<link rel="icon" href="data:," />
</head>
<body>
<div id="status">Loading kernel...</div>
Expand Down
74 changes: 74 additions & 0 deletions apps/browser-demos/test/fixtures/opfs-seek-client-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { OpfsFileSystem } from "../../../../host/src/vfs/opfs";

const O_RDWR = 0x0002;
const O_CREAT = 0x0040;
const O_TRUNC = 0x0200;
const SEEK_SET = 0;
const SEEK_CUR = 1;

function errorName(action: () => unknown): string | null {
try {
action();
return null;
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
}

self.onmessage = (
event: MessageEvent<{ buffer: SharedArrayBuffer; path: string }>,
) => {
const { buffer, path } = event.data;
const fs = OpfsFileSystem.create(buffer);
let fd = -1;

try {
fd = fs.open(path, O_CREAT | O_TRUNC | O_RDWR, 0o600);
const data = new TextEncoder().encode("abcdef");
if (fs.write(fd, data, null, data.length) !== data.length) {
throw new Error("short OPFS fixture write");
}

fs.seek(fd, 2, SEEK_SET);
const negativeError = errorName(() => fs.seek(fd, -3, SEEK_CUR));
const afterNegative = fs.seek(fd, 0, SEEK_CUR);

const wideOffset = 2 ** 32 + 1;
const wideResult = fs.seek(fd, wideOffset, SEEK_SET);

fs.seek(fd, Number.MAX_SAFE_INTEGER, SEEK_SET);
const overflowError = errorName(() => fs.seek(fd, 1, SEEK_CUR));
const afterOverflow = fs.seek(fd, 0, SEEK_CUR);

fs.close(fd);
fd = -1;
fs.unlink(path);
self.postMessage({
type: "result",
negativeError,
afterNegative,
wideResult,
overflowError,
afterOverflow,
});
} catch (error) {
if (fd >= 0) {
try {
fs.close(fd);
} catch {
// Preserve the original failure.
}
}
try {
fs.unlink(path);
} catch {
// Preserve the original failure.
}
self.postMessage({
type: "error",
error: error instanceof Error ? error.message : String(error),
});
} finally {
self.close();
}
};
106 changes: 106 additions & 0 deletions apps/browser-demos/test/opfs-seek.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { expect, test } from "@playwright/test";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const proxyWorkerPath = resolve(
__dirname,
"../../../host/src/vfs/opfs-worker.ts",
);
const clientWorkerPath = resolve(
__dirname,
"fixtures/opfs-seek-client-worker.ts",
);

test("OPFS preserves signed 64-bit seek results and failed offsets", async ({
page,
baseURL,
browserName,
}) => {
test.skip(
browserName !== "chromium",
"OPFS sync access handles are Chromium-only here",
);
expect(baseURL).toBeTruthy();

const proxyWorkerUrl = new URL(`/@fs/${proxyWorkerPath}`, baseURL).href;
const clientWorkerUrl = new URL(`/@fs/${clientWorkerPath}`, baseURL).href;
await page.goto(new URL("/trap-signal-test.html", baseURL).href);

const result = await page.evaluate(
async ({ proxyWorkerUrl, clientWorkerUrl }) => {
const buffer = new SharedArrayBuffer(4 * 1024 * 1024);
const proxy = new Worker(proxyWorkerUrl, { type: "module" });
const client = new Worker(clientWorkerUrl, { type: "module" });

const receive = <T>(worker: Worker, expectedType: string): Promise<T> =>
new Promise((resolvePromise, reject) => {
const timeout = setTimeout(
() => reject(new Error(`timed out waiting for ${expectedType}`)),
15_000,
);
worker.addEventListener(
"message",
(event) => {
if (event.data?.type !== expectedType) {
if (event.data?.type === "error") {
clearTimeout(timeout);
reject(new Error(event.data.error));
}
return;
}
clearTimeout(timeout);
resolvePromise(event.data as T);
},
{ once: false },
);
worker.addEventListener(
"error",
(event) => {
clearTimeout(timeout);
reject(
new Error(
`${expectedType}: ${event.message || "worker module failed to load"} ` +
`(${event.filename}:${event.lineno}:${event.colno})`,
),
);
},
{ once: true },
);
});

try {
const ready = receive<{ type: "ready" }>(proxy, "ready");
proxy.postMessage({ type: "init", buffer });
await ready;

const pending = receive<{
type: "result";
negativeError: string | null;
afterNegative: number;
wideResult: number;
overflowError: string | null;
afterOverflow: number;
}>(client, "result");
client.postMessage({
buffer,
path: `/kandelo-opfs-seek-${crypto.randomUUID()}`,
});
return await pending;
} finally {
client.terminate();
proxy.terminate();
}
},
{ proxyWorkerUrl, clientWorkerUrl },
);

expect(result).toEqual({
type: "result",
negativeError: "EINVAL",
afterNegative: 2,
wideResult: 2 ** 32 + 1,
overflowError: "EOVERFLOW",
afterOverflow: Number.MAX_SAFE_INTEGER,
});
});
126 changes: 121 additions & 5 deletions crates/kernel/src/syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2933,6 +2933,9 @@ pub fn sys_lseek(
// directory position. The cookie is the d_off value from getdents64.
if ofd.file_type == FileType::Directory {
if whence == SEEK_SET {
if offset < 0 {
return Err(Errno::EINVAL);
}
// Close the existing dir handle if open
if ofd.dir_host_handle >= 0 {
let _ = host.host_closedir(ofd.dir_host_handle);
Expand Down Expand Up @@ -2987,8 +2990,10 @@ pub fn sys_lseek(
let cur = ofd.offset;
let new_off = match whence {
SEEK_SET => offset,
SEEK_CUR => cur + offset,
SEEK_END => FB_SMEM_LEN as i64 + offset,
SEEK_CUR => cur.checked_add(offset).ok_or(Errno::EOVERFLOW)?,
SEEK_END => (FB_SMEM_LEN as i64)
.checked_add(offset)
.ok_or(Errno::EOVERFLOW)?,
_ => return Err(Errno::EINVAL),
};
if new_off < 0 {
Expand All @@ -3006,6 +3011,9 @@ pub fn sys_lseek(
|| ofd.host_handle == crate::devfs::DEVFS_DIR_HANDLE
{
if whence == SEEK_SET {
if offset < 0 {
return Err(Errno::EINVAL);
}
ofd.dir_synth_state = 0;
ofd.dir_entry_offset = 0;
ofd.offset = offset;
Expand Down Expand Up @@ -3042,7 +3050,7 @@ pub fn sys_lseek(
let size = synthetic_file_content(&ofd.path).map_or(0, |d| d.len() as i64);
let new_pos = match whence {
SEEK_SET => offset,
SEEK_CUR => ofd.offset + offset,
SEEK_CUR => ofd.offset.checked_add(offset).ok_or(Errno::EOVERFLOW)?,
SEEK_END => size.checked_add(offset).ok_or(Errno::EOVERFLOW)?,
_ => return Err(Errno::EINVAL),
};
Expand All @@ -3067,7 +3075,7 @@ pub fn sys_lseek(
let new_pos = match whence {
SEEK_SET => offset,
SEEK_CUR => current.checked_add(offset).ok_or(Errno::EOVERFLOW)?,
SEEK_END => size + offset,
SEEK_END => size.checked_add(offset).ok_or(Errno::EOVERFLOW)?,
_ => return Err(Errno::EINVAL),
};
if new_pos < 0 {
Expand All @@ -3079,11 +3087,17 @@ pub fn sys_lseek(

let new_offset = match whence {
SEEK_SET => {
if offset < 0 {
return Err(Errno::EINVAL);
}
host.host_seek(ofd.host_handle, offset, whence)?;
offset
}
SEEK_CUR => {
let pos = ofd.offset + offset;
let pos = ofd.offset.checked_add(offset).ok_or(Errno::EOVERFLOW)?;
if pos < 0 {
return Err(Errno::EINVAL);
}
host.host_seek(ofd.host_handle, pos, SEEK_SET)?;
pos
}
Expand Down Expand Up @@ -13660,6 +13674,108 @@ mod tests {
assert_eq!(pos, 90);
}

#[test]
fn lseek_errors_preserve_kernel_owned_offsets() {
fn install_fd(
proc: &mut Process,
file_type: FileType,
host_handle: i64,
path: &[u8],
) -> (i32, usize) {
let ofd_idx = proc
.ofd_table
.create(file_type, O_RDWR, host_handle, path.to_vec());
let fd = proc
.fd_table
.alloc(crate::fd::OpenFileDescRef(ofd_idx), 0)
.unwrap();
(fd, ofd_idx)
}

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

let (dir_fd, dir_ofd) = install_fd(&mut proc, FileType::Directory, 71, b"/dir");
{
let ofd = proc.ofd_table.get_mut(dir_ofd).unwrap();
ofd.offset = 4;
ofd.dir_synth_state = 2;
ofd.dir_entry_offset = 4;
}
assert_eq!(
sys_lseek(&mut proc, &mut host, dir_fd, -1, SEEK_SET),
Err(Errno::EINVAL)
);
let ofd = proc.ofd_table.get(dir_ofd).unwrap();
assert_eq!((ofd.offset, ofd.dir_synth_state, ofd.dir_entry_offset), (4, 2, 4));

let (proc_fd, proc_ofd) = install_fd(
&mut proc,
FileType::Regular,
crate::procfs::PROCFS_DIR_HANDLE,
b"/proc",
);
proc.ofd_table.get_mut(proc_ofd).unwrap().offset = 3;
assert_eq!(
sys_lseek(&mut proc, &mut host, proc_fd, -1, SEEK_SET),
Err(Errno::EINVAL)
);
assert_eq!(proc.ofd_table.get(proc_ofd).unwrap().offset, 3);

let (fb_fd, fb_ofd) = install_fd(
&mut proc,
FileType::CharDevice,
VirtualDevice::Fb0.host_handle(),
b"/dev/fb0",
);
assert_eq!(
sys_lseek(&mut proc, &mut host, fb_fd, 7, SEEK_SET),
Ok(7)
);
assert_eq!(
sys_lseek(&mut proc, &mut host, fb_fd, i64::MAX, SEEK_CUR),
Err(Errno::EOVERFLOW)
);
assert_eq!(proc.ofd_table.get(fb_ofd).unwrap().offset, 7);

let (synthetic_fd, synthetic_ofd) = install_fd(
&mut proc,
FileType::Regular,
SYNTHETIC_FILE_HANDLE,
b"/etc/mtab",
);
assert_eq!(
sys_lseek(&mut proc, &mut host, synthetic_fd, 2, SEEK_SET),
Ok(2)
);
assert_eq!(
sys_lseek(
&mut proc,
&mut host,
synthetic_fd,
i64::MAX,
SEEK_CUR,
),
Err(Errno::EOVERFLOW)
);
assert_eq!(proc.ofd_table.get(synthetic_ofd).unwrap().offset, 2);

let memfd = sys_memfd_create(&mut proc, b"seek-overflow", 0).unwrap();
sys_write(&mut proc, &mut host, memfd, b"abcdef").unwrap();
assert_eq!(
sys_lseek(&mut proc, &mut host, memfd, 2, SEEK_SET),
Ok(2)
);
assert_eq!(
sys_lseek(&mut proc, &mut host, memfd, i64::MAX, SEEK_END),
Err(Errno::EOVERFLOW)
);
assert_eq!(
sys_lseek(&mut proc, &mut host, memfd, 0, SEEK_CUR),
Ok(2)
);
}

#[test]
fn test_lseek_pipe_fails() {
let mut proc = Process::new(1);
Expand Down
4 changes: 4 additions & 0 deletions docs/abi-versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ kernel. Specifically, any of the following requires an `ABI_VERSION` bump:
fork-using user program. The kernel does not read these exports
directly, but the host runtime in `host/src/worker-main.ts` does —
a rename here silently breaks fork for every already-built binary.
- Changing the linked musl/glue syscall function types or argument-slot widths,
including the wasm32 cancellation-point `__syscall_cp` path. These are not
currently visible in the structural snapshot, but stale objects and archives
can otherwise link with incompatible Wasm function signatures.
- Changing the name, version, encoding, or role semantics of the
`kandelo.wpk_fork.capabilities` custom section. The host uses these claims to
decide whether a main/side-module pair can safely coordinate fork replay.
Expand Down
Loading