Resolve wasm traps to unit source
This commit is contained in:
Executable
+75
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Executable
+54
@@ -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"
|
||||
Reference in New Issue
Block a user