Resolve wasm traps to unit source

This commit is contained in:
udo
2026-07-18 10:58:08 +00:00
parent eda124b15f
commit 495a8ae5f0
13 changed files with 338 additions and 17 deletions
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Extract a compact address-to-source table before a unit's DWARF is stripped."""
import argparse
import ast
import os
import re
import subprocess
DIRECTORY = re.compile(r'^include_directories\[\s*(\d+)\] = (".*")$')
FILE = re.compile(r'^file_names\[\s*(\d+)\]:$')
ROW = re.compile(r'^0x([0-9a-fA-F]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+')
def quoted(value: str) -> str:
return ast.literal_eval(value)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--dwarfdump", required=True)
parser.add_argument("--wasm", required=True)
parser.add_argument("--module", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
text = subprocess.run(
[args.dwarfdump, "--debug-line", args.wasm],
check=True,
stdout=subprocess.PIPE,
text=True,
).stdout
directories: dict[int, str] = {}
files: dict[int, tuple[str, int]] = {}
rows: list[tuple[int, int, int, int]] = []
current_file = 0
for line in text.splitlines():
if match := DIRECTORY.match(line):
directories[int(match.group(1))] = quoted(match.group(2))
continue
if match := FILE.match(line):
current_file = int(match.group(1))
continue
stripped = line.strip()
if current_file and stripped.startswith("name: "):
files[current_file] = (quoted(stripped[6:]), 0)
continue
if current_file and stripped.startswith("dir_index: "):
name, _ = files[current_file]
files[current_file] = (name, int(stripped[11:]))
continue
if match := ROW.match(line):
rows.append(tuple(int(value, 16 if index == 0 else 10) for index, value in enumerate(match.groups())))
paths: dict[int, str] = {}
for file_id, (name, directory_id) in files.items():
directory = directories.get(directory_id, "")
path = name if os.path.isabs(name) else os.path.normpath(os.path.join(directory, name))
if not os.path.isabs(path):
path = os.path.abspath(path)
if "\t" in path or "\n" in path or "\r" in path:
raise ValueError(f"source-map path contains a control character: {path!r}")
paths[file_id] = path
with open(args.output, "w", encoding="utf-8") as output:
output.write(f"UCE_SOURCE_MAP_V1\t{args.module}\n")
for file_id, path in sorted(paths.items()):
output.write(f"F\t{file_id}\t{path}\n")
for address, line, column, file_id in rows:
output.write(f"L\t{address:x}\t{file_id}\t{line}\t{column}\n")
if __name__ == "__main__":
main()
+3
View File
@@ -182,6 +182,9 @@ def main() -> int:
for needle in required:
if needle not in abi_text:
errors.append(f"uce.abi missing {needle!r}")
module_payloads = customs.get("uce.module", [])
if not module_payloads or not module_payloads[-1]:
errors.append("missing uce.module custom section")
export_names = {name for name, _ in exports}
forbidden_exports = {"uce_alloc", "uce_free"}
for name in sorted(export_names & forbidden_exports):
+17 -4
View File
@@ -15,7 +15,9 @@ ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
ROOT=$(pwd)
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
MODULE_TMP="$DEST_DIR/$PP_FN.uce-module.txt"
WASM_TMP="$DEST_DIR/$WASM_FN.tmp.$$"
MAP_TMP="$DEST_DIR/$WASM_FN.source-map.tmp.$$"
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
COMMON_FLAGS=(
@@ -33,7 +35,7 @@ COMMON_FLAGS=(
-DPLATFORM_NAME=\"wasm32-wasip1\"
)
if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ]; then
if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ] || [ ! -x "$SDK/bin/llvm-dwarfdump" ]; then
echo "wasi-sdk tools not found; set WASI_SDK" >&2
exit 1
fi
@@ -45,7 +47,7 @@ PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
mkdir -p "$DEST_DIR" >/dev/null 2>&1
trap 'rm -f "$OBJ_FN" "$ABI_TMP" "$WASM_TMP"' EXIT
trap 'rm -f "$OBJ_FN" "$ABI_TMP" "$MODULE_TMP" "$WASM_TMP" "$MAP_TMP"' EXIT
build_pch_if_needed() {
if [ "$PCH_ENABLED" = "0" ]; then
@@ -67,6 +69,7 @@ unit_abi_version=$ABI_VERSION
toolchain=$TOOLCHAIN_ID
source=$SRC_FN
EOF
printf '%s' "$(basename "$WASM_TMP")" > "$MODULE_TMP"
build_pch_if_needed
PCH_FLAGS=()
@@ -93,9 +96,19 @@ fi
--export-if-defined=__uce_once \
--export-if-defined=__uce_init
"$SDK/bin/llvm-objcopy" --strip-debug --add-section=uce.abi="$ABI_TMP" "$WASM_TMP"
python3 scripts/build_unit_source_map.py \
--dwarfdump "$SDK/bin/llvm-dwarfdump" \
--wasm "$WASM_TMP" \
--module "$(basename "$WASM_TMP")" \
--output "$MAP_TMP"
"$SDK/bin/llvm-objcopy" --strip-debug \
--add-section=uce.abi="$ABI_TMP" \
--add-section=uce.module="$MODULE_TMP" \
"$WASM_TMP"
python3 scripts/check_unit_wasm.py "$WASM_TMP" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
mv "$MAP_TMP" "$DEST_DIR/$WASM_FN.source-map"
mv "$WASM_TMP" "$DEST_DIR/$WASM_FN"
rm -f "$OBJ_FN" "$ABI_TMP"
rm -f "$OBJ_FN" "$ABI_TMP" "$MODULE_TMP"
+1 -1
View File
@@ -53,7 +53,7 @@ require_command() {
verify_tree() {
local root="$1"
for tool in clang++ wasm-ld llvm-objcopy llvm-nm; do
for tool in clang++ wasm-ld llvm-objcopy llvm-nm llvm-dwarfdump; do
if [[ ! -x "$root/bin/$tool" ]]; then
echo "Missing WASI SDK tool: $root/bin/$tool" >&2
return 1
+1
View File
@@ -77,5 +77,6 @@ if [[ "$action" == "run" ]]; then
scripts/test_raw_http_request_log.sh
scripts/test_component_resolution_ttl.sh
scripts/test_unit_export_surface.sh
scripts/test_wasm_source_locations.sh
scripts/test_socket_activation.sh
fi
+15 -2
View File
@@ -50,7 +50,7 @@ fi
entry_wasm="$artifact_dir/entry.uce.wasm"
named_wasm="$artifact_dir/named.uce.wasm"
python3 - "$entry_wasm" "$named_wasm" <<'PY'
python3 - "$entry_wasm" "$named_wasm" "$absolute_source_dir" <<'PY'
import sys
from pathlib import Path
@@ -60,9 +60,10 @@ cases = [
(Path(sys.argv[1]), {"__wasm_call_ctors", "__uce_set_current_request", "__uce_cli", "visibility_shared"}, b"uce-private-unused-marker-8f61d2"),
(Path(sys.argv[2]), {"__wasm_call_ctors", "__uce_set_current_request", "__uce_component_NAMED", "named_shared"}, b"uce-named-unused-marker-4ae973"),
]
source_dir = Path(sys.argv[3])
for path, allowed, unused_marker in cases:
data = path.read_bytes()
_, imports, exports = collect(path)
customs, imports, exports = collect(path)
names = {name for name, _ in exports}
unexpected = names - allowed
missing = allowed - names
@@ -74,6 +75,18 @@ for path, allowed, unused_marker in cases:
raise SystemExit(f"{path}: retained {len(imports)} imports (expected fewer than 40)")
if path.stat().st_size >= 1024 * 1024:
raise SystemExit(f"{path}: artifact is {path.stat().st_size} bytes (expected under 1 MiB)")
source_map = Path(str(path) + ".source-map")
if not source_map.is_file() or source_map.stat().st_size >= 256 * 1024:
raise SystemExit(f"{path}: missing or oversized source map")
module = customs["uce.module"][-1].decode()
lines = source_map.read_text().splitlines()
if not lines or lines[0] != f"UCE_SOURCE_MAP_V1\t{module}":
raise SystemExit(f"{path}: source map does not match wasm module identity")
expected_source = str(source_dir / path.name.removesuffix(".wasm"))
if not any(line.startswith("F\t") and line.endswith("\t" + expected_source) for line in lines):
raise SystemExit(f"{path}: source map does not identify {expected_source}")
if not any(line.startswith("L\t") for line in lines):
raise SystemExit(f"{path}: source map has no address rows")
PY
echo "unit export surface passed"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="wasm-source-location-test-$$"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
bin_directory="${UCE_TEST_BIN_DIRECTORY:-/tmp/uce/work}"
if [[ -r /etc/uce/settings.cfg ]]; then
configured_site_directory=$(awk -F= '/^[[:space:]]*HTTP_DOCUMENT_ROOT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
configured_bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
[[ -n "${UCE_TEST_SITE_DIRECTORY:-}" ]] || site_directory="${configured_site_directory:-$site_directory}"
[[ -n "${UCE_TEST_BIN_DIRECTORY:-}" ]] || bin_directory="${configured_bin_directory:-$bin_directory}"
fi
source_dir="$site_directory/$test_name"
absolute_source_dir=""
artifact_dir=""
cleanup() {
rm -rf "$source_dir"
[[ -z "$artifact_dir" ]] || rm -rf "$artifact_dir"
}
trap cleanup EXIT
mkdir -p "$source_dir"
absolute_source_dir=$(realpath "$source_dir")
artifact_dir="$bin_directory$absolute_source_dir"
printf '%s\n' 'CLI(Request& context) { __builtin_trap(); }' >"$source_dir/entry.uce"
set +e
output=$(scripts/uce-cli "/$test_name/entry.uce" 2>&1)
status=$?
set -e
if [[ $status -eq 0 ]]; then
echo "trapping wasm unit unexpectedly succeeded" >&2
exit 1
fi
expected="$absolute_source_dir/entry.uce:1:25"
if [[ "$output" != *'wasm `unreachable` instruction executed'* || "$output" != *"source locations:"* || "$output" != *"$expected"* ]]; then
echo "wasm trap omitted its source location:" >&2
echo "$output" >&2
exit 1
fi
rm "$artifact_dir/entry.uce.wasm.source-map"
set +e
without_map=$(scripts/uce-cli "/$test_name/entry.uce" 2>&1)
status=$?
set -e
if [[ $status -eq 0 || "$without_map" != *'wasm `unreachable` instruction executed'* || "$without_map" == *"source locations:"* ]]; then
echo "missing-map trap fallback failed:" >&2
echo "$without_map" >&2
exit 1
fi
echo "wasm source locations passed"