From 881dba078a2c4f4c959174ce313adcff8894adbf Mon Sep 17 00:00:00 2001 From: udo Date: Sun, 19 Jul 2026 15:43:52 +0000 Subject: [PATCH] Bound Wasm artifact metadata loading --- docs/setup.md | 8 +- docs/wasm-runtime-architecture.md | 23 +- scripts/run_cli_tests.sh | 1 + scripts/test_wasm_metadata_deadline.sh | 470 +++++++++++++++++++++++++ site/doc/pages/request_perf.txt | 2 +- src/wasm/worker.cpp | 247 ++++++++++++- 6 files changed, 733 insertions(+), 18 deletions(-) create mode 100755 scripts/test_wasm_metadata_deadline.sh diff --git a/docs/setup.md b/docs/setup.md index c0fc12d..cbf9825 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -246,7 +246,13 @@ Important settings: output; generated C++, exports, source map, Wasm, metadata, cached-module invalidation, and diagnostics are published or rolled back as one guarded generation. Proactive and offline precompile work remain independent of - request invocation deadlines. + request invocation deadlines. Serialized-module metadata scanning checks the + absolute deadline between section headers and before and after every bounded + 4 KiB positional read. Cache-miss full-artifact reads use the same checks + around 64 KiB positional chunks. A single in-progress regular-file syscall + cannot be cooperatively interrupted; later reads and parsing cannot overrun the budget. + Initial/final descriptor identity, unique selected metadata sections, and + strict 64-bit LEB high-bit validation reject changed or ambiguous artifacts. After editing settings, restart UCE: diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index a176e29..66fe119 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -247,9 +247,19 @@ through a bounded 4 KiB positional buffer and retains only `dylink.0`, `uce.abi` and the tiny `uce.module` identity. It skips code and data bodies rather than issuing byte-at-a-time reads or faulting those bodies into every new worker. The request profile reports the physical read-ahead bytes and positional read -count. Initial and final descriptor identity checks reject an artifact changed -during the scan. A missing/stale/invalid serialized module -still reads, validates, compiles, and republishes the complete wasm artifact. +count. The scanner checks the same non-renewable invocation deadline before +each section and before and after every buffer refill; expiry is returned as the +canonical `UCE_INVOCATION_TIMEOUT` error. One in-progress regular-file +`pread()` remains the irreducible synchronous boundary. Initial descriptor +validation matches device, inode, mode, nanosecond timestamps, and size from +the preceding lookup; final validation rejects in-place truncation or mutation. +Selected metadata sections must be unique and individually remain at most 1 MiB, +which also bounds aggregate retained metadata. Both streamed and full-artifact +LEB readers reject nonzero unused bits in the tenth `u64` byte. A +missing/stale/invalid serialized module +still reads the complete wasm artifact in deadline-checked 64 KiB positional +chunks, validates it, compiles it, and republishes the serialization. The full +read uses the same initial/final descriptor identity guard. The proactive compiler also creates that serialization immediately after source compilation, keeping first-worker native compilation off the request path. Cold module compilation and deserialization are host work, so `load_unit()` @@ -564,7 +574,7 @@ header free-functions are `inline`. The wasm backend exposes only declarations the dependency byte-for-byte, and requires the parent to recover. Failed builds persist the input signature that actually failed, so metadata from an older successful artifact cannot indefinitely defer that rebuild. - also rejects an unreadable unit without publishing a wasm artifact, restores + The gate also rejects an unreadable unit without publishing a wasm artifact, restores its permissions and proves the next CLI request compiles it, then sends 48 requests and asserts the observed worker PID set does not exceed `WORKER_COUNT`, guarding against accidental reintroduction of request-count @@ -572,6 +582,11 @@ header free-functions are `inline`. The wasm backend exposes only declarations the generated C++ snapshot has been written. The compiler must either retry and serve the post-edit source or fail closed; it must never stamp current source metadata onto wasm produced from an older snapshot. +- **Metadata scanner deadline**: an isolated one-worker gate uses a targeted + positional-read interposer to prove canonical expiry after one delayed 4 KiB + metadata read and after one delayed cache-miss 64 KiB full-artifact read, + concurrent truncation and same-size mutation rejection, malformed tenth-byte + LEB rejection, duplicate selected-section rejection, and same-worker recovery. - **Core compatibility**: the production core owns and exports its growable function table, enabling `InstancePre`. A separately built legacy `--import-table` core must still pass the demo and 64-request pool-isolation diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 717a2b3..b47902a 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -85,6 +85,7 @@ if [[ "$action" == "run" ]]; then timeout --signal=TERM --kill-after=5s 120s scripts/test_entry_freshness_ttl.sh scripts/test_unit_export_surface.sh timeout --signal=TERM --kill-after=5s 120s scripts/test_wasm_metadata_buffer.sh + timeout --signal=TERM --kill-after=5s 150s scripts/test_wasm_metadata_deadline.sh scripts/test_wasm_source_locations.sh scripts/test_socket_activation.sh fi diff --git a/scripts/test_wasm_metadata_deadline.sh b/scripts/test_wasm_metadata_deadline.sh new file mode 100755 index 0000000..ceaecfc --- /dev/null +++ b/scripts/test_wasm_metadata_deadline.sh @@ -0,0 +1,470 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +if [[ "${1:-}" != "--inside" ]]; then + exec timeout --signal=TERM --kill-after=5s 120s unshare --mount --fork --kill-child=TERM "$0" --inside +fi + +root="/tmp/uce-metadata-deadline-$$" +site="$root/site" +work="$root/work" +settings="$root/settings.cfg" +socket="$root/run/cli.sock" +log="$root/service.log" +control="$root/pread.control" +server_pid="" + +cleanup() { + status=$? + if [[ -n "$server_pid" ]] && kill -0 "$server_pid" 2>/dev/null; then + kill -TERM "$server_pid" 2>/dev/null || true + for _ in $(seq 1 100); do kill -0 "$server_pid" 2>/dev/null || break; sleep 0.02; done + kill -KILL "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi + if (( status != 0 )) && [[ -r "$log" ]]; then cat "$log" >&2; fi + rm -rf "$root" + return "$status" +} +trap cleanup EXIT +mkdir -p "$site/components" "$work" "$root/run" "$root/session" "$root/upload" +sed -E '/^[[:space:]]*(BIN_DIRECTORY|PRECOMPILE_FILES_IN|SITE_DIRECTORY|FCGI_SOCKET_PATH|FCGI_PORT|CLI_SOCKET_PATH|WS_BROKER_SOCKET_PATH|HTTP_PORT|HTTP_DOCUMENT_ROOT|SESSION_PATH|TMP_UPLOAD_PATH|WASM_CORE_PATH|WASM_INVOCATION_TIMEOUT_MS|WASM_EPOCH_PERIOD_MS|PROACTIVE_COMPILE_ENABLED|WORKER_COUNT)[[:space:]]*=/d' \ + /etc/uce/settings.cfg >"$settings" +cat >>"$settings" <"$site/driver.uce" <<'UCE' +CLI(Request& context) { + if(context.get["health"] == "1") { print(request_perf()["worker_pid"].to_u64(), "|health"); return; } + print(component(context.get["target"], context)); +} +UCE +printf '%s\n' 'COMPONENT(Request& context) { print("deadline-component"); }' >"$site/components/deadline.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("truncate-component"); }' >"$site/components/truncate.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("mutation-component"); }' >"$site/components/mutation.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("uleb-component"); }' >"$site/components/uleb.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("duplicate-component"); }' >"$site/components/duplicate.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("full-component"); }' >"$site/components/full.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("full-uleb-component"); }' >"$site/components/full-uleb.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("full-duplicate-component"); }' >"$site/components/full-duplicate.uce" + +timeout --signal=TERM --kill-after=5s 60s env UCE_PRECOMPILE_FILES_IN="$site" UCE_PRECOMPILE_BIN_DIRECTORY="$work" \ + UCE_PRECOMPILE_JOBS=1 bin/uce_fastcgi.linux.bin --precompile >"$root/precompile.log" 2>&1 +cache="$(scripts/unit_cache_directory "$work")$(realpath "$site")" +for unit in deadline truncate mutation uleb duplicate full full-uleb full-duplicate; do + [[ -s "$cache/components/$unit.uce.wasm" && -s "$cache/components/$unit.uce.cwasm" ]] + cp "$cache/components/$unit.uce.wasm" "$root/$unit.original.wasm" +done + +cat >"$root/interpose.c" <<'C' +#define _GNU_SOURCE +#define _LARGEFILE64_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct control { + char target[PATH_MAX]; + char mode[16]; + int delay_ms; + char ready[PATH_MAX]; + char release[PATH_MAX]; + char counter[PATH_MAX]; +}; + +static int read_control(int fd, struct control* control) +{ + const char* path = getenv("UCE_TEST_PREAD_CONTROL"); + if(!path || !*path) return 0; + int input = open(path, O_RDONLY | O_CLOEXEC); + if(input < 0) return 0; + char text[PATH_MAX * 4]; + ssize_t count = read(input, text, sizeof(text) - 1); + close(input); + if(count <= 0) return 0; + text[count] = 0; + if(sscanf(text, "%4095s %15s %d %4095s %4095s %4095s", control->target, control->mode, + &control->delay_ms, control->ready, control->release, control->counter) != 6) return 0; + char link[64], resolved[PATH_MAX]; + snprintf(link, sizeof(link), "/proc/self/fd/%d", fd); + ssize_t length = readlink(link, resolved, sizeof(resolved) - 1); + if(length <= 0) return 0; + resolved[length] = 0; + return strcmp(resolved, control->target) == 0; +} + +static void mark(const char* path) +{ + if(strcmp(path, "-") == 0) return; + int fd = open(path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0600); + if(fd >= 0) { ssize_t ignored = write(fd, "1\n", 2); (void)ignored; close(fd); } +} + +static void wait_release(const struct control* control) +{ + if(strcmp(control->release, "-") == 0) return; + struct timespec started, now, pause = { 0, 10000000 }; + clock_gettime(CLOCK_MONOTONIC, &started); + while(access(control->release, F_OK) != 0) + { + nanosleep(&pause, 0); + clock_gettime(CLOCK_MONOTONIC, &now); + if(now.tv_sec - started.tv_sec >= 2) break; + } +} + +static void delay_ms(int milliseconds) +{ + if(milliseconds <= 0) return; + struct timespec pause = { milliseconds / 1000, (milliseconds % 1000) * 1000000L }; + while(nanosleep(&pause, &pause) != 0 && errno == EINTR) {} +} + +static void before_read(const struct control* control) +{ + mark(control->counter); + if(strcmp(control->mode, "before") == 0 && access(control->ready, F_OK) != 0) + { + mark(control->ready); + wait_release(control); + } + if(strcmp(control->mode, "delay") == 0) delay_ms(control->delay_ms); +} + +static void after_read(const struct control* control) +{ + if(strcmp(control->mode, "after") == 0 && access(control->ready, F_OK) != 0) + { + mark(control->ready); + wait_release(control); + } +} + +ssize_t pread(int fd, void* buffer, size_t size, off_t offset) +{ + static ssize_t (*real_pread)(int, void*, size_t, off_t); + if(!real_pread) real_pread = dlsym(RTLD_NEXT, "pread"); + struct control control; + int targeted = read_control(fd, &control); + if(targeted) before_read(&control); + ssize_t result = real_pread(fd, buffer, size, offset); + if(targeted) after_read(&control); + return result; +} + +ssize_t pread64(int fd, void* buffer, size_t size, off64_t offset) +{ + static ssize_t (*real_pread64)(int, void*, size_t, off64_t); + if(!real_pread64) real_pread64 = dlsym(RTLD_NEXT, "pread64"); + struct control control; + int targeted = read_control(fd, &control); + if(targeted) before_read(&control); + ssize_t result = real_pread64(fd, buffer, size, offset); + if(targeted) after_read(&control); + return result; +} +C +timeout 15s cc -shared -fPIC -O2 -Wall -Wextra -Werror -o "$root/interpose.so" "$root/interpose.c" -ldl + +timeout --signal=TERM --kill-after=5s 90s env LD_PRELOAD="$root/interpose.so" UCE_TEST_PREAD_CONTROL="$control" \ + bin/uce_fastcgi.linux.bin >"$log" 2>&1 & +server_pid=$! +for _ in $(seq 1 400); do [[ -S "$socket" ]] && break; sleep 0.02; done +[[ -S "$socket" ]] || { cat "$log" >&2; exit 1; } + +request() { + timeout --signal=TERM --kill-after=1s 5s scripts/uce-cli --socket "$socket" --get /driver.uce "$@" +} + +health=$(request health=1) +worker_pid=${health%%|*} +[[ "$health" == "$worker_pid|health" ]] + +restore() { + local unit="$1" wasm="$cache/components/$1.uce.wasm" cwasm="$cache/components/$1.uce.cwasm" + cp "$root/$unit.original.wasm" "$wasm" + python3 - "$wasm" "$cwasm" <<'PY' +import os, pathlib, sys +wasm, cwasm = map(pathlib.Path, sys.argv[1:]) +stamp = cwasm.stat().st_mtime_ns - 1_000_000 +os.utime(wasm, ns=(stamp, stamp)) +PY +} + +prepare_deadline() { + local wasm="$cache/components/deadline.uce.wasm" cwasm="$cache/components/deadline.uce.cwasm" + python3 - "$wasm" "$cwasm" <<'PY' +import os, pathlib, sys +wasm, cwasm = map(pathlib.Path, sys.argv[1:]) +with wasm.open("ab") as output: + output.write(b"\0\1\0" * 7000) +stamp = cwasm.stat().st_mtime_ns - 1_000_000 +os.utime(wasm, ns=(stamp, stamp)) +PY +} + +prepare_mutation() { + local wasm="$cache/components/mutation.uce.wasm" cwasm="$cache/components/mutation.uce.cwasm" + python3 - "$wasm" "$cwasm" "$root/mutation-offset" <<'PY' +import os, pathlib, sys +wasm, cwasm, offset_file = map(pathlib.Path, sys.argv[1:]) +payload = bytes([7]) + b"padding" + bytes(8192) +value = len(payload) +encoded = bytearray() +while True: + byte = value & 0x7f + value >>= 7 + encoded.append(byte | (0x80 if value else 0)) + if not value: + break +with wasm.open("ab") as output: + output.write(bytes([0]) + encoded + payload) +offset_file.write_text(str(wasm.stat().st_size - 4096)) +stamp = cwasm.stat().st_mtime_ns - 1_000_000 +os.utime(wasm, ns=(stamp, stamp)) +PY +} + +wait_ready() { + local ready="$1" request_pid="$2" deadline=$((SECONDS + 3)) + while [[ ! -e "$ready" ]] && kill -0 "$request_pid" 2>/dev/null && (( SECONDS < deadline )); do sleep 0.01; done + [[ -e "$ready" ]] || { echo "metadata read gate did not activate" >&2; return 1; } +} + +prepare_deadline +deadline_wasm="$cache/components/deadline.uce.wasm" +deadline_counter="$root/deadline.counter" +printf '%s delay 150 - - %s\n' "$deadline_wasm" "$deadline_counter" >"$control" +started=$(date +%s%N) +set +e +deadline_output=$(request target=components/deadline.uce 2>&1) +set -e +elapsed_ms=$(( ($(date +%s%N) - started) / 1000000 )) +[[ "$deadline_output" == *UCE_INVOCATION_TIMEOUT:* ]] || { echo "metadata scan lacked canonical timeout: $deadline_output" >&2; exit 1; } +(( elapsed_ms >= 100 && elapsed_ms < 2000 )) || { echo "metadata timeout took ${elapsed_ms}ms" >&2; exit 1; } +[[ $(wc -l <"$deadline_counter") -eq 1 ]] || { echo "metadata scanner read past the first delayed refill" >&2; exit 1; } +rm -f "$control" +[[ "$(request health=1)" == "$worker_pid|health" ]] +restore deadline +[[ "$(request target=components/deadline.uce)" == deadline-component ]] + +rm -f "$cache/components/deadline.uce.cwasm" +touch "$deadline_wasm" +printf '%s delay 150 - - %s\n' "$deadline_wasm" "$root/full-deadline.counter" >"$control" +started=$(date +%s%N) +set +e +full_deadline_output=$(request target=components/deadline.uce 2>&1) +set -e +elapsed_ms=$(( ($(date +%s%N) - started) / 1000000 )) +[[ "$full_deadline_output" == *UCE_INVOCATION_TIMEOUT:* ]] || { echo "full artifact read lacked canonical timeout: $full_deadline_output" >&2; exit 1; } +(( elapsed_ms >= 100 && elapsed_ms < 2000 )) || { echo "full artifact timeout took ${elapsed_ms}ms" >&2; exit 1; } +[[ $(wc -l <"$root/full-deadline.counter") -eq 1 ]] || { echo "full artifact reader continued after the first delayed chunk" >&2; exit 1; } +rm -f "$control" +[[ "$(request health=1)" == "$worker_pid|health" ]] +[[ "$(request target=components/deadline.uce)" == deadline-component ]] +[[ -s "$cache/components/deadline.uce.cwasm" ]] + +truncate_wasm="$cache/components/truncate.uce.wasm" +truncate_ready="$root/truncate.ready" +truncate_release="$root/truncate.release" +printf '%s before 0 %s %s %s\n' "$truncate_wasm" "$truncate_ready" "$truncate_release" "$root/truncate.counter" >"$control" +( set +e; request target=components/truncate.uce >"$root/truncate.output" 2>&1; printf '%s\n' "$?" >"$root/truncate.status" ) & +request_pid=$! +wait_ready "$truncate_ready" "$request_pid" +truncate -s 4 "$truncate_wasm" +touch "$truncate_release" +wait "$request_pid" +[[ $(<"$root/truncate.status") -ne 0 ]] +grep -q '\[wasm\] component load failed: not a supported wasm module' "$log" +! grep -q 'truncate-component' "$root/truncate.output" +rm -f "$control" +[[ "$(request health=1)" == "$worker_pid|health" ]] +restore truncate +[[ "$(request target=components/truncate.uce)" == truncate-component ]] + +prepare_mutation +mutation_wasm="$cache/components/mutation.uce.wasm" +mutation_ready="$root/mutation.ready" +mutation_release="$root/mutation.release" +printf '%s after 0 %s %s %s\n' "$mutation_wasm" "$mutation_ready" "$mutation_release" "$root/mutation.counter" >"$control" +( set +e; request target=components/mutation.uce >"$root/mutation.output" 2>&1; printf '%s\n' "$?" >"$root/mutation.status" ) & +request_pid=$! +wait_ready "$mutation_ready" "$request_pid" +python3 - "$mutation_wasm" "$(<"$root/mutation-offset")" <<'PY' +import os, pathlib, sys +path = pathlib.Path(sys.argv[1]) +offset = int(sys.argv[2]) +with path.open("r+b", buffering=0) as artifact: + artifact.seek(offset) + byte = artifact.read(1) + artifact.seek(offset) + artifact.write(bytes([byte[0] ^ 1])) + os.fsync(artifact.fileno()) +PY +touch "$mutation_release" +wait "$request_pid" +[[ $(<"$root/mutation.status") -ne 0 ]] +grep -q '\[wasm\] component load failed: wasm artifact changed while loading metadata' "$log" +! grep -q 'mutation-component' "$root/mutation.output" +rm -f "$control" +[[ "$(request health=1)" == "$worker_pid|health" ]] +restore mutation +[[ "$(request target=components/mutation.uce)" == mutation-component ]] + +uleb_wasm="$cache/components/uleb.uce.wasm" +uleb_cwasm="$cache/components/uleb.uce.cwasm" +python3 - "$uleb_wasm" "$uleb_cwasm" <<'PY' +import os, pathlib, sys +wasm, cwasm = map(pathlib.Path, sys.argv[1:]) +with wasm.open("r+b", buffering=0) as artifact: + artifact.seek(9) + artifact.write(bytes([0x80]) * 9 + bytes([0x02])) + os.fsync(artifact.fileno()) +stamp = cwasm.stat().st_mtime_ns - 1_000_000 +os.utime(wasm, ns=(stamp, stamp)) +PY +set +e +uleb_output=$(request target=components/uleb.uce 2>&1) +set -e +grep -q '\[wasm\] component load failed: malformed wasm section header' "$log" +[[ "$uleb_output" != *uleb-component* ]] +[[ "$(request health=1)" == "$worker_pid|health" ]] +restore uleb +[[ "$(request target=components/uleb.uce)" == uleb-component ]] + +duplicate_wasm="$cache/components/duplicate.uce.wasm" +duplicate_cwasm="$cache/components/duplicate.uce.cwasm" +python3 - "$duplicate_wasm" "$duplicate_cwasm" <<'PY' +import os, pathlib, sys +wasm, cwasm = map(pathlib.Path, sys.argv[1:]) +payload = bytes([10]) + b"uce.module" + b"duplicate" +value = len(payload) +encoded = bytearray() +while True: + byte = value & 0x7f + value >>= 7 + encoded.append(byte | (0x80 if value else 0)) + if not value: + break +with wasm.open("ab") as artifact: + artifact.write(bytes([0]) + encoded + payload) +stamp = cwasm.stat().st_mtime_ns - 1_000_000 +os.utime(wasm, ns=(stamp, stamp)) +PY +set +e +duplicate_output=$(request target=components/duplicate.uce 2>&1) +set -e +grep -q '\[wasm\] component load failed: duplicate uce.module metadata section' "$log" +[[ "$duplicate_output" != *duplicate-component* ]] +[[ "$(request health=1)" == "$worker_pid|health" ]] +restore duplicate +[[ "$(request target=components/duplicate.uce)" == duplicate-component ]] + +full_wasm="$cache/components/full.uce.wasm" +full_cwasm="$cache/components/full.uce.cwasm" +python3 - "$full_wasm" <<'PY' +import pathlib, sys +wasm = pathlib.Path(sys.argv[1]) +payload = bytes([7]) + b"uce.abi" + bytes(1024 * 1024) +value = len(payload) +encoded = bytearray() +while True: + byte = value & 0x7f + value >>= 7 + encoded.append(byte | (0x80 if value else 0)) + if not value: + break +with wasm.open("ab") as artifact: + artifact.write(bytes([0]) + encoded + payload) +PY +rm -f "$full_cwasm" +set +e +full_output=$(request target=components/full.uce 2>&1) +set -e +grep -Eq '\[wasm\] component load failed: .*oversized wasm metadata section' "$log" +[[ "$full_output" != *full-component* ]] +[[ "$(request health=1)" == "$worker_pid|health" ]] +cp "$root/full.original.wasm" "$full_wasm" +[[ "$(request target=components/full.uce)" == full-component ]] +[[ -s "$full_cwasm" ]] + +full_uleb_wasm="$cache/components/full-uleb.uce.wasm" +full_uleb_cwasm="$cache/components/full-uleb.uce.cwasm" +python3 - "$full_uleb_wasm" <<'PY' +import os, pathlib, sys +wasm = pathlib.Path(sys.argv[1]) +with wasm.open("r+b", buffering=0) as artifact: + artifact.seek(9) + artifact.write(bytes([0x80]) * 9 + bytes([0x02])) + os.fsync(artifact.fileno()) +PY +rm -f "$full_uleb_cwasm" +malformed_before=$(grep -c 'malformed wasm section header' "$log" || true) +set +e +full_uleb_output=$(request target=components/full-uleb.uce 2>&1) +set -e +malformed_after=$(grep -c 'malformed wasm section header' "$log" || true) +(( malformed_after == malformed_before + 1 )) +[[ "$full_uleb_output" != *full-uleb-component* ]] +[[ "$(request health=1)" == "$worker_pid|health" ]] +cp "$root/full-uleb.original.wasm" "$full_uleb_wasm" +[[ "$(request target=components/full-uleb.uce)" == full-uleb-component ]] +[[ -s "$full_uleb_cwasm" ]] + +full_duplicate_wasm="$cache/components/full-duplicate.uce.wasm" +full_duplicate_cwasm="$cache/components/full-duplicate.uce.cwasm" +python3 - "$full_duplicate_wasm" <<'PY' +import pathlib, sys +wasm = pathlib.Path(sys.argv[1]) +payload = bytes([10]) + b"uce.module" + b"duplicate" +value = len(payload) +encoded = bytearray() +while True: + byte = value & 0x7f + value >>= 7 + encoded.append(byte | (0x80 if value else 0)) + if not value: + break +with wasm.open("ab") as artifact: + artifact.write(bytes([0]) + encoded + payload) +PY +rm -f "$full_duplicate_cwasm" +duplicate_before=$(grep -c 'duplicate uce.module metadata section' "$log" || true) +set +e +full_duplicate_output=$(request target=components/full-duplicate.uce 2>&1) +set -e +duplicate_after=$(grep -c 'duplicate uce.module metadata section' "$log" || true) +(( duplicate_after == duplicate_before + 1 )) +[[ "$full_duplicate_output" != *full-duplicate-component* ]] +[[ "$(request health=1)" == "$worker_pid|health" ]] +cp "$root/full-duplicate.original.wasm" "$full_duplicate_wasm" +[[ "$(request target=components/full-duplicate.uce)" == full-duplicate-component ]] +[[ -s "$full_duplicate_cwasm" ]] + +! grep -Eiq 'panic|segfault|permission denied' "$log" +echo "wasm metadata deadline passed: streamed and full-read timeouts, mutation guards, and parser bounds passed; same worker recovered" diff --git a/site/doc/pages/request_perf.txt b/site/doc/pages/request_perf.txt index 5ec7df0..d071508 100644 --- a/site/doc/pages/request_perf.txt +++ b/site/doc/pages/request_perf.txt @@ -19,7 +19,7 @@ Entry readiness reports inclusive `ready_freshness_us` plus `ready_source_genera Successful first loads within the request are counted by `unit_load_count` and divided into `unit_module_us`, `unit_allocate_us`, `unit_import_us`, `unit_instantiate_us`, and `unit_initialize_us`. These cover compiled-module lookup, guest memory/table allocation, import construction, Wasmtime instantiation, and relocations/constructors/request binding. `entry_unit_load_count` and `entry_unit_materialize_us` isolate the initial page/CLI unit; `dynamic_include_load_count` and `dynamic_include_materialize_us` isolate side units first requested through `component()`. The per-unit bounded list adds `kind` (`entry` or `component`) and `materialize_us`, the inclusive host-side time from module acquisition through request binding. Repeated handlers from an already loaded unit are excluded. -`unit_module_cache_hit_count` and `unit_module_cache_miss_count` divide module loads by the worker's compiled-module cache. A miss is further identified by `unit_module_serialized_cache_hit_count` when Wasmtime deserializes the current `.cwasm`; `unit_module_compile_count` means it fell back to compiling the `.wasm`. `unit_module_lookup_us`, `unit_module_read_us`, `unit_module_read_bytes`, `unit_module_read_count`, `unit_module_parse_us`, `unit_module_compile_us`, and `unit_module_classify_us` divide `unit_module_us` into artifact stat/cache lookup, wasm metadata/full-artifact read volume, positional metadata reads, custom-section parse, deserialize-or-compile, and immutable import classification. Per-unit `unit_module_operations` expose the same `read_count`. On a serialized-module hit, `read_bytes` is the physical bounded read-ahead volume rather than only the logical selected bytes. The scanner skips code/data bodies and retains only section headers plus `dylink.0`, `uce.abi`, and `uce.module`; compilation fallback reads the complete wasm and does not use positional metadata reads. The phase sum can be below the total because allocation and cache publication overhead remain in the aggregate. +`unit_module_cache_hit_count` and `unit_module_cache_miss_count` divide module loads by the worker's compiled-module cache. A miss is further identified by `unit_module_serialized_cache_hit_count` when Wasmtime deserializes the current `.cwasm`; `unit_module_compile_count` means it fell back to compiling the `.wasm`. `unit_module_lookup_us`, `unit_module_read_us`, `unit_module_read_bytes`, `unit_module_read_count`, `unit_module_parse_us`, `unit_module_compile_us`, and `unit_module_classify_us` divide `unit_module_us` into artifact stat/cache lookup, wasm metadata/full-artifact read volume, positional reads, custom-section parse, deserialize-or-compile, and immutable import classification. Per-unit `unit_module_operations` expose the same `read_count`. On a serialized-module hit, `read_bytes` is the physical bounded read-ahead volume rather than only the logical selected bytes. The scanner skips code/data bodies and retains one bounded `dylink.0`, `uce.abi`, and `uce.module` section. It checks the absolute invocation deadline between sections and bounded 4 KiB positional reads, and verifies descriptor identity before and after scanning. Compilation fallback reads the complete wasm in deadline-checked 64 KiB positional chunks under the same identity guard. The phase sum can be below the total because allocation and cache publication overhead remain in the aggregate. `unit_symbol_resolve_count` and `unit_symbol_resolve_us` isolate function and data symbol lookup within `unit_import_us`. The remainder of import time covers import-vector construction, Wasmtime Globals, GOT function table placement, and related bindings. diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index bfa89d9..4ef079d 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -123,6 +123,7 @@ struct WasmUnitModuleLoadProfile { bool cache_hit = false; bool serialized_cache_hit = false; + bool timed_out = false; u64 lookup_us = 0; u64 read_us = 0; u64 read_bytes = 0; @@ -725,6 +726,8 @@ static bool wasm_read_uleb(const std::vector& buf, size_t& pos, size_t end, if(pos >= end || shift >= 64) return(false); u8 byte = buf[pos++]; + if(shift == 63 && (byte & 0x7e) != 0) + return(false); out |= ((u64)(byte & 0x7f)) << shift; if((byte & 0x80) == 0) return(true); @@ -732,8 +735,19 @@ static bool wasm_read_uleb(const std::vector& buf, size_t& pos, size_t end, } } -static bool wasm_parse_sections(const std::vector& bytes, WasmDylinkInfo& dylink, WasmAbiInfo& abi, String& error) +static bool wasm_parse_sections(const std::vector& bytes, WasmDylinkInfo& dylink, WasmAbiInfo& abi, String& error, + bool deadline_active = false, std::chrono::steady_clock::time_point deadline = {}, bool* timed_out = 0) { + auto deadline_expired = [&]() { + if(!deadline_active || std::chrono::steady_clock::now() < deadline) + return(false); + if(timed_out) + *timed_out = true; + error = "wasm metadata parse timed out"; + return(true); + }; + if(timed_out) + *timed_out = false; if(bytes.size() < 8 || memcmp(bytes.data(), "\0asm", 4) != 0) { error = "not a wasm module"; @@ -745,8 +759,13 @@ static bool wasm_parse_sections(const std::vector& bytes, WasmDylinkInfo& dy return(false); } size_t pos = 8; + bool dylink_seen = false; + bool abi_seen = false; + bool module_seen = false; while(pos < bytes.size()) { + if(deadline_expired()) + return(false); u8 section_id = bytes[pos++]; u64 size = 0; if(!wasm_read_uleb(bytes, pos, bytes.size(), size) || size > bytes.size() - pos) @@ -764,12 +783,30 @@ static bool wasm_parse_sections(const std::vector& bytes, WasmDylinkInfo& dy error = "malformed custom section name"; return(false); } + if(name_len > 64) + { + pos = end; + continue; + } String name((const char*)bytes.data() + cursor, (size_t)name_len); cursor += (size_t)name_len; + if((name == "dylink.0" || name == "uce.abi" || name == "uce.module") && size > 1024 * 1024) + { + error = "oversized wasm metadata section"; + return(false); + } if(name == "dylink.0") { + if(dylink_seen) + { + error = "duplicate dylink.0 metadata section"; + return(false); + } + dylink_seen = true; while(cursor < end) { + if(deadline_expired()) + return(false); u8 sub = bytes[cursor++]; u64 sub_len = 0; if(!wasm_read_uleb(bytes, cursor, end, sub_len) || sub_len > end - cursor) @@ -808,11 +845,19 @@ static bool wasm_parse_sections(const std::vector& bytes, WasmDylinkInfo& dy } else if(name == "uce.abi") { + if(abi_seen) + { + error = "duplicate uce.abi metadata section"; + return(false); + } + abi_seen = true; String text((const char*)bytes.data() + cursor, end - cursor); abi.found = true; size_t line_start = 0; while(line_start < text.size()) { + if(deadline_expired()) + return(false); size_t line_end = text.find('\n', line_start); if(line_end == String::npos) line_end = text.size(); @@ -825,7 +870,15 @@ static bool wasm_parse_sections(const std::vector& bytes, WasmDylinkInfo& dy } } else if(name == "uce.module") + { + if(module_seen) + { + error = "duplicate uce.module metadata section"; + return(false); + } + module_seen = true; abi.module_name.assign((const char*)bytes.data() + cursor, end - cursor); + } } pos = end; } @@ -849,6 +902,98 @@ static bool wasm_read_file(const String& path, std::vector& out) return((bool)in); } +static bool wasm_read_file_deadline(const String& path, std::vector& out, String& error, u64& bytes_read, u64& read_count, + bool& timed_out, bool deadline_active, std::chrono::steady_clock::time_point deadline, const struct stat& expected_st) +{ + bytes_read = 0; + read_count = 0; + timed_out = false; + auto expired = [&]() { + if(!deadline_active || std::chrono::steady_clock::now() < deadline) + return(false); + timed_out = true; + error = "wasm artifact read timed out"; + return(true); + }; + int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if(fd < 0) + return(false); + struct stat st; + u64 expected_modified_ns = (u64)expected_st.st_mtim.tv_sec * 1000000000ull + (u64)expected_st.st_mtim.tv_nsec; + u64 expected_changed_ns = (u64)expected_st.st_ctim.tv_sec * 1000000000ull + (u64)expected_st.st_ctim.tv_nsec; + if(fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) + { + close(fd); + error = "wasm artifact is not a regular file"; + return(false); + } + u64 modified_ns = (u64)st.st_mtim.tv_sec * 1000000000ull + (u64)st.st_mtim.tv_nsec; + u64 changed_ns = (u64)st.st_ctim.tv_sec * 1000000000ull + (u64)st.st_ctim.tv_nsec; + if(st.st_dev != expected_st.st_dev || st.st_ino != expected_st.st_ino || st.st_mode != expected_st.st_mode || + modified_ns != expected_modified_ns || changed_ns != expected_changed_ns || st.st_size != expected_st.st_size) + { + close(fd); + error = "wasm artifact changed while loading"; + return(false); + } + out.clear(); + u64 offset = 0; + while(offset < (u64)st.st_size) + { + if(expired()) + break; + size_t wanted = (size_t)std::min(64 * 1024, (u64)st.st_size - offset); + if(offset > out.max_size() || wanted > out.max_size() - (size_t)offset) + { + error = "wasm artifact is too large"; + break; + } + try + { + out.resize((size_t)offset + wanted); + } + catch(const std::bad_alloc&) + { + error = "cannot allocate wasm artifact buffer"; + break; + } + catch(const std::length_error&) + { + error = "wasm artifact is too large"; + break; + } + if(expired()) + break; + ssize_t count = pread(fd, out.data() + offset, wanted, (off_t)offset); + if(count < 0 && errno == EINTR) + continue; + if(count <= 0) + { + error = "cannot read wasm artifact"; + break; + } + offset += (u64)count; + out.resize((size_t)offset); + bytes_read += (u64)count; + read_count++; + if(expired()) + break; + } + struct stat final_st; + if(error == "" && (fstat(fd, &final_st) != 0 || final_st.st_dev != st.st_dev || final_st.st_ino != st.st_ino || + final_st.st_mode != st.st_mode || final_st.st_mtim.tv_sec != st.st_mtim.tv_sec || + final_st.st_mtim.tv_nsec != st.st_mtim.tv_nsec || final_st.st_ctim.tv_sec != st.st_ctim.tv_sec || + final_st.st_ctim.tv_nsec != st.st_ctim.tv_nsec || final_st.st_size != st.st_size)) + error = "wasm artifact changed while loading"; + close(fd); + if(error != "") + { + out.clear(); + return(false); + } + return(true); +} + static bool wasm_source_map_load(const String& path, WasmSourceMap& map) { std::ifstream input(path); @@ -905,6 +1050,17 @@ struct WasmMetadataReader u64 buffer_offset = 0; size_t buffer_size = 0; u8 buffer[4096]; + bool deadline_active = false; + std::chrono::steady_clock::time_point deadline; + bool timed_out = false; + + bool expired() + { + if(!deadline_active || std::chrono::steady_clock::now() < deadline) + return(false); + timed_out = true; + return(true); + } bool read(u64 offset, u8* out, size_t size) { @@ -912,6 +1068,8 @@ struct WasmMetadataReader return(false); while(size > 0) { + if(expired()) + return(false); if(offset < buffer_offset || offset >= buffer_offset + buffer_size) { buffer_offset = offset; @@ -919,6 +1077,8 @@ struct WasmMetadataReader size_t wanted = (size_t)std::min(sizeof(buffer), file_size - offset); while(true) { + if(expired()) + return(false); ssize_t count = pread(fd, buffer, wanted, (off_t)offset); if(count < 0 && errno == EINTR) continue; @@ -927,6 +1087,8 @@ struct WasmMetadataReader buffer_size = (size_t)count; bytes_read += (u64)count; read_count++; + if(expired()) + return(false); break; } } @@ -949,6 +1111,8 @@ struct WasmMetadataReader u8 byte = 0; if(!read(pos++, &byte, 1)) return(false); + if(shift == 63 && (byte & 0x7e) != 0) + return(false); out |= ((u64)(byte & 0x7f)) << shift; if((byte & 0x80) == 0) return(true); @@ -970,15 +1134,16 @@ static void wasm_write_uleb(std::vector& out, u64 value) } static bool wasm_read_metadata_file(const String& path, std::vector& metadata, String& error, u64& bytes_read, u64& read_count, - u64 expected_modified_ns, u64 expected_changed_ns, u64 expected_size) + bool& timed_out, bool deadline_active, std::chrono::steady_clock::time_point deadline, const struct stat& expected_st) { bytes_read = 0; read_count = 0; + timed_out = false; int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); if(fd < 0) return(false); struct stat st; - if(fstat(fd, &st) != 0 || st.st_size < 8) + if(fstat(fd, &st) != 0 || !S_ISREG(st.st_mode) || st.st_size < 8) { close(fd); error = "not a wasm module"; @@ -986,7 +1151,10 @@ static bool wasm_read_metadata_file(const String& path, std::vector& metadat } u64 modified_ns = (u64)st.st_mtim.tv_sec * 1000000000ull + (u64)st.st_mtim.tv_nsec; u64 changed_ns = (u64)st.st_ctim.tv_sec * 1000000000ull + (u64)st.st_ctim.tv_nsec; - if(modified_ns != expected_modified_ns || changed_ns != expected_changed_ns || (u64)st.st_size != expected_size) + u64 expected_modified_ns = (u64)expected_st.st_mtim.tv_sec * 1000000000ull + (u64)expected_st.st_mtim.tv_nsec; + u64 expected_changed_ns = (u64)expected_st.st_ctim.tv_sec * 1000000000ull + (u64)expected_st.st_ctim.tv_nsec; + if(st.st_dev != expected_st.st_dev || st.st_ino != expected_st.st_ino || st.st_mode != expected_st.st_mode || + modified_ns != expected_modified_ns || changed_ns != expected_changed_ns || st.st_size != expected_st.st_size) { close(fd); error = "wasm artifact changed while loading metadata"; @@ -996,19 +1164,30 @@ static bool wasm_read_metadata_file(const String& path, std::vector& metadat WasmMetadataReader reader; reader.fd = fd; reader.file_size = file_size; + reader.deadline_active = deadline_active; + reader.deadline = deadline; u8 header[8]; if(!reader.read(0, header, sizeof(header)) || memcmp(header, "\0asm\1\0\0\0", sizeof(header)) != 0) { bytes_read = reader.bytes_read; read_count = reader.read_count; + timed_out = reader.timed_out; close(fd); - error = "not a supported wasm module"; + error = timed_out ? "wasm metadata read timed out" : "not a supported wasm module"; return(false); } metadata.assign(header, header + sizeof(header)); u64 pos = sizeof(header); + bool dylink_seen = false; + bool abi_seen = false; + bool module_seen = false; while(pos < file_size) { + if(reader.expired()) + { + error = "wasm metadata read timed out"; + break; + } u8 section_id = 0; if(!reader.read(pos++, §ion_id, 1)) { @@ -1043,6 +1222,16 @@ static bool wasm_read_metadata_file(const String& path, std::vector& metadat } if(name == "dylink.0" || name == "uce.abi" || name == "uce.module") { + bool duplicate = (name == "dylink.0" && dylink_seen) || (name == "uce.abi" && abi_seen) || + (name == "uce.module" && module_seen); + if(duplicate) + { + error = "duplicate " + name + " metadata section"; + break; + } + if(name == "dylink.0") dylink_seen = true; + else if(name == "uce.abi") abi_seen = true; + else module_seen = true; if(section_size > 1024 * 1024) { error = "oversized wasm metadata section"; @@ -1063,6 +1252,9 @@ static bool wasm_read_metadata_file(const String& path, std::vector& metadat } bytes_read = reader.bytes_read; read_count = reader.read_count; + timed_out = reader.timed_out || reader.expired(); + if(timed_out) + error = "wasm metadata read timed out"; struct stat final_st; if(error == "" && (fstat(fd, &final_st) != 0 || final_st.st_dev != st.st_dev || final_st.st_ino != st.st_ino || final_st.st_mtim.tv_sec != st.st_mtim.tv_sec || final_st.st_mtim.tv_nsec != st.st_mtim.tv_nsec || @@ -1195,8 +1387,16 @@ public: return(cfg.cache_root + source_path + ".wasm"); } - std::shared_ptr unit_module(const String& source_path, String& error, WasmUnitModuleLoadProfile& profile) + std::shared_ptr unit_module(const String& source_path, String& error, WasmUnitModuleLoadProfile& profile, + bool deadline_active, std::chrono::steady_clock::time_point deadline) { + auto deadline_expired = [&]() { + if(!deadline_active || std::chrono::steady_clock::now() < deadline) + return(false); + profile.timed_out = true; + error = "wasm module load timed out"; + return(true); + }; auto lookup_start = std::chrono::steady_clock::now(); String wasm_path = unit_wasm_path(source_path); struct stat st; @@ -1207,11 +1407,20 @@ public: error = "no wasm artifact for " + source_path + " (expected " + wasm_path + ")"; return(nullptr); } + if(!S_ISREG(st.st_mode)) + { + profile.lookup_us = (u64)std::chrono::duration_cast( + std::chrono::steady_clock::now() - lookup_start).count(); + error = "wasm artifact is not a regular file: " + wasm_path; + return(nullptr); + } auto cached = module_cache.find(wasm_path); u64 modified_ns = (u64)st.st_mtim.tv_sec * 1000000000ull + (u64)st.st_mtim.tv_nsec; u64 changed_ns = (u64)st.st_ctim.tv_sec * 1000000000ull + (u64)st.st_ctim.tv_nsec; profile.lookup_us = (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - lookup_start).count(); + if(deadline_expired()) + return(nullptr); if(cached != module_cache.end() && cached->second->modified_ns == modified_ns && cached->second->changed_ns == changed_ns && cached->second->size == (u64)st.st_size) { profile.cache_hit = true; @@ -1230,13 +1439,15 @@ public: profile.compile_us = (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - compile_start).count(); profile.serialized_cache_hit = compiled_or_cached.has_value(); + if(deadline_expired()) + return(nullptr); std::vector bytes; auto read_start = std::chrono::steady_clock::now(); bool read_ok = profile.serialized_cache_hit - ? wasm_read_metadata_file(wasm_path, bytes, error, profile.read_bytes, profile.read_count, modified_ns, changed_ns, unit->size) - : wasm_read_file(wasm_path, bytes); - if(!profile.serialized_cache_hit) - profile.read_bytes = bytes.size(); + ? wasm_read_metadata_file(wasm_path, bytes, error, profile.read_bytes, profile.read_count, profile.timed_out, + deadline_active, deadline, st) + : wasm_read_file_deadline(wasm_path, bytes, error, profile.read_bytes, profile.read_count, profile.timed_out, + deadline_active, deadline, st); if(!read_ok) { profile.read_us = (u64)std::chrono::duration_cast( @@ -1247,8 +1458,10 @@ public: } profile.read_us = (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - read_start).count(); + if(deadline_expired()) + return(nullptr); auto parse_start = std::chrono::steady_clock::now(); - if(!wasm_parse_sections(bytes, unit->dylink, unit->abi, error)) + if(!wasm_parse_sections(bytes, unit->dylink, unit->abi, error, deadline_active, deadline, &profile.timed_out)) { profile.parse_us = (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - parse_start).count(); @@ -1257,6 +1470,8 @@ public: } profile.parse_us = (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - parse_start).count(); + if(deadline_expired()) + return(nullptr); if(!unit->dylink.found) { error = wasm_path + ": missing dylink.0 mem_info (not a PIC side module)"; @@ -1275,6 +1490,8 @@ public: profile.compile_us += (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - compile_start).count(); } + if(deadline_expired()) + return(nullptr); if(!compiled_or_cached) { error = wasm_path + ": compile failed: " + compile_error; @@ -1284,6 +1501,8 @@ public: auto classify_start = std::chrono::steady_clock::now(); for(auto import_type : unit->module->imports()) { + if(deadline_expired()) + return(nullptr); String mod_name(import_type.module()); String name(import_type.name()); auto extern_type = wasmtime::ExternType::from_import(import_type); @@ -1317,6 +1536,8 @@ public: } profile.classify_us = (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - classify_start).count(); + if(deadline_expired()) + return(nullptr); module_cache[wasm_path] = unit; return(unit); } @@ -2274,7 +2495,7 @@ private: String error; auto module_start = std::chrono::steady_clock::now(); WasmUnitModuleLoadProfile module_profile; - auto mod = worker.unit_module(source_path, error, module_profile); + auto mod = worker.unit_module(source_path, error, module_profile, invocation_active, invocation_deadline); u64 module_total_us = (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - module_start).count(); unit_module_total_us += module_total_us; @@ -2319,6 +2540,8 @@ private: module_operation_index = 32; unit_module_operations_dropped++; } + if(module_profile.timed_out || invocation_expired()) + return(invocation_timeout_error()); if(!mod) return(error); // Compiling/deserializing a cold module is host work. Refresh the guest