296 lines
10 KiB
JavaScript
296 lines
10 KiB
JavaScript
/**
|
|
* Optional WebAssembly compile / instantiate for feature-gated kernel extensions.
|
|
* Uses global WebAssembly when present (Bare/V8); no Node built-ins.
|
|
*/
|
|
import { BARE_OS_POSIX_PROFILE_VERSION } from 'bare-os-protocol/bare-os-posix-profile.js'
|
|
import { bareOsMonotonicNowMs } from './bare-os-monotonic-time.js'
|
|
|
|
const DEFAULT_MAX = 512 * 1024
|
|
|
|
/**
|
|
* @param {Uint8Array} source
|
|
* @param {{ maxBytes?: number }} [opts]
|
|
*/
|
|
export async function bareOsWasmKernelCompile(source, opts = {}) {
|
|
const maxB = Math.min(
|
|
DEFAULT_MAX,
|
|
Math.max(1024, Number(opts.maxBytes) || DEFAULT_MAX)
|
|
)
|
|
if (!(source instanceof Uint8Array) || source.byteLength === 0) {
|
|
return { ok: false, reason: 'invalid_source' }
|
|
}
|
|
if (source.byteLength > maxB) {
|
|
return { ok: false, reason: 'source_too_large' }
|
|
}
|
|
const W = globalThis.WebAssembly
|
|
if (!W || typeof W.compile !== 'function') {
|
|
return { ok: false, reason: 'webassembly_unavailable' }
|
|
}
|
|
try {
|
|
const mod = await W.compile(source)
|
|
/** @type {{ name: string }[]} */
|
|
let names = []
|
|
if (typeof W.Module.exports === 'function') {
|
|
names = W.Module.exports(mod).map((e) => String(e.name || ''))
|
|
}
|
|
return { ok: true, exportNames: names }
|
|
} catch (e) {
|
|
return { ok: false, reason: String(e && e.message ? e.message : e) }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read bounded UTF-8 from Wasm linear memory (host view).
|
|
* @param {WebAssembly.Memory} memory
|
|
* @param {number} ptr
|
|
* @param {number} len
|
|
* @param {number} [maxLen]
|
|
*/
|
|
function readUtf8FromMemory(memory, ptr, len, maxLen = 4096) {
|
|
const buf = memory.buffer
|
|
const n = Math.min(len >>> 0, maxLen)
|
|
const p = ptr >>> 0
|
|
if (p + n > buf.byteLength) return ''
|
|
return new TextDecoder().decode(new Uint8Array(buf, p, n))
|
|
}
|
|
|
|
/**
|
|
* Write NUL-terminated UTF-8 into Wasm memory; returns bytes written (excl. NUL) or -1.
|
|
* @param {WebAssembly.Memory} memory
|
|
* @param {number} ptr
|
|
* @param {number} outCap
|
|
* @param {string} text
|
|
*/
|
|
function writeCStrToMemory(memory, ptr, outCap, text) {
|
|
const enc = new TextEncoder().encode(String(text || ''))
|
|
const cap = Math.max(0, outCap >>> 0)
|
|
if (cap < 2) return -1
|
|
const max = cap - 1
|
|
const n = Math.min(enc.length, max)
|
|
const p = ptr >>> 0
|
|
const buf = memory.buffer
|
|
if (p + cap > buf.byteLength) return -1
|
|
const u8 = new Uint8Array(buf, p, cap)
|
|
u8.set(enc.subarray(0, n))
|
|
u8[n] = 0
|
|
return n
|
|
}
|
|
|
|
/**
|
|
* Bounded `instantiate` with an isolated `Memory` and minimal `env` imports.
|
|
* Optional sync imports **`env.bare_os_pathconf`** / **`env.bare_os_umask_get`** when
|
|
* **`BARE_OS_WASM_KERNEL_SYSCALL=1`** (or **`opts.wasmSyscallImports: true`**) and **`ctx.bareOsPathconf`** exists.
|
|
* Also **`env.bare_os_wall_time_ms32`**: low 32 bits of wall time (`Date.now()`, signed i32 wrap) for bounded guest timing.
|
|
* With **`BARE_OS_WASM_KERNEL_MONOTONIC_MS=1`**, adds **`env.bare_os_monotonic_ms`**: floored monotonic milliseconds (`performance.now` when available), i32-shaped.
|
|
* With **`BARE_OS_WASM_KERNEL_HOSTNAME_IMPORT=1`** (requires syscall imports), adds **`env.bare_os_hostname_peek`**: writes session **`HOSTNAME`** (or **`bare-os`**) NUL-terminated into Wasm memory (**bounded**).
|
|
* With **`BARE_OS_WASM_KERNEL_CTX_API_PEEK=1`** (requires syscall imports), adds **`env.bare_os_ctx_api_version_peek`**: writes **`ctx.bareOsCtxApiVersion`** NUL-terminated (**bounded**).
|
|
* With **`BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK=1`** (requires syscall imports), adds **`env.bare_os_posix_profile_peek`**: writes declared **`BARE_OS_POSIX_PROFILE_VERSION`** NUL-terminated (**bounded**).
|
|
*
|
|
* @param {Uint8Array} source
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {{
|
|
* maxBytes?: number,
|
|
* memoryInitialPages?: number,
|
|
* memoryMaxPages?: number,
|
|
* timeoutMs?: number,
|
|
* wasmSyscallImports?: boolean,
|
|
* shellEnv?: Record<string, string | undefined>
|
|
* }} [opts]
|
|
*/
|
|
export async function bareOsWasmKernelInstantiate(source, ctx, opts = {}) {
|
|
const maxB = Math.min(
|
|
DEFAULT_MAX,
|
|
Math.max(1024, Number(opts.maxBytes) || DEFAULT_MAX)
|
|
)
|
|
if (!(source instanceof Uint8Array) || source.byteLength === 0) {
|
|
return { ok: false, reason: 'invalid_source' }
|
|
}
|
|
if (source.byteLength > maxB) {
|
|
return { ok: false, reason: 'source_too_large' }
|
|
}
|
|
const W = globalThis.WebAssembly
|
|
if (!W || typeof W.instantiate !== 'function' || typeof W.Memory !== 'function') {
|
|
return { ok: false, reason: 'webassembly_unavailable' }
|
|
}
|
|
const memInitial = Math.min(
|
|
256,
|
|
Math.max(1, Number(opts.memoryInitialPages) || 1)
|
|
)
|
|
const memMax = Math.min(
|
|
256,
|
|
Math.max(memInitial, Number(opts.memoryMaxPages) || 8)
|
|
)
|
|
const timeoutMs = Math.min(
|
|
60000,
|
|
Math.max(50, Number(opts.timeoutMs) || 5000)
|
|
)
|
|
try {
|
|
const mod = await W.compile(source)
|
|
/** @type {{ name: string }[]} */
|
|
let exportNames = []
|
|
if (typeof W.Module.exports === 'function') {
|
|
exportNames = W.Module.exports(mod).map((e) => String(e.name || ''))
|
|
}
|
|
const memory = new W.Memory({ initial: memInitial, maximum: memMax })
|
|
const shellEnv =
|
|
opts.shellEnv && typeof opts.shellEnv === 'object'
|
|
? /** @type {Record<string, string | undefined>} */ (opts.shellEnv)
|
|
: {}
|
|
const wantSyscall =
|
|
opts.wasmSyscallImports === true ||
|
|
shellEnv.BARE_OS_WASM_KERNEL_SYSCALL === '1' ||
|
|
shellEnv.BARE_OS_WASM_KERNEL_SYSCALL === 'true'
|
|
const wantMonotonicMs =
|
|
wantSyscall &&
|
|
(shellEnv.BARE_OS_WASM_KERNEL_MONOTONIC_MS === '1' ||
|
|
shellEnv.BARE_OS_WASM_KERNEL_MONOTONIC_MS === 'true')
|
|
const wantHostname =
|
|
wantSyscall &&
|
|
(shellEnv.BARE_OS_WASM_KERNEL_HOSTNAME_IMPORT === '1' ||
|
|
shellEnv.BARE_OS_WASM_KERNEL_HOSTNAME_IMPORT === 'true')
|
|
const wantCtxApiPeek =
|
|
wantSyscall &&
|
|
(shellEnv.BARE_OS_WASM_KERNEL_CTX_API_PEEK === '1' ||
|
|
shellEnv.BARE_OS_WASM_KERNEL_CTX_API_PEEK === 'true')
|
|
const wantPosixBridgePeek =
|
|
wantSyscall &&
|
|
(shellEnv.BARE_OS_WASM_KERNEL_POSIX_BRIDGE_PEEK === '1' ||
|
|
shellEnv.BARE_OS_WASM_KERNEL_POSIX_BRIDGE_PEEK === 'true')
|
|
const wantPosixProfilePeek =
|
|
wantSyscall &&
|
|
(shellEnv.BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK === '1' ||
|
|
shellEnv.BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK === 'true')
|
|
const pathconfFn = ctx && typeof ctx.bareOsPathconf === 'function'
|
|
? ctx.bareOsPathconf.bind(ctx)
|
|
: null
|
|
/** @type {Record<string, unknown>} */
|
|
const envImports = {
|
|
memory,
|
|
bare_os_nop: () => 0
|
|
}
|
|
if (wantSyscall) {
|
|
envImports.bare_os_wall_time_ms32 = () => Date.now() | 0
|
|
}
|
|
if (wantMonotonicMs) {
|
|
envImports.bare_os_monotonic_ms = () =>
|
|
Math.floor(bareOsMonotonicNowMs()) | 0
|
|
}
|
|
if (wantSyscall && pathconfFn) {
|
|
envImports.bare_os_pathconf = (
|
|
pathPtr,
|
|
pathLen,
|
|
namePtr,
|
|
nameLen,
|
|
outPtr,
|
|
outCap
|
|
) => {
|
|
try {
|
|
const path = readUtf8FromMemory(
|
|
memory,
|
|
Number(pathPtr),
|
|
Number(pathLen)
|
|
).trim()
|
|
const name = readUtf8FromMemory(
|
|
memory,
|
|
Number(namePtr),
|
|
Number(nameLen)
|
|
).trim()
|
|
if (!path.startsWith('/') || !name) return -1
|
|
const v = pathconfFn(path, name)
|
|
if (v == null || v === undefined) return -2
|
|
return writeCStrToMemory(
|
|
memory,
|
|
Number(outPtr),
|
|
Number(outCap),
|
|
String(v)
|
|
)
|
|
} catch {
|
|
return -3
|
|
}
|
|
}
|
|
envImports.bare_os_umask_get = () => {
|
|
const m = parseInt(String(shellEnv.UMASK || '022'), 8)
|
|
return Number.isFinite(m) ? m & 0o777 : 0o22
|
|
}
|
|
if (wantHostname) {
|
|
envImports.bare_os_hostname_peek = (outPtr, outCap) => {
|
|
const vfsEnv =
|
|
ctx &&
|
|
ctx.vfs &&
|
|
ctx.vfs.env &&
|
|
typeof ctx.vfs.env === 'object'
|
|
? /** @type {Record<string, string>} */ (ctx.vfs.env)
|
|
: {}
|
|
const h = String(
|
|
vfsEnv.HOSTNAME || shellEnv.HOSTNAME || 'bare-os'
|
|
).slice(0, 256)
|
|
return writeCStrToMemory(memory, Number(outPtr), Number(outCap), h)
|
|
}
|
|
}
|
|
if (wantCtxApiPeek) {
|
|
envImports.bare_os_ctx_api_version_peek = (outPtr, outCap) => {
|
|
const v = String(
|
|
ctx && ctx.bareOsCtxApiVersion != null
|
|
? ctx.bareOsCtxApiVersion
|
|
: ''
|
|
).slice(0, 64)
|
|
return writeCStrToMemory(memory, Number(outPtr), Number(outCap), v)
|
|
}
|
|
}
|
|
if (wantPosixBridgePeek) {
|
|
envImports.bare_os_posix_bridge_peek = () => {
|
|
const sock =
|
|
shellEnv.BARE_OS_POSIX_SOCKET_FD_BRIDGE === '1' ||
|
|
shellEnv.BARE_OS_POSIX_SOCKET_FD_BRIDGE === 'true'
|
|
const fcntlWait =
|
|
shellEnv.BARE_OS_POSIX_FCNTL_BLOCKING_WAIT === '1' ||
|
|
shellEnv.BARE_OS_POSIX_FCNTL_BLOCKING_WAIT === 'true'
|
|
let flags = 0
|
|
if (sock) flags |= 1
|
|
if (fcntlWait) flags |= 2
|
|
return flags | 0
|
|
}
|
|
}
|
|
if (wantPosixProfilePeek) {
|
|
envImports.bare_os_posix_profile_peek = (outPtr, outCap) => {
|
|
const v = String(BARE_OS_POSIX_PROFILE_VERSION || '').slice(0, 32)
|
|
return writeCStrToMemory(memory, Number(outPtr), Number(outCap), v)
|
|
}
|
|
}
|
|
}
|
|
const importObject = {
|
|
env: envImports
|
|
}
|
|
const instP = W.instantiate(mod, importObject)
|
|
const done = await Promise.race([
|
|
instP,
|
|
new Promise((_, rej) =>
|
|
setTimeout(
|
|
() => rej(new Error('wasm_instantiate_timeout')),
|
|
timeoutMs
|
|
)
|
|
)
|
|
])
|
|
const instance = /** @type {{ instance: WebAssembly.Instance }} */ (done)
|
|
.instance
|
|
return {
|
|
ok: true,
|
|
exportNames,
|
|
instance,
|
|
memory,
|
|
memoryInitialPages: memInitial,
|
|
memoryMaxPages: memMax,
|
|
wasmSyscallImports: !!wantSyscall,
|
|
wasmPathconfImport: !!(wantSyscall && pathconfFn),
|
|
wasmWallClockMs32Import: !!wantSyscall,
|
|
wasmMonotonicMsImport: !!wantMonotonicMs,
|
|
wasmHostnamePeekImport: !!(wantSyscall && wantHostname),
|
|
wasmCtxApiVersionPeekImport: !!(wantSyscall && wantCtxApiPeek),
|
|
wasmPosixBridgePeekImport: !!(wantSyscall && wantPosixBridgePeek),
|
|
wasmPosixProfilePeekImport: !!(wantSyscall && wantPosixProfilePeek)
|
|
}
|
|
} catch (e) {
|
|
return { ok: false, reason: String(e && e.message ? e.message : e) }
|
|
}
|
|
}
|