Bound Wasm artifact metadata loading

This commit is contained in:
udo
2026-07-19 15:43:52 +00:00
parent 15fdfe725d
commit 881dba078a
6 changed files with 733 additions and 18 deletions
+7 -1
View File
@@ -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:
+19 -4
View File
@@ -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
+1
View File
@@ -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
+470
View File
@@ -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" <<CFG
BIN_DIRECTORY=$work
PRECOMPILE_FILES_IN=$site
SITE_DIRECTORY=$site
FCGI_SOCKET_PATH=$root/run/fastcgi.sock
FCGI_PORT=
CLI_SOCKET_PATH=$socket
WS_BROKER_SOCKET_PATH=$root/run/ws.sock
HTTP_PORT=
HTTP_DOCUMENT_ROOT=$site
SESSION_PATH=$root/session
TMP_UPLOAD_PATH=$root/upload
WASM_CORE_PATH=$(pwd)/bin/wasm/core.wasm
WASM_INVOCATION_TIMEOUT_MS=100
WASM_EPOCH_PERIOD_MS=10
PROACTIVE_COMPILE_ENABLED=0
WORKER_COUNT=1
CFG
mount --bind "$settings" /etc/uce/settings.cfg
cat >"$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 <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
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"
+1 -1
View File
@@ -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.
+235 -12
View File
@@ -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<u8>& 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<u8>& buf, size_t& pos, size_t end,
}
}
static bool wasm_parse_sections(const std::vector<u8>& bytes, WasmDylinkInfo& dylink, WasmAbiInfo& abi, String& error)
static bool wasm_parse_sections(const std::vector<u8>& 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<u8>& 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<u8>& 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<u8>& 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<u8>& 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<u8>& out)
return((bool)in);
}
static bool wasm_read_file_deadline(const String& path, std::vector<u8>& 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<u64>(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<u64>(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<u8>& out, u64 value)
}
static bool wasm_read_metadata_file(const String& path, std::vector<u8>& 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<u8>& 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<u8>& 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++, &section_id, 1))
{
@@ -1043,6 +1222,16 @@ static bool wasm_read_metadata_file(const String& path, std::vector<u8>& 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<u8>& 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<WasmUnitModule> unit_module(const String& source_path, String& error, WasmUnitModuleLoadProfile& profile)
std::shared_ptr<WasmUnitModule> 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::microseconds>(
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::microseconds>(
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::microseconds>(
std::chrono::steady_clock::now() - compile_start).count();
profile.serialized_cache_hit = compiled_or_cached.has_value();
if(deadline_expired())
return(nullptr);
std::vector<u8> 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<std::chrono::microseconds>(
@@ -1247,8 +1458,10 @@ public:
}
profile.read_us = (u64)std::chrono::duration_cast<std::chrono::microseconds>(
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::microseconds>(
std::chrono::steady_clock::now() - parse_start).count();
@@ -1257,6 +1470,8 @@ public:
}
profile.parse_us = (u64)std::chrono::duration_cast<std::chrono::microseconds>(
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::microseconds>(
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::microseconds>(
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::microseconds>(
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