update
This commit is contained in:
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
umask 022
|
||||
|
||||
spec=$PRE15_DUT/tests/pre15/fixtures/B01-g3-archives.json
|
||||
helper=$PRE15_DUT/tests/pre15/fixtures/g3.py
|
||||
metadata_archive=$PRE15_DUT/tests/results/manual/2026-08-08T2337Z-metadata-vfs/prepare-fixtures.sh
|
||||
final_archive=$PRE15_DUT/tests/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.sh
|
||||
|
||||
for tool in mkfs.erofs fsck.erofs dump.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing G3 equivalence tool: $tool"
|
||||
done
|
||||
test "$(id -u)" -eq 0 || \
|
||||
pre15_infra_blocked 'root is required for deterministic device-node fixtures'
|
||||
|
||||
python3 - "$spec" "$PRE15_DUT" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
spec = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
dut = Path(sys.argv[2])
|
||||
for item in spec["archives"]:
|
||||
path = dut / item["path"]
|
||||
data = path.read_bytes()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
lines = len(data.splitlines())
|
||||
if digest != item["sha256"] or lines != item["lines"]:
|
||||
raise SystemExit(
|
||||
f"{item['id']}: hash/line mismatch {digest}/{lines}, "
|
||||
f"expected {item['sha256']}/{item['lines']}"
|
||||
)
|
||||
PY
|
||||
|
||||
pre15_record_fixture g3-archive-spec "$spec"
|
||||
pre15_record_fixture g3-stable-helper "$helper"
|
||||
pre15_record_fixture g3-archive-metadata "$metadata_archive"
|
||||
pre15_record_fixture g3-archive-final "$final_archive"
|
||||
|
||||
mkdir "$PRE15_CASE_TMP/archive-metadata-source" "$PRE15_CASE_TMP/archive-metadata"
|
||||
rmdir "$PRE15_CASE_TMP/archive-metadata-source" "$PRE15_CASE_TMP/archive-metadata"
|
||||
FIXTURE_DIR="$PRE15_CASE_TMP/archive-metadata-source" \
|
||||
ARTIFACT_DIR="$PRE15_CASE_TMP/archive-metadata" "$metadata_archive"
|
||||
find "$PRE15_CASE_TMP/archive-metadata-source/namei/wide" \
|
||||
-mindepth 1 -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort \
|
||||
> "$PRE15_CASE_TMP/archive-metadata/expected-wide.txt"
|
||||
|
||||
python3 -B "$helper" make-metadata \
|
||||
--source "$PRE15_CASE_TMP/stable-a-metadata-source" \
|
||||
--output "$PRE15_CASE_TMP/stable-a-metadata"
|
||||
python3 -B "$helper" make-metadata \
|
||||
--source "$PRE15_CASE_TMP/stable-b-metadata-source" \
|
||||
--output "$PRE15_CASE_TMP/stable-b-metadata"
|
||||
mkdir -p "$PRE15_RUN_DIR/artifacts"
|
||||
python3 -B "$helper" compare-set \
|
||||
--left-source "$PRE15_CASE_TMP/archive-metadata-source" \
|
||||
--left-artifacts "$PRE15_CASE_TMP/archive-metadata" \
|
||||
--right-source "$PRE15_CASE_TMP/stable-a-metadata-source" \
|
||||
--right-artifacts "$PRE15_CASE_TMP/stable-a-metadata" \
|
||||
--output "$PRE15_RUN_DIR/artifacts/metadata-archive-equivalence.json"
|
||||
python3 -B "$helper" compare-set \
|
||||
--left-source "$PRE15_CASE_TMP/stable-a-metadata-source" \
|
||||
--left-artifacts "$PRE15_CASE_TMP/stable-a-metadata" \
|
||||
--right-source "$PRE15_CASE_TMP/stable-b-metadata-source" \
|
||||
--right-artifacts "$PRE15_CASE_TMP/stable-b-metadata" \
|
||||
--output "$PRE15_RUN_DIR/artifacts/metadata-repeat-equivalence.json"
|
||||
|
||||
FIXTURE_DIR="$PRE15_CASE_TMP/archive-final-source" \
|
||||
ARTIFACT_DIR="$PRE15_CASE_TMP/archive-final" "$final_archive"
|
||||
python3 -B "$helper" make-final \
|
||||
--source "$PRE15_CASE_TMP/stable-a-final-source" \
|
||||
--output "$PRE15_CASE_TMP/stable-a-final"
|
||||
python3 -B "$helper" make-final \
|
||||
--source "$PRE15_CASE_TMP/stable-b-final-source" \
|
||||
--output "$PRE15_CASE_TMP/stable-b-final"
|
||||
python3 -B "$helper" compare-set \
|
||||
--left-source "$PRE15_CASE_TMP/archive-final-source" \
|
||||
--left-artifacts "$PRE15_CASE_TMP/archive-final" \
|
||||
--right-source "$PRE15_CASE_TMP/stable-a-final-source" \
|
||||
--right-artifacts "$PRE15_CASE_TMP/stable-a-final" \
|
||||
--output "$PRE15_RUN_DIR/artifacts/final-archive-equivalence.json"
|
||||
python3 -B "$helper" compare-set \
|
||||
--left-source "$PRE15_CASE_TMP/stable-a-final-source" \
|
||||
--left-artifacts "$PRE15_CASE_TMP/stable-a-final" \
|
||||
--right-source "$PRE15_CASE_TMP/stable-b-final-source" \
|
||||
--right-artifacts "$PRE15_CASE_TMP/stable-b-final" \
|
||||
--output "$PRE15_RUN_DIR/artifacts/final-repeat-equivalence.json"
|
||||
|
||||
for evidence in \
|
||||
metadata-archive-equivalence.json metadata-repeat-equivalence.json \
|
||||
final-archive-equivalence.json final-repeat-equivalence.json; do
|
||||
pre15_record_fixture "g3-$evidence" "$PRE15_RUN_DIR/artifacts/$evidence"
|
||||
done
|
||||
|
||||
python3 - "$PRE15_RUN_DIR/artifacts" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
names = (
|
||||
"metadata-archive-equivalence.json",
|
||||
"metadata-repeat-equivalence.json",
|
||||
"final-archive-equivalence.json",
|
||||
"final-repeat-equivalence.json",
|
||||
)
|
||||
for name in names:
|
||||
result = json.loads((root / name).read_text(encoding="ascii"))
|
||||
if result["status"] != "PASS":
|
||||
raise SystemExit(f"{name}: {result['status']}")
|
||||
print("G3 archived=548 lines; source/artifact bytes and expectations are equivalent")
|
||||
PY
|
||||
|
||||
pre15_target_reached
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
controls=$PRE15_DUT/tests/pre15/fixtures/B01-runner-controls.json
|
||||
pre15_record_fixture runner-control-contract "$controls"
|
||||
pre15_record_fixture evidence-schema "$PRE15_DUT/tests/pre15/EVIDENCE-SCHEMA.json"
|
||||
|
||||
command -v qemu-system-x86_64 >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked 'qemu-system-x86_64 is required for the early-exit control'
|
||||
command -v ssh >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked 'ssh is required for the connection-failure control'
|
||||
|
||||
pre15_control_run "$PRE15_CASE_TMP/known-good" 5 \
|
||||
sh -c 'printf "reached\n" > "$PRE15_TARGET_MARKER_FILE"'
|
||||
pre15_control_run "$PRE15_CASE_TMP/dut-mismatch" 5 \
|
||||
sh -c 'printf "reached\n" > "$PRE15_TARGET_MARKER_FILE"; exit 10'
|
||||
pre15_control_run "$PRE15_CASE_TMP/guest-command-failure" 5 sh -c 'exit 1'
|
||||
pre15_control_run "$PRE15_CASE_TMP/ssh-failure" 5 sh -c \
|
||||
'if ssh -o BatchMode=yes -o ConnectTimeout=1 -p 1 [email protected] true >/dev/null 2>&1; then exit 20; fi; exit 21'
|
||||
pre15_control_run "$PRE15_CASE_TMP/qemu-early-exit" 5 sh -c \
|
||||
'qemu-system-x86_64 --pre15-invalid-option >/dev/null 2>&1; qemu_rc=$?; test "$qemu_rc" -ne 0 || exit 20; exit 21'
|
||||
|
||||
timeout_pid_file=$PRE15_CASE_TMP/timeout.pid
|
||||
export timeout_pid_file
|
||||
pre15_control_run "$PRE15_CASE_TMP/timeout" 1 sh -c \
|
||||
'printf "%s\n" "$$" > "$timeout_pid_file"; exec sleep 30'
|
||||
timeout_pid=$(cat "$timeout_pid_file")
|
||||
if kill -0 "$timeout_pid" 2>/dev/null; then
|
||||
pre15_runner_fail "timeout control left PID $timeout_pid alive"
|
||||
fi
|
||||
|
||||
pre15_control_run "$PRE15_CASE_TMP/owned-cleanup" 5 sh -c '
|
||||
sleep 30 >/dev/null 2>&1 &
|
||||
owned_pid=$!
|
||||
printf "pid\t%s\tselftest-owned-sleep\n" "$owned_pid" >> "$PRE15_OWNERSHIP_FILE"
|
||||
printf "%s\n" "$owned_pid" > "$PRE15_RUN_DIR/owned.pid"
|
||||
printf "reached\n" > "$PRE15_TARGET_MARKER_FILE"
|
||||
'
|
||||
owned_pid=$(cat "$PRE15_CASE_TMP/owned-cleanup/owned.pid")
|
||||
if kill -0 "$owned_pid" 2>/dev/null; then
|
||||
pre15_runner_fail "owned cleanup left PID $owned_pid alive"
|
||||
fi
|
||||
|
||||
outside_path=$PRE15_CASE_TMP/cleanup-must-refuse
|
||||
printf 'owned by outer selftest\n' > "$outside_path"
|
||||
export outside_path
|
||||
pre15_control_run "$PRE15_CASE_TMP/cleanup-failure" 5 sh -c '
|
||||
printf "path\t%s\toutside-control-boundary\n" "$outside_path" >> "$PRE15_OWNERSHIP_FILE"
|
||||
printf "reached\n" > "$PRE15_TARGET_MARKER_FILE"
|
||||
'
|
||||
test -f "$outside_path" || \
|
||||
pre15_runner_fail 'cleanup safety control deleted a non-owned path'
|
||||
|
||||
python3 - "$controls" "$PRE15_CASE_TMP" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
contract = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))["controls"]
|
||||
root = Path(sys.argv[2])
|
||||
for name, expected in contract.items():
|
||||
actual = (root / name / "result.tsv").read_text(encoding="ascii").rstrip("\n").split("\t")
|
||||
if actual != expected:
|
||||
raise SystemExit(f"{name}: {actual!r}, expected {expected!r}")
|
||||
print(f"{name}: status={actual[0]} origin={actual[1]} cleanup={actual[2]} timeout={actual[3]}")
|
||||
PY
|
||||
|
||||
mkdir -p "$PRE15_RUN_DIR/artifacts/controls"
|
||||
for control in \
|
||||
cleanup-failure dut-mismatch guest-command-failure known-good \
|
||||
owned-cleanup qemu-early-exit ssh-failure timeout; do
|
||||
mkdir "$PRE15_RUN_DIR/artifacts/controls/$control"
|
||||
cp "$PRE15_CASE_TMP/$control/result.tsv" \
|
||||
"$PRE15_CASE_TMP/$control/stdout.log" \
|
||||
"$PRE15_CASE_TMP/$control/stderr.log" \
|
||||
"$PRE15_CASE_TMP/$control/cleanup.log" \
|
||||
"$PRE15_RUN_DIR/artifacts/controls/$control/"
|
||||
done
|
||||
|
||||
pre15_target_reached
|
||||
Executable
+319
@@ -0,0 +1,319 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_FREEBSD_SRC:?PRE15_FREEBSD_SRC is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
probe=$PRE15_DUT/tests/pre15/probes/ondisk_layout.c
|
||||
freebsd_header=$PRE15_DUT/src/erofs_fs.h
|
||||
internal_header=$PRE15_DUT/src/internal.h
|
||||
linux_header=$PRE15_ROOT/src-linux/erofs_fs.h
|
||||
sys=$PRE15_FREEBSD_SRC/sys
|
||||
target=x86_64-unknown-freebsd15.0
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
include_root=$PRE15_CASE_TMP/include
|
||||
|
||||
for tool in clang python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B02 host tool: $tool"
|
||||
done
|
||||
test -d "$sys/amd64/include" || \
|
||||
pre15_infra_blocked "FreeBSD amd64 headers are absent: $sys"
|
||||
|
||||
pre15_record_fixture b02-layout-probe "$probe"
|
||||
pre15_record_fixture b02-freebsd-ondisk-header "$freebsd_header"
|
||||
pre15_record_fixture b02-freebsd-internal-header "$internal_header"
|
||||
pre15_record_fixture b02-linux-ondisk-header "$linux_header"
|
||||
|
||||
mkdir -p "$artifacts" "$include_root"
|
||||
ln -s "$sys/amd64/include" "$include_root/machine"
|
||||
ln -s "$sys/x86/include" "$include_root/x86"
|
||||
resource_include=$(clang -print-resource-dir)/include
|
||||
pre15_target_reached
|
||||
|
||||
if clang --target="$target" -fsyntax-only -std=gnu17 -Wall -Wextra -Werror \
|
||||
-nostdinc -isystem "$resource_include" \
|
||||
-isystem "$PRE15_FREEBSD_SRC/include" -isystem "$include_root" \
|
||||
-isystem "$sys" -Xclang -fdump-record-layouts "$probe" \
|
||||
> "$artifacts/freebsd-record-layouts.txt" \
|
||||
2> "$artifacts/freebsd-layout.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'FreeBSD ondisk layout/type/macro oracle failed'
|
||||
fi
|
||||
if clang -DB02_LINUX_REFERENCE -fsyntax-only -std=gnu17 \
|
||||
-Wall -Wextra -Werror -Xclang -fdump-record-layouts "$probe" \
|
||||
> "$artifacts/linux-record-layouts.txt" \
|
||||
2> "$artifacts/linux-layout.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'Linux reference layout/type/macro oracle failed'
|
||||
fi
|
||||
|
||||
clang --target="$target" -E -dM -std=gnu17 -nostdinc \
|
||||
-isystem "$resource_include" -isystem "$PRE15_FREEBSD_SRC/include" \
|
||||
-isystem "$include_root" -isystem "$sys" "$probe" | \
|
||||
awk '$2 ~ /^(EROFS_|Z_EROFS_)/ { print }' | LC_ALL=C sort \
|
||||
> "$artifacts/freebsd-macros.txt"
|
||||
clang -DB02_LINUX_REFERENCE -E -dM -std=gnu17 "$probe" | \
|
||||
awk '$2 ~ /^(EROFS_|Z_EROFS_)/ { print }' | LC_ALL=C sort \
|
||||
> "$artifacts/linux-macros.txt"
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$artifacts/static-check.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
output = Path(sys.argv[2])
|
||||
dut = root / "repo-pre-15"
|
||||
src = dut / "src"
|
||||
ondisk = (src / "erofs_fs.h").read_text(encoding="utf-8")
|
||||
internal = (src / "internal.h").read_text(encoding="utf-8")
|
||||
linux = (root / "src-linux/erofs_fs.h").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def require_once(source: str, token: str) -> None:
|
||||
count = source.count(token)
|
||||
if count != 1:
|
||||
raise SystemExit(f"expected one occurrence of {token!r}, found {count}")
|
||||
|
||||
|
||||
def require_order(source: str, tokens: tuple[str, ...], label: str) -> None:
|
||||
positions = []
|
||||
for token in tokens:
|
||||
require_once(source, token)
|
||||
positions.append(source.index(token))
|
||||
if positions != sorted(positions):
|
||||
raise SystemExit(f"{label} section order mismatch: {tokens!r}")
|
||||
|
||||
|
||||
ondisk_markers = (
|
||||
"struct erofs_deviceslot {",
|
||||
"struct erofs_super_block {",
|
||||
"EROFS_INODE_CHUNK_BASED",
|
||||
"#define EROFS_CHUNK_FORMAT_BLKBITS_MASK",
|
||||
"#define EROFS_INODE_LAYOUT_COMPACT",
|
||||
"struct erofs_inode_chunk_info {",
|
||||
"struct erofs_inode_compact {",
|
||||
"struct erofs_inode_extended {",
|
||||
"struct erofs_xattr_ibody_header {",
|
||||
"struct erofs_xattr_entry {",
|
||||
"struct erofs_xattr_long_prefix {",
|
||||
"#define EROFS_NULL_ADDR",
|
||||
"struct erofs_inode_chunk_index {",
|
||||
"#define EROFS_DIRENT_NID_METABOX_BIT",
|
||||
"struct erofs_dirent {",
|
||||
"#define EROFS_NAME_LEN",
|
||||
"struct z_erofs_lz4_cfgs {",
|
||||
"struct z_erofs_map_header {",
|
||||
"Z_EROFS_LCLUSTER_TYPE_PLAIN",
|
||||
"struct z_erofs_lcluster_index {",
|
||||
"struct z_erofs_extent {",
|
||||
)
|
||||
require_order(ondisk, ondisk_markers, "FreeBSD ondisk")
|
||||
require_order(linux, ondisk_markers, "Linux ondisk")
|
||||
|
||||
aliases = {
|
||||
"EROFS_INODE_LAYOUT_COMPACT": "0",
|
||||
"EROFS_INODE_LAYOUT_EXTENDED": "1",
|
||||
"EROFS_INODE_LAYOUT_PLAIN": "EROFS_INODE_FLAT_PLAIN",
|
||||
}
|
||||
for name, value in aliases.items():
|
||||
pattern = rf"^#define[ \t]+{name}[ \t]+{value}$"
|
||||
if len(re.findall(pattern, ondisk, re.MULTILINE)) != 1:
|
||||
raise SystemExit(f"layout alias mismatch: {name}")
|
||||
|
||||
if not re.search(
|
||||
r"^#define[ \t]+EROFS_DEVT_SLOT_SIZE[ \t]+"
|
||||
r"sizeof\(struct erofs_deviceslot\)$",
|
||||
ondisk,
|
||||
re.MULTILINE,
|
||||
):
|
||||
raise SystemExit("EROFS_DEVT_SLOT_SIZE is not derived from its record")
|
||||
if re.search(r"^#if.*EROFS_DEVT_SLOT_SIZE", ondisk, re.MULTILINE):
|
||||
raise SystemExit("sizeof-based EROFS_DEVT_SLOT_SIZE is used in #if")
|
||||
|
||||
internal_markers = (
|
||||
"struct erofs_mount_opts {",
|
||||
"struct erofs_sb_lz4_info {",
|
||||
"struct erofs_device_info {",
|
||||
"struct erofs_xattr_prefix_item {",
|
||||
"struct erofs_zextent_cache {",
|
||||
"struct erofs_mount {",
|
||||
"#define EROFS_FEATURE_FUNCS",
|
||||
"EROFS_FEATURE_FUNCS(lz4_0padding",
|
||||
"struct erofs_node {",
|
||||
"struct erofs_fid {",
|
||||
"erofs_inode_version(unsigned int ifmt)",
|
||||
"#define EROFS_MAP_MAPPED",
|
||||
"struct erofs_map_blocks {",
|
||||
"struct erofs_map_dev {",
|
||||
"/* Buffer and device I/O. */",
|
||||
"/* Logical mapping and file data. */",
|
||||
"/* Inode and vnode lifecycle. */",
|
||||
"/* Directory operations. */",
|
||||
"/* Compressed mapping and data. */",
|
||||
"/* Compression configuration. */",
|
||||
"/* VOP vectors. */",
|
||||
)
|
||||
require_order(internal, internal_markers, "FreeBSD internal")
|
||||
if re.search(r"return[ \t]+-E[A-Z0-9_]+", internal):
|
||||
raise SystemExit("Linux negative-errno convention entered FreeBSD internal.h")
|
||||
|
||||
vnops = (src / "erofs_vnops.c").read_text(encoding="utf-8")
|
||||
super_source = (src / "super.c").read_text(encoding="utf-8")
|
||||
inode = (src / "inode.c").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def initializer(source: str, declaration: str, prefix: str) -> list[tuple[str, str]]:
|
||||
match = re.search(re.escape(declaration) + r"\s*=\s*\{(.*?)\n\};", source, re.DOTALL)
|
||||
if not match:
|
||||
raise SystemExit(f"initializer absent: {declaration}")
|
||||
return re.findall(
|
||||
rf"^\s*\.({prefix}_[A-Za-z0-9_]+)\s*=\s*([^,]+),",
|
||||
match.group(1),
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
vnode = initializer(vnops, "struct vop_vector erofs_vnodeops", "vop")
|
||||
fifo = initializer(vnops, "struct vop_vector erofs_fifoops", "vop")
|
||||
vfs = initializer(super_source, "static struct vfsops erofs_vfsops", "vfs")
|
||||
expected_vnode = [
|
||||
("vop_default", "&default_vnodeops"),
|
||||
("vop_inactive", "erofs_inactive"),
|
||||
("vop_reclaim", "erofs_reclaim"),
|
||||
("vop_lookup", "vfs_cache_lookup"),
|
||||
("vop_cachedlookup", "erofs_lookup"),
|
||||
("vop_readdir", "erofs_readdir"),
|
||||
("vop_readlink", "erofs_readlink"),
|
||||
("vop_open", "erofs_open"),
|
||||
("vop_read", "erofs_read"),
|
||||
("vop_bmap", "erofs_bmap"),
|
||||
("vop_getpages", "vnode_pager_local_getpages"),
|
||||
("vop_getpages_async", "vnode_pager_local_getpages_async"),
|
||||
("vop_getattr", "erofs_getattr"),
|
||||
("vop_setattr", "erofs_setattr"),
|
||||
("vop_access", "erofs_access"),
|
||||
("vop_pathconf", "erofs_pathconf"),
|
||||
("vop_getextattr", "erofs_getextattr"),
|
||||
("vop_listextattr", "erofs_listextattr"),
|
||||
("vop_deleteextattr", "erofs_deleteextattr"),
|
||||
("vop_setextattr", "erofs_setextattr"),
|
||||
("vop_getacl", "erofs_vop_getacl"),
|
||||
("vop_aclcheck", "erofs_aclcheck"),
|
||||
("vop_setacl", "erofs_setacl"),
|
||||
("vop_vptofh", "erofs_vptofh"),
|
||||
]
|
||||
expected_fifo = [
|
||||
("vop_default", "&fifo_specops"),
|
||||
("vop_access", "erofs_access"),
|
||||
("vop_aclcheck", "erofs_aclcheck"),
|
||||
("vop_deleteextattr", "erofs_deleteextattr"),
|
||||
("vop_getacl", "erofs_vop_getacl"),
|
||||
("vop_getextattr", "erofs_getextattr"),
|
||||
("vop_getattr", "erofs_getattr"),
|
||||
("vop_listextattr", "erofs_listextattr"),
|
||||
("vop_pathconf", "erofs_pathconf"),
|
||||
("vop_reclaim", "erofs_reclaim"),
|
||||
("vop_setacl", "erofs_setacl"),
|
||||
("vop_setattr", "erofs_setattr"),
|
||||
("vop_setextattr", "erofs_setextattr"),
|
||||
("vop_vptofh", "erofs_vptofh"),
|
||||
]
|
||||
expected_vfs = [
|
||||
("vfs_fhtovp", "erofs_fhtovp"),
|
||||
("vfs_mount", "erofs_mount"),
|
||||
("vfs_root", "erofs_root"),
|
||||
("vfs_statfs", "erofs_statfs"),
|
||||
("vfs_unmount", "erofs_unmount"),
|
||||
("vfs_vget", "erofs_vgetf"),
|
||||
]
|
||||
if vnode != expected_vnode or fifo != expected_fifo or vfs != expected_vfs:
|
||||
raise SystemExit("VOP/VFS callback targets changed")
|
||||
|
||||
hash_calls = re.findall(
|
||||
r"vfs_hash_(?:get|insert)\s*\([^;]*?erofs_vfs_hash_cmp", inode, re.DOTALL
|
||||
)
|
||||
if len(hash_calls) != 2:
|
||||
raise SystemExit(f"hash comparator callsites changed: {len(hash_calls)}")
|
||||
|
||||
decompressors = []
|
||||
for path in sorted(src.glob("decompressor*.c")):
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
match = re.match(r"\s*\.(config|decompress)\s*=\s*([^,]+),", line)
|
||||
if match:
|
||||
decompressors.append((path.name, match.group(1), match.group(2)))
|
||||
expected_decompressors = [
|
||||
("decompressor.c", "decompress", "z_erofs_transform_plain"),
|
||||
("decompressor.c", "decompress", "z_erofs_transform_plain"),
|
||||
("decompressor.c", "config", "z_erofs_load_lz4_config"),
|
||||
("decompressor.c", "decompress", "z_erofs_lz4_decompress"),
|
||||
("decompressor_deflate.c", "config", "z_erofs_load_deflate_config"),
|
||||
("decompressor_deflate.c", "decompress", "z_erofs_deflate_decompress"),
|
||||
("decompressor_lzma.c", "config", "z_erofs_load_lzma_config"),
|
||||
("decompressor_lzma.c", "decompress", "z_erofs_lzma_decompress"),
|
||||
("decompressor_zstd.c", "config", "z_erofs_load_zstd_config"),
|
||||
("decompressor_zstd.c", "decompress", "z_erofs_zstd_decompress"),
|
||||
]
|
||||
if decompressors != expected_decompressors:
|
||||
raise SystemExit("decompressor callback targets changed")
|
||||
|
||||
result = {
|
||||
"status": "PASS",
|
||||
"source_symbol_denominator": {
|
||||
"baseline_physical": 464,
|
||||
"baseline_unique": 389,
|
||||
"b02_macro_delta": 3,
|
||||
"expected_physical": 467,
|
||||
"expected_unique": 392,
|
||||
"added_names": sorted(aliases),
|
||||
"kld_symbol_delta": 0,
|
||||
},
|
||||
"callbacks": {
|
||||
"vnode_slots": len(vnode),
|
||||
"fifo_slots": len(fifo),
|
||||
"vfs_slots": len(vfs),
|
||||
"hash_comparator_callsites": len(hash_calls),
|
||||
"decompressor_fields": len(decompressors),
|
||||
},
|
||||
"freebsd_adaptations": [
|
||||
"packed ondisk records",
|
||||
"little-endian typedefs",
|
||||
"explicit supported incompat mask",
|
||||
"FreeBSD VOP/VFS types",
|
||||
"positive errno convention",
|
||||
],
|
||||
}
|
||||
with output.open("x", encoding="ascii") as stream:
|
||||
json.dump(result, stream, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
print(
|
||||
"static PASS source-symbols=464+3 callbacks=24+14/6 hash=2 decompress=10"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'header order, symbol denominator, or callback oracle failed'
|
||||
fi
|
||||
|
||||
sha256sum \
|
||||
"$artifacts/freebsd-record-layouts.txt" \
|
||||
"$artifacts/linux-record-layouts.txt" \
|
||||
"$artifacts/freebsd-macros.txt" \
|
||||
"$artifacts/linux-macros.txt" \
|
||||
"$artifacts/static-check.json" \
|
||||
> "$artifacts/SHA256SUMS"
|
||||
|
||||
printf 'layout PASS FreeBSD and Linux size/offset/alignment/type/macro oracles\n'
|
||||
Executable
+190
@@ -0,0 +1,190 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
internal=$PRE15_DUT/src/internal.h
|
||||
data=$PRE15_DUT/src/data.c
|
||||
inode=$PRE15_DUT/src/inode.c
|
||||
super=$PRE15_DUT/src/super.c
|
||||
zmap=$PRE15_DUT/src/zmap.c
|
||||
linux_internal=$PRE15_ROOT/src-linux/internal.h
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B05 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b05-freebsd-internal "$internal"
|
||||
pre15_record_fixture b05-freebsd-data "$data"
|
||||
pre15_record_fixture b05-freebsd-inode "$inode"
|
||||
pre15_record_fixture b05-freebsd-super "$super"
|
||||
pre15_record_fixture b05-freebsd-zmap "$zmap"
|
||||
pre15_record_fixture b05-linux-internal "$linux_internal"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$artifacts/field-check.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
output = Path(sys.argv[2])
|
||||
dut_src = root / "repo-pre-15" / "src"
|
||||
internal = (dut_src / "internal.h").read_text(encoding="utf-8")
|
||||
data = (dut_src / "data.c").read_text(encoding="utf-8")
|
||||
inode = (dut_src / "inode.c").read_text(encoding="utf-8")
|
||||
super_source = (dut_src / "super.c").read_text(encoding="utf-8")
|
||||
zmap = (dut_src / "zmap.c").read_text(encoding="utf-8")
|
||||
linux = (root / "src-linux" / "internal.h").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def struct_body(source: str, name: str) -> str:
|
||||
match = re.search(
|
||||
rf"^struct {re.escape(name)} \{{\n(?P<body>.*?)^\}};",
|
||||
source,
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
if not match:
|
||||
raise SystemExit(f"missing struct {name}")
|
||||
return match.group("body")
|
||||
|
||||
|
||||
def fields(source: str, name: str) -> list[str]:
|
||||
result = []
|
||||
for line in struct_body(source, name).splitlines():
|
||||
declaration = line.strip()
|
||||
if not declaration or declaration.startswith("/*"):
|
||||
continue
|
||||
match = re.search(r"([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[[^]]+\])?;$", declaration)
|
||||
if not match:
|
||||
raise SystemExit(f"unparsed field in struct {name}: {declaration!r}")
|
||||
result.append(match.group(1))
|
||||
return result
|
||||
|
||||
|
||||
freebsd_device = fields(internal, "erofs_device_info")
|
||||
linux_device = fields(linux, "erofs_device_info")
|
||||
expected_device = [
|
||||
"devvp",
|
||||
"dev",
|
||||
"cp",
|
||||
"mediasize",
|
||||
"sectorsize",
|
||||
"blocks",
|
||||
"uniaddr",
|
||||
]
|
||||
if freebsd_device != expected_device:
|
||||
raise SystemExit(f"FreeBSD device field order mismatch: {freebsd_device!r}")
|
||||
if [field for field in linux_device if field in {"blocks", "uniaddr"}] != [
|
||||
"blocks",
|
||||
"uniaddr",
|
||||
]:
|
||||
raise SystemExit(f"Linux device common-field order mismatch: {linux_device!r}")
|
||||
|
||||
freebsd_map = fields(internal, "erofs_map_dev")
|
||||
linux_map = fields(linux, "erofs_map_dev")
|
||||
expected_map = ["m_dif", "m_pa", "m_deviceid", "m_plen"]
|
||||
if freebsd_map != expected_map:
|
||||
raise SystemExit(f"FreeBSD map field order mismatch: {freebsd_map!r}")
|
||||
if [field for field in linux_map if field in {"m_dif", "m_pa", "m_deviceid"}] != [
|
||||
"m_dif",
|
||||
"m_pa",
|
||||
"m_deviceid",
|
||||
]:
|
||||
raise SystemExit(f"Linux map common-field order mismatch: {linux_map!r}")
|
||||
|
||||
inode_fields = fields(internal, "erofs_inode")
|
||||
for removed in ("ino", "inline_data"):
|
||||
if removed in inode_fields:
|
||||
raise SystemExit(f"removed inode field remains: {removed}")
|
||||
if "m_sbi" in freebsd_map:
|
||||
raise SystemExit("removed map back-pointer remains")
|
||||
|
||||
removed_uses = {
|
||||
"map back-pointer": sum(
|
||||
source.count("m_sbi") for source in (internal, data, inode, super_source, zmap)
|
||||
),
|
||||
"saved ondisk ino": len(re.findall(r"\bvi->ino\b", inode)),
|
||||
"inline-data cache": len(re.findall(r"\bvi->inline_data\b", inode)),
|
||||
}
|
||||
if any(removed_uses.values()):
|
||||
raise SystemExit(f"removed field use remains: {removed_uses!r}")
|
||||
|
||||
helper = re.search(
|
||||
r"erofs_fill_from_devinfo\(struct erofs_map_dev \*map,\n"
|
||||
r" struct erofs_device_info \*dif, erofs_off_t pa\)",
|
||||
data,
|
||||
)
|
||||
if not helper:
|
||||
raise SystemExit("map helper still accepts the removed super back-pointer")
|
||||
if len(re.findall(r"erofs_fill_from_devinfo\(", data)) != 4:
|
||||
raise SystemExit("unexpected map helper definition/call count")
|
||||
|
||||
initializer = re.search(
|
||||
r"map = \(struct erofs_map_dev\) \{\n"
|
||||
r"\t\t\.m_pa = off,\n"
|
||||
r"\t\t\.m_deviceid = device_id,\n"
|
||||
r"\t\t\.m_plen = len,\n"
|
||||
r"\t\};",
|
||||
data,
|
||||
)
|
||||
if not initializer:
|
||||
raise SystemExit("erofs_map_dev initializer order mismatch")
|
||||
|
||||
required_markers = {
|
||||
"raw nid identity": "vi->nid = nid;",
|
||||
"layout source": "vi->datalayout = erofs_inode_datalayout(ifmt);",
|
||||
"device vnode": "struct vnode *devvp;",
|
||||
"GEOM consumer": "struct g_consumer *cp;",
|
||||
"provider media size": "uint64_t mediasize;",
|
||||
"provider sector size": "uint32_t sectorsize;",
|
||||
"future volume label": "char volume_name[17];",
|
||||
"deferred LZ4 state": "struct erofs_sb_lz4_info lz4;",
|
||||
}
|
||||
for label, marker in required_markers.items():
|
||||
if marker not in internal and marker not in inode:
|
||||
raise SystemExit(f"required ownership/source marker missing: {label}")
|
||||
|
||||
result = {
|
||||
"freebsd_device_fields": freebsd_device,
|
||||
"freebsd_map_fields": freebsd_map,
|
||||
"linux_device_common_fields": [
|
||||
field for field in linux_device if field in {"blocks", "uniaddr"}
|
||||
],
|
||||
"linux_map_common_fields": [
|
||||
field for field in linux_map if field in {"m_dif", "m_pa", "m_deviceid"}
|
||||
],
|
||||
"removed_field_uses": removed_uses,
|
||||
"retained_freebsd_ownership_fields": [
|
||||
"devvp",
|
||||
"dev",
|
||||
"cp",
|
||||
"mediasize",
|
||||
"sectorsize",
|
||||
],
|
||||
}
|
||||
with output.open("x", encoding="ascii") as stream:
|
||||
json.dump(result, stream, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
print("fields PASS device=7 map=4 removed-uses=0")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B05 field layout or write-only closure oracle failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/field-check.json" > "$artifacts/SHA256SUMS"
|
||||
printf 'B05 PASS private field model and zero-consumer closure\n'
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
gate=$PRE15_DUT/tests/pre15/gates/P15-005.sh
|
||||
tuple_oracle=$PRE15_DUT/tests/pre15/fixtures/B06-tuples.json
|
||||
ownership_oracle=$PRE15_DUT/tests/pre15/fixtures/B06-ownership.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in cc python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B06 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b06-gate "$gate"
|
||||
pre15_record_fixture b06-tuple-oracle "$tuple_oracle"
|
||||
pre15_record_fixture b06-ownership-oracle "$ownership_oracle"
|
||||
for source in internal.h data.c decompressor.c inode.c super.c xattr.c zdata.c zmap.c; do
|
||||
pre15_record_fixture "b06-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if "$gate" --worktree --oracle "$tuple_oracle" \
|
||||
--output "$artifacts/gate" >"$artifacts/gate.stdout" \
|
||||
2>"$artifacts/gate.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B06 G02 candidate replay failed'
|
||||
fi
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$ownership_oracle" "$artifacts/gate" \
|
||||
"$artifacts/ownership-check.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
expected = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
|
||||
gate_dir = Path(sys.argv[3])
|
||||
output = Path(sys.argv[4])
|
||||
result = json.loads((gate_dir / "result.json").read_text(encoding="ascii"))
|
||||
ownership = json.loads((gate_dir / "ownership.json").read_text(encoding="ascii"))
|
||||
|
||||
if result["status"] != "GO" or result["tuple_status"] != "PASS":
|
||||
raise SystemExit(f"candidate tuple result is not GO: {result!r}")
|
||||
if not result["object_mode"] or result["oracle_equal"] is not True:
|
||||
raise SystemExit(f"candidate did not replay the frozen object oracle: {result!r}")
|
||||
if ownership["status"] != "PASS" or ownership["failures"]:
|
||||
raise SystemExit(f"candidate ownership result failed: {ownership['failures']!r}")
|
||||
if ownership["consumers"] != expected["consumers"]:
|
||||
raise SystemExit("ownership consumer set changed")
|
||||
if ownership["direct_metadata_functions"] != expected["direct_metadata_functions"]:
|
||||
raise SystemExit("direct metadata consumer inventory changed")
|
||||
if ownership["helper_paths"]["paths"] != expected["helper_paths"]:
|
||||
raise SystemExit("actual helper lifecycle paths changed")
|
||||
|
||||
metric_order = expected["metric_order"]
|
||||
actual_metrics = {
|
||||
item["function"]: [item[field] for field in metric_order]
|
||||
for item in ownership["candidate_contract"]["function_metrics"]
|
||||
}
|
||||
if actual_metrics != expected["function_metrics"]:
|
||||
raise SystemExit("per-function ownership metrics changed")
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"backend function missing: {name}")
|
||||
start = source.rfind("\n\n", 0, match.start()) + 2
|
||||
brace = source.find("{", match.end())
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1] + "\n"
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated backend function: {name}")
|
||||
|
||||
|
||||
backend_hashes = {}
|
||||
for key, frozen_hash in expected["freebsd_backend_function_sha256"].items():
|
||||
filename, function = key.split(":", 1)
|
||||
source = (root / "repo-pre-15" / "src" / filename).read_text(encoding="utf-8")
|
||||
current_hash = hashlib.sha256(
|
||||
extract_function(source, function).encode("utf-8")
|
||||
).hexdigest()
|
||||
backend_hashes[key] = current_hash
|
||||
if current_hash != frozen_hash:
|
||||
raise SystemExit(f"FreeBSD backend lifecycle changed: {key}")
|
||||
|
||||
internal_path = root / "repo-pre-15" / "src" / "internal.h"
|
||||
internal = internal_path.read_text(encoding="utf-8")
|
||||
match = re.search(
|
||||
r"^struct erofs_map_blocks \{\n.*?^\};\n",
|
||||
internal,
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
if not match:
|
||||
raise SystemExit("missing legacy map tuple object")
|
||||
map_hash = hashlib.sha256(match.group(0).encode("utf-8")).hexdigest()
|
||||
if map_hash != expected["map_blocks_sha256"]:
|
||||
raise SystemExit("B07 map objectization leaked into B06")
|
||||
|
||||
required = [
|
||||
"struct erofs_buf {",
|
||||
"#define EROFS_BUF_INITIALIZER { .data = NULL, .release = NULL }",
|
||||
"void erofs_put_metabuf(struct erofs_buf *buf);",
|
||||
"int erofs_bread(struct erofs_sb_info *sbi, erofs_off_t off, size_t len, void **bufp);",
|
||||
"void erofs_brelse(void *buf);",
|
||||
]
|
||||
for marker in required:
|
||||
if marker not in internal:
|
||||
raise SystemExit(f"required B06/raw API marker missing: {marker}")
|
||||
if re.search(
|
||||
r"erofs_read_metadata\s*\([^;]*void\s*\*\*bufp\s*\)\s*;",
|
||||
internal,
|
||||
re.MULTILINE | re.DOTALL,
|
||||
):
|
||||
raise SystemExit("legacy metadata void-pointer API remains")
|
||||
for filename in ("dir.c", "namei.c"):
|
||||
text = (root / "repo-pre-15" / "src" / filename).read_text(encoding="utf-8")
|
||||
if "struct erofs_buf" in text or "erofs_put_metabuf" in text:
|
||||
raise SystemExit(f"raw directory consumer was objectized: {filename}")
|
||||
|
||||
summary = {
|
||||
"status": "PASS",
|
||||
"tuple_cases": result["case_count"],
|
||||
"tuple_oracle_equal": result["oracle_equal"],
|
||||
"ownership_paths": result["ownership_path_count"],
|
||||
"helper_paths": len(expected["helper_paths"]),
|
||||
"function_metrics": len(actual_metrics),
|
||||
"freebsd_backend_functions": len(backend_hashes),
|
||||
"map_blocks_sha256": map_hash,
|
||||
"qemu": result["qemu"],
|
||||
"full_feature_suite": result["full_feature_suite"],
|
||||
}
|
||||
with output.open("x", encoding="ascii") as stream:
|
||||
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
print("B06 ownership PASS tuples=14 paths=22 helper=5 functions=28")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B06 ownership fixture comparison failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/gate/result.json" "$artifacts/gate/tuples.json" \
|
||||
"$artifacts/gate/ownership.json" "$artifacts/ownership-check.json" \
|
||||
>"$artifacts/SHA256SUMS"
|
||||
printf 'B06 PASS explicit metadata ownership and frozen tuple replay\n'
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
gate=$PRE15_DUT/tests/pre15/gates/P15-006.sh
|
||||
input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
|
||||
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in cc python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B07a host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b07a-gate "$gate"
|
||||
pre15_record_fixture b07a-input "$input"
|
||||
pre15_record_fixture b07a-oracle "$oracle"
|
||||
for source in internal.h data.c zmap.c zdata.c super.c; do
|
||||
pre15_record_fixture "b07a-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if "$gate" --worktree --oracle "$oracle" --output "$artifacts/gate" \
|
||||
>"$artifacts/gate.stdout" 2>"$artifacts/gate.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07a P15-006 candidate replay failed'
|
||||
fi
|
||||
|
||||
if python3 - "$PRE15_DUT" "$oracle" "$artifacts/gate" \
|
||||
"$artifacts/adapter-check.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
dut = Path(sys.argv[1])
|
||||
oracle = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
|
||||
gate = Path(sys.argv[3])
|
||||
output = Path(sys.argv[4])
|
||||
result = json.loads((gate / "result.json").read_text(encoding="ascii"))
|
||||
tuples = json.loads((gate / "tuples.json").read_text(encoding="ascii"))
|
||||
devices = json.loads((gate / "devices.json").read_text(encoding="ascii"))
|
||||
|
||||
if result["status"] != "GO" or result["failures"]:
|
||||
raise SystemExit(f"P15-006 candidate did not GO: {result!r}")
|
||||
if not result["adapter_mode"] or result["oracle_equal"] is not True:
|
||||
raise SystemExit("candidate did not use the common map adapter and frozen oracle")
|
||||
if result["map_case_count"] != 80 or result["device_case_count"] != 13:
|
||||
raise SystemExit("P15-006 corpus denominator changed")
|
||||
if result["model_sha256"] != oracle["model_sha256"]:
|
||||
raise SystemExit("independent model digest changed")
|
||||
if result["tuple_bytes_sha256"] != oracle["tuple_bytes_sha256"]:
|
||||
raise SystemExit("tuple byte stream changed")
|
||||
if result["adapter_probe"]["status"] != "PASS":
|
||||
raise SystemExit("FULL/COMPACT adapter pass-through probe failed")
|
||||
if not result["adapter_probe"]["full_and_compact_dispatch"]:
|
||||
raise SystemExit("compressed layout dispatch is incomplete")
|
||||
if result["adapter_probe"]["flag_mask"] != 0x1F:
|
||||
raise SystemExit("not all map flag bits passed through the adapter")
|
||||
if tuples["tuple_bytes_sha256"] != oracle["tuple_bytes_sha256"]:
|
||||
raise SystemExit("tuple artifact does not match frozen bytes")
|
||||
if devices["status"] != "PASS" or len(devices["records"]) != 13:
|
||||
raise SystemExit("device/GEOM resolution oracle failed")
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
start = source.rfind("\n\n", 0, match.start()) + 2
|
||||
brace = source.find("{", match.end())
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
internal = (dut / "src/internal.h").read_text(encoding="utf-8")
|
||||
data = (dut / "src/data.c").read_text(encoding="utf-8")
|
||||
zmap = (dut / "src/zmap.c").read_text(encoding="utf-8")
|
||||
zdata = (dut / "src/zdata.c").read_text(encoding="utf-8")
|
||||
super_source = (dut / "src/super.c").read_text(encoding="utf-8")
|
||||
|
||||
object_prototype = re.search(
|
||||
r"int erofs_map_blocks\s*\(struct erofs_sb_info \*sbi,\s*"
|
||||
r"struct erofs_inode \*vi,\s*struct erofs_map_blocks \*map\s*\);",
|
||||
internal,
|
||||
re.MULTILINE,
|
||||
)
|
||||
if object_prototype is None:
|
||||
raise SystemExit("common map object prototype is absent")
|
||||
if re.search(r"erofs_map_blocks\s*\([^;]*phys_off", internal, re.DOTALL):
|
||||
raise SystemExit("legacy scatter prototype remains exported")
|
||||
if "static int\nerofs_map_blocks_legacy(" not in data:
|
||||
raise SystemExit("legacy producer target is not file-local")
|
||||
if len(re.findall(r"\berofs_map_blocks_legacy\s*\(", data)) != 4:
|
||||
raise SystemExit("B07a must retain exactly two legacy data consumers")
|
||||
if len(re.findall(r"\berofs_map_blocks\s*\(", data)) != 1:
|
||||
raise SystemExit("B07a common map entry has an unexpected data.c caller")
|
||||
|
||||
adapter = extract_function(data, "erofs_map_blocks")
|
||||
required_adapter = [
|
||||
"struct erofs_map_blocks next = { .m_la = map->m_la };",
|
||||
"z_erofs_map_blocks_iter(sbi, vi, &next, 0);",
|
||||
"next.m_deviceid = device_id;",
|
||||
"next.m_flags |= EROFS_MAP_MAPPED;",
|
||||
"next.m_flags |= EROFS_MAP_META;",
|
||||
"*map = next;",
|
||||
]
|
||||
for marker in required_adapter:
|
||||
if marker not in adapter:
|
||||
raise SystemExit(f"adapter contract marker is absent: {marker}")
|
||||
|
||||
for function in ("erofs_read_data", "erofs_read_uio"):
|
||||
body = extract_function(data, function)
|
||||
if "erofs_map_blocks_legacy(" not in body or "erofs_map_blocks(" in body:
|
||||
raise SystemExit(f"B07a migrated consumer early: {function}")
|
||||
for source_name, source in (("zdata.c", zdata), ("super.c", super_source)):
|
||||
if "z_erofs_map_blocks_iter(" not in source or "erofs_map_blocks(" in source:
|
||||
raise SystemExit(f"B07a migrated compressed consumer early: {source_name}")
|
||||
if "map adapter must preserve every map flag" not in zmap:
|
||||
raise SystemExit("compressed map flag contract assertion is absent")
|
||||
|
||||
summary = {
|
||||
"status": "PASS",
|
||||
"map_cases": result["map_case_count"],
|
||||
"device_cases": result["device_case_count"],
|
||||
"tuple_bytes_sha256": result["tuple_bytes_sha256"],
|
||||
"model_sha256": result["model_sha256"],
|
||||
"adapter_probe": result["adapter_probe"],
|
||||
"legacy_data_consumers": 2,
|
||||
"compressed_consumers_migrated": False,
|
||||
"qemu": result["qemu"],
|
||||
"full_feature_suite": result["full_feature_suite"],
|
||||
}
|
||||
with output.open("x", encoding="ascii") as stream:
|
||||
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
print("B07a PASS map=80 device=13 tuple-bytes exact flags=0x1f")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07a adapter boundary check failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/gate/result.json" "$artifacts/gate/tuples.json" \
|
||||
"$artifacts/gate/devices.json" "$artifacts/adapter-check.json" \
|
||||
>"$artifacts/SHA256SUMS"
|
||||
printf 'B07a PASS exact map adapter; producers and consumers remain staged\n'
|
||||
Executable
+360
@@ -0,0 +1,360 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
gate=$PRE15_DUT/tests/pre15/gates/P15-006.sh
|
||||
input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
|
||||
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
baseline=6673f51152a5195a8a8903aa801f820abce7936e
|
||||
b07a=141b11f0d847a63a47b6c6143e4c2913a1147a0f
|
||||
|
||||
for tool in cc git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B07b host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b07b-gate "$gate"
|
||||
pre15_record_fixture b07b-input "$input"
|
||||
pre15_record_fixture b07b-oracle "$oracle"
|
||||
for source in internal.h data.c zmap.c zdata.c super.c; do
|
||||
pre15_record_fixture "b07b-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if "$gate" --base "$baseline" --oracle "$oracle" \
|
||||
--output "$artifacts/baseline" >"$artifacts/baseline.stdout" \
|
||||
2>"$artifacts/baseline.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07b frozen P15-006 baseline replay failed'
|
||||
fi
|
||||
|
||||
if python3 - "$gate" "$artifacts/P15-006-B07b.py" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
source = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||
marker = "<<'PY'\n"
|
||||
start = source.index(marker) + len(marker)
|
||||
end = source.rindex("\nPY\n")
|
||||
program = source[start:end]
|
||||
|
||||
|
||||
def replace_once(old: str, new: str) -> None:
|
||||
global program
|
||||
if program.count(old) != 1:
|
||||
raise SystemExit(f"P15-006 derivation marker count changed: {old[:60]!r}")
|
||||
program = program.replace(old, new)
|
||||
|
||||
|
||||
replace_once(
|
||||
''' if actual_hash != expected_hash:
|
||||
raise SystemExit(f"protected producer/GEOM body changed: {key}")
|
||||
''',
|
||||
''' if actual_hash != expected_hash and key != "data.c:erofs_map_blocks_chunk":
|
||||
raise SystemExit(f"protected producer/GEOM body changed: {key}")
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
'''if candidate_mode:
|
||||
legacy_body = extract_function(data_source, "erofs_map_blocks_legacy")
|
||||
expected_legacy_hash = SPEC["candidate_legacy_map_sha256"]
|
||||
else:
|
||||
legacy_body = extract_function(data_source, "erofs_map_blocks")
|
||||
expected_legacy_hash = SPEC["baseline_legacy_map_sha256"]
|
||||
if sha256_bytes(legacy_body.encode("utf-8")) != expected_legacy_hash:
|
||||
raise SystemExit("plain/chunk legacy producer body changed")
|
||||
''',
|
||||
'''if candidate_mode:
|
||||
legacy_body = extract_function(data_source, "erofs_map_blocks_legacy")
|
||||
expected_legacy_hash = None
|
||||
else:
|
||||
legacy_body = extract_function(data_source, "erofs_map_blocks")
|
||||
expected_legacy_hash = SPEC["baseline_legacy_map_sha256"]
|
||||
if expected_legacy_hash is not None and sha256_bytes(legacy_body.encode("utf-8")) != expected_legacy_hash:
|
||||
raise SystemExit("plain/chunk legacy producer body changed")
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
'''program += extract_function(data_source, "erofs_inline_tail_start")
|
||||
program += extract_function(data_source, "erofs_map_blocks_chunk")
|
||||
program += legacy_body
|
||||
if candidate_mode:
|
||||
program += extract_function(data_source, "erofs_map_blocks")
|
||||
''',
|
||||
'''program += extract_function(data_source, "erofs_inline_tail_start")
|
||||
program += extract_function(data_source, "erofs_map_blocks_chunk")
|
||||
if candidate_mode:
|
||||
program += extract_function(data_source, "erofs_map_blocks_flatmode")
|
||||
program += extract_function(data_source, "erofs_map_blocks")
|
||||
else:
|
||||
program += legacy_body
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
''' adapter_program += extract_function(data_source, "erofs_inline_tail_start")
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks_chunk")
|
||||
adapter_program += legacy_body
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks")
|
||||
''',
|
||||
''' adapter_program += extract_function(data_source, "erofs_inline_tail_start")
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks_chunk")
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks_flatmode")
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks")
|
||||
''',
|
||||
)
|
||||
|
||||
Path(sys.argv[2]).write_text(program + "\n", encoding="ascii")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07b could not derive the current-producer replay'
|
||||
fi
|
||||
|
||||
if python3 "$artifacts/P15-006-B07b.py" "$PRE15_ROOT" "$input" \
|
||||
worktree '' "$artifacts/candidate" "$oracle" \
|
||||
>"$artifacts/candidate.stdout" 2>"$artifacts/candidate.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07b object producer replay failed'
|
||||
fi
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$oracle" \
|
||||
"$artifacts/baseline" "$artifacts/candidate" "$artifacts/producer-check.json" \
|
||||
"$b07a" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
oracle = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
|
||||
baseline = Path(sys.argv[4])
|
||||
candidate = Path(sys.argv[5])
|
||||
output = Path(sys.argv[6])
|
||||
b07a = sys.argv[7]
|
||||
|
||||
|
||||
def load(directory: Path, name: str) -> dict:
|
||||
return json.loads((directory / name).read_text(encoding="ascii"))
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
start = source.rfind("\n\n", 0, match.start()) + 2
|
||||
brace = source.find("{", match.end())
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def committed_source(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{b07a}:repo-pre-15/src/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B07a source {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
base_result = load(baseline, "result.json")
|
||||
base_tuples = load(baseline, "tuples.json")
|
||||
base_devices = load(baseline, "devices.json")
|
||||
result = load(candidate, "result.json")
|
||||
tuples = load(candidate, "tuples.json")
|
||||
devices = load(candidate, "devices.json")
|
||||
|
||||
for label, record in (("baseline", base_result), ("candidate", result)):
|
||||
if record["status"] != "GO" or record["failures"]:
|
||||
raise SystemExit(f"{label} P15-006 replay did not GO: {record!r}")
|
||||
if base_result["map_case_count"] != 80 or result["map_case_count"] != 80:
|
||||
raise SystemExit("B07 map denominator changed")
|
||||
if base_result["device_case_count"] != 13 or result["device_case_count"] != 13:
|
||||
raise SystemExit("B07 device denominator changed")
|
||||
if result["oracle_equal"] is not True or base_result["oracle_equal"] is not True:
|
||||
raise SystemExit("frozen B07 oracle comparison failed")
|
||||
if result["model_sha256"] != oracle["model_sha256"]:
|
||||
raise SystemExit("independent P15-006 model digest changed")
|
||||
if result["tuple_bytes_sha256"] != oracle["tuple_bytes_sha256"]:
|
||||
raise SystemExit("full tuple stream differs from the frozen B07 oracle")
|
||||
if result["adapter_probe"]["status"] != "PASS":
|
||||
raise SystemExit("common adapter probe failed after producer migration")
|
||||
|
||||
base_records = {record["id"]: record for record in base_tuples["records"]}
|
||||
records = {record["id"]: record for record in tuples["records"]}
|
||||
if set(base_records) != set(records) or len(records) != 80:
|
||||
raise SystemExit("B07 tuple ID set changed")
|
||||
full_diffs = [
|
||||
case_id for case_id in records
|
||||
if records[case_id]["tuple_hex"] != base_records[case_id]["tuple_hex"]
|
||||
]
|
||||
producer_ids = [
|
||||
record["id"] for record in tuples["records"] if record["engine"] == "data"
|
||||
]
|
||||
if len(producer_ids) != 50:
|
||||
raise SystemExit(f"plain/chunk producer denominator changed: {len(producer_ids)}")
|
||||
producer_diffs = [case_id for case_id in producer_ids if case_id in full_diffs]
|
||||
producer_bytes = b"".join(bytes.fromhex(records[case_id]["tuple_hex"]) for case_id in producer_ids)
|
||||
producer_sha256 = hashlib.sha256(producer_bytes).hexdigest()
|
||||
|
||||
if full_diffs or producer_diffs:
|
||||
raise SystemExit(
|
||||
f"B07b tuple difference: producer={producer_diffs!r} full={full_diffs!r}"
|
||||
)
|
||||
if (
|
||||
devices["records"] != base_devices["records"]
|
||||
or devices["status"] != "PASS"
|
||||
or base_devices["status"] != "PASS"
|
||||
):
|
||||
raise SystemExit("B07b device-resolution records changed")
|
||||
|
||||
data = (dut / "src/data.c").read_text(encoding="utf-8")
|
||||
chunk = extract_function(data, "erofs_map_blocks_chunk")
|
||||
flatmode = extract_function(data, "erofs_map_blocks_flatmode")
|
||||
common = extract_function(data, "erofs_map_blocks")
|
||||
legacy = extract_function(data, "erofs_map_blocks_legacy")
|
||||
if "struct erofs_map_blocks *map" not in chunk.split("{", 1)[0]:
|
||||
raise SystemExit("chunk producer does not accept the common map object")
|
||||
for marker in (
|
||||
"map->m_pa", "map->m_llen", "map->m_plen", "map->m_deviceid",
|
||||
"map->m_flags |= EROFS_MAP_MAPPED",
|
||||
):
|
||||
if marker not in chunk:
|
||||
raise SystemExit(f"chunk producer field is not object-backed: {marker}")
|
||||
for marker in (
|
||||
"map->m_la", "map->m_pa", "map->m_llen", "map->m_plen",
|
||||
"EROFS_MAP_MAPPED", "EROFS_MAP_META",
|
||||
):
|
||||
if marker not in flatmode:
|
||||
raise SystemExit(f"flat producer field is not object-backed: {marker}")
|
||||
if "erofs_map_blocks_flatmode(sbi, vi, &next)" not in common:
|
||||
raise SystemExit("common entry does not call the object flat producer")
|
||||
if "erofs_map_blocks(sbi, vi, &map)" not in legacy:
|
||||
raise SystemExit("legacy scatter adapter does not delegate to the object entry")
|
||||
if len(re.findall(r"\berofs_map_blocks_legacy\s*\(", data)) != 3:
|
||||
raise SystemExit("B07b must retain exactly two legacy data consumers")
|
||||
|
||||
old_data = committed_source("data.c")
|
||||
for function in ("erofs_read_data", "erofs_read_uio"):
|
||||
body = extract_function(data, function)
|
||||
if body != extract_function(old_data, function):
|
||||
raise SystemExit(f"B07b changed consumer early: {function}")
|
||||
if "erofs_map_blocks_legacy(" not in body or "erofs_map_blocks(" in body:
|
||||
raise SystemExit(f"B07b migrated consumer early: {function}")
|
||||
for filename in ("internal.h", "super.c", "zdata.c", "zmap.c"):
|
||||
current = (dut / "src" / filename).read_text(encoding="utf-8")
|
||||
if current != committed_source(filename):
|
||||
raise SystemExit(f"B07b changed non-producer source: {filename}")
|
||||
|
||||
cleanup = Counter(
|
||||
(record["actual"]["acquire_count"], record["actual"]["release_count"])
|
||||
for record in tuples["records"]
|
||||
)
|
||||
if any(acquire != release for acquire, release in cleanup):
|
||||
raise SystemExit(f"metadata cleanup became unbalanced: {cleanup!r}")
|
||||
if any(record["actual"]["errno"] < 0 for record in tuples["records"]):
|
||||
raise SystemExit("negative errno observed")
|
||||
|
||||
summary = {
|
||||
"status": "PASS",
|
||||
"final_id": "P15-006",
|
||||
"map_cases": 80,
|
||||
"producer_cases": len(producer_ids),
|
||||
"compressed_control_cases": 30,
|
||||
"device_cases": 13,
|
||||
"producer_tuple_diff_count": len(producer_diffs),
|
||||
"full_tuple_diff_count": len(full_diffs),
|
||||
"producer_tuple_bytes": len(producer_bytes),
|
||||
"producer_tuple_bytes_sha256": producer_sha256,
|
||||
"full_tuple_bytes_sha256": result["tuple_bytes_sha256"],
|
||||
"model_sha256": result["model_sha256"],
|
||||
"legacy_data_consumers": 2,
|
||||
"compressed_consumers_migrated": False,
|
||||
"cleanup_distribution": {
|
||||
f"{acquire}:{release}": count
|
||||
for (acquire, release), count in sorted(cleanup.items())
|
||||
},
|
||||
"qemu": "NOT_RUN",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
}
|
||||
with output.open("x", encoding="ascii") as stream:
|
||||
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
print(
|
||||
"B07b PASS producer=50 diff=0 full=80 device=13 "
|
||||
f"producer-sha256={producer_sha256}"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07b producer boundary check failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/baseline/result.json" \
|
||||
"$artifacts/baseline/tuples.json" "$artifacts/baseline/devices.json" \
|
||||
"$artifacts/candidate/result.json" "$artifacts/candidate/tuples.json" \
|
||||
"$artifacts/candidate/devices.json" "$artifacts/producer-check.json" \
|
||||
>"$artifacts/SHA256SUMS"
|
||||
printf 'B07b PASS object producers; all consumers remain staged\n'
|
||||
Executable
+531
@@ -0,0 +1,531 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
gate=$PRE15_DUT/tests/pre15/gates/P15-006.sh
|
||||
input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
|
||||
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
baseline=6673f51152a5195a8a8903aa801f820abce7936e
|
||||
b07b=d58f131cfe855728969aa3695d34a26a61b90277
|
||||
|
||||
for tool in cc git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B07c host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b07c-gate "$gate"
|
||||
pre15_record_fixture b07c-input "$input"
|
||||
pre15_record_fixture b07c-oracle "$oracle"
|
||||
for source in internal.h data.c zmap.c zdata.c super.c; do
|
||||
pre15_record_fixture "b07c-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if "$gate" --base "$baseline" --oracle "$oracle" \
|
||||
--output "$artifacts/baseline" >"$artifacts/baseline.stdout" \
|
||||
2>"$artifacts/baseline.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07c frozen P15-006 baseline replay failed'
|
||||
fi
|
||||
|
||||
if python3 - "$gate" "$artifacts/P15-006-B07c.py" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
source = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||
marker = "<<'PY'\n"
|
||||
start = source.index(marker) + len(marker)
|
||||
end = source.rindex("\nPY\n")
|
||||
program = source[start:end]
|
||||
|
||||
|
||||
def replace_once(old: str, new: str) -> None:
|
||||
global program
|
||||
if program.count(old) != 1:
|
||||
raise SystemExit(f"P15-006 derivation marker count changed: {old[:70]!r}")
|
||||
program = program.replace(old, new)
|
||||
|
||||
|
||||
replace_once(
|
||||
'''if candidate_mode and not re.search(
|
||||
r"^erofs_map_blocks_legacy\\s*\\(", data_source, re.MULTILINE
|
||||
):
|
||||
raise SystemExit("candidate map adapter has no file-local legacy target")
|
||||
''',
|
||||
'''if candidate_mode and re.search(
|
||||
r"^erofs_map_blocks_legacy\\s*\\(", data_source, re.MULTILINE
|
||||
):
|
||||
raise SystemExit("B07c candidate still has the legacy scatter adapter")
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
'''for key, expected_hash in SPEC["protected_function_sha256"].items():
|
||||
filename, function = key.split(":", 1)
|
||||
text = data_source if filename == "data.c" else zmap_source
|
||||
actual_hash = sha256_bytes(extract_function(text, function).encode("utf-8"))
|
||||
protected_hashes[key] = actual_hash
|
||||
if actual_hash != expected_hash:
|
||||
raise SystemExit(f"protected producer/GEOM body changed: {key}")
|
||||
''',
|
||||
'''for key, expected_hash in SPEC["protected_function_sha256"].items():
|
||||
filename, function = key.split(":", 1)
|
||||
text = data_source if filename == "data.c" else zmap_source
|
||||
actual_function = (
|
||||
"z_erofs_map_blocks"
|
||||
if key == "zmap.c:z_erofs_map_blocks_iter"
|
||||
else function
|
||||
)
|
||||
actual_hash = sha256_bytes(extract_function(text, actual_function).encode("utf-8"))
|
||||
protected_hashes[key] = actual_hash
|
||||
allowed = {
|
||||
"data.c:erofs_map_blocks_chunk",
|
||||
"zmap.c:z_erofs_map_blocks_iter",
|
||||
}
|
||||
if actual_hash != expected_hash and key not in allowed:
|
||||
raise SystemExit(f"protected producer/GEOM body changed: {key}")
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
'''if candidate_mode:
|
||||
legacy_body = extract_function(data_source, "erofs_map_blocks_legacy")
|
||||
expected_legacy_hash = SPEC["candidate_legacy_map_sha256"]
|
||||
else:
|
||||
legacy_body = extract_function(data_source, "erofs_map_blocks")
|
||||
expected_legacy_hash = SPEC["baseline_legacy_map_sha256"]
|
||||
if sha256_bytes(legacy_body.encode("utf-8")) != expected_legacy_hash:
|
||||
raise SystemExit("plain/chunk legacy producer body changed")
|
||||
''',
|
||||
'''if candidate_mode:
|
||||
legacy_body = ""
|
||||
expected_legacy_hash = None
|
||||
else:
|
||||
legacy_body = extract_function(data_source, "erofs_map_blocks")
|
||||
expected_legacy_hash = SPEC["baseline_legacy_map_sha256"]
|
||||
if expected_legacy_hash is not None and sha256_bytes(legacy_body.encode("utf-8")) != expected_legacy_hash:
|
||||
raise SystemExit("plain/chunk legacy producer body changed")
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
' "z_erofs_map_blocks_iter",\n',
|
||||
' "z_erofs_map_blocks",\n',
|
||||
)
|
||||
replace_once(
|
||||
'program += "int z_erofs_map_blocks_iter(struct erofs_sb_info *, struct erofs_inode *, struct erofs_map_blocks *, int);\\n"\n',
|
||||
'program += "int z_erofs_map_blocks(struct erofs_sb_info *, struct erofs_inode *, struct erofs_map_blocks *);\\n"\n',
|
||||
)
|
||||
replace_once(
|
||||
'''program += extract_function(data_source, "erofs_inline_tail_start")
|
||||
program += extract_function(data_source, "erofs_map_blocks_chunk")
|
||||
program += legacy_body
|
||||
if candidate_mode:
|
||||
program += extract_function(data_source, "erofs_map_blocks")
|
||||
''',
|
||||
'''program += extract_function(data_source, "erofs_inline_tail_start")
|
||||
program += extract_function(data_source, "erofs_map_blocks_chunk")
|
||||
if candidate_mode:
|
||||
program += extract_function(data_source, "erofs_map_blocks_flatmode")
|
||||
program += extract_function(data_source, "erofs_map_blocks")
|
||||
else:
|
||||
program += legacy_body
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
'''int z_erofs_map_blocks_iter(struct erofs_sb_info *sbi,
|
||||
struct erofs_inode *vi, struct erofs_map_blocks *map, int flags)
|
||||
''',
|
||||
'''int z_erofs_map_blocks(struct erofs_sb_info *sbi,
|
||||
struct erofs_inode *vi, struct erofs_map_blocks *map)
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
''' (void)sbi;
|
||||
if (flags != 0)
|
||||
return (EINVAL);
|
||||
++gate_adapter_calls;
|
||||
''',
|
||||
''' (void)sbi;
|
||||
++gate_adapter_calls;
|
||||
''',
|
||||
)
|
||||
replace_once(
|
||||
''' adapter_program += extract_function(data_source, "erofs_inline_tail_start")
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks_chunk")
|
||||
adapter_program += legacy_body
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks")
|
||||
''',
|
||||
''' adapter_program += extract_function(data_source, "erofs_inline_tail_start")
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks_chunk")
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks_flatmode")
|
||||
adapter_program += extract_function(data_source, "erofs_map_blocks")
|
||||
''',
|
||||
)
|
||||
|
||||
Path(sys.argv[2]).write_text(program + "\n", encoding="ascii")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07c could not derive the current-consumer replay'
|
||||
fi
|
||||
|
||||
if python3 "$artifacts/P15-006-B07c.py" "$PRE15_ROOT" "$input" \
|
||||
worktree '' "$artifacts/candidate" "$oracle" \
|
||||
>"$artifacts/candidate.stdout" 2>"$artifacts/candidate.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07c common consumer replay failed'
|
||||
fi
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$oracle" \
|
||||
"$artifacts/baseline" "$artifacts/candidate" "$artifacts/consumer-check.json" \
|
||||
"$b07b" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
oracle = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
|
||||
baseline = Path(sys.argv[4])
|
||||
candidate = Path(sys.argv[5])
|
||||
output = Path(sys.argv[6])
|
||||
b07b = sys.argv[7]
|
||||
|
||||
|
||||
def load(directory: Path, name: str) -> dict:
|
||||
return json.loads((directory / name).read_text(encoding="ascii"))
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
start = source.rfind("\n\n", 0, match.start()) + 2
|
||||
brace = source.find("{", match.end())
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def function_names(source: str) -> set[str]:
|
||||
return set(re.findall(
|
||||
r"^([A-Za-z_][A-Za-z0-9_]*)\s*\(", source, re.MULTILINE
|
||||
))
|
||||
|
||||
|
||||
def committed_source(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{b07b}:repo-pre-15/src/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B07b source {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def replace_once(source: str, old: str, new: str, label: str) -> str:
|
||||
if source.count(old) != 1:
|
||||
raise SystemExit(f"{label} transform marker count changed")
|
||||
return source.replace(old, new)
|
||||
|
||||
|
||||
def require_order(source: str, markers: list[str], label: str) -> None:
|
||||
position = -1
|
||||
for marker in markers:
|
||||
next_position = source.find(marker, position + 1)
|
||||
if next_position < 0:
|
||||
raise SystemExit(f"{label} is missing ordered marker: {marker}")
|
||||
position = next_position
|
||||
|
||||
|
||||
base_result = load(baseline, "result.json")
|
||||
base_tuples = load(baseline, "tuples.json")
|
||||
base_devices = load(baseline, "devices.json")
|
||||
result = load(candidate, "result.json")
|
||||
tuples = load(candidate, "tuples.json")
|
||||
devices = load(candidate, "devices.json")
|
||||
|
||||
for label, record in (("baseline", base_result), ("candidate", result)):
|
||||
if record["status"] != "GO" or record["failures"]:
|
||||
raise SystemExit(f"{label} P15-006 replay did not GO: {record!r}")
|
||||
if result["map_case_count"] != 80 or result["device_case_count"] != 13:
|
||||
raise SystemExit("P15-006 corpus denominator changed")
|
||||
if result["oracle_equal"] is not True or base_result["oracle_equal"] is not True:
|
||||
raise SystemExit("frozen B07 oracle comparison failed")
|
||||
if result["model_sha256"] != oracle["model_sha256"]:
|
||||
raise SystemExit("independent P15-006 model digest changed")
|
||||
if result["tuple_bytes_sha256"] != oracle["tuple_bytes_sha256"]:
|
||||
raise SystemExit("full tuple stream differs from the frozen B07 oracle")
|
||||
if result["adapter_probe"]["status"] != "PASS":
|
||||
raise SystemExit("common compressed adapter probe failed")
|
||||
|
||||
base_records = {record["id"]: record for record in base_tuples["records"]}
|
||||
records = {record["id"]: record for record in tuples["records"]}
|
||||
if set(base_records) != set(records) or len(records) != 80:
|
||||
raise SystemExit("B07 tuple ID set changed")
|
||||
tuple_diffs = [
|
||||
case_id for case_id in records
|
||||
if records[case_id]["tuple_hex"] != base_records[case_id]["tuple_hex"]
|
||||
]
|
||||
if tuple_diffs:
|
||||
raise SystemExit(f"B07c full tuple differences: {tuple_diffs!r}")
|
||||
if (
|
||||
devices["records"] != base_devices["records"]
|
||||
or devices["status"] != "PASS"
|
||||
or base_devices["status"] != "PASS"
|
||||
):
|
||||
raise SystemExit("B07c device-resolution records changed")
|
||||
|
||||
sources = {
|
||||
name: (dut / "src" / name).read_text(encoding="utf-8")
|
||||
for name in ("internal.h", "data.c", "super.c", "zdata.c", "zmap.c")
|
||||
}
|
||||
old = {name: committed_source(name) for name in sources}
|
||||
joined = "\n".join(sources.values())
|
||||
if re.search(r"\berofs_map_blocks_legacy\b|\bz_erofs_map_blocks_iter\b", joined):
|
||||
raise SystemExit("legacy map interface remains")
|
||||
|
||||
common = extract_function(sources["data.c"], "erofs_map_blocks")
|
||||
read_data = extract_function(sources["data.c"], "erofs_read_data")
|
||||
read_uio = extract_function(sources["data.c"], "erofs_read_uio")
|
||||
zread = extract_function(sources["zdata.c"], "z_erofs_do_read")
|
||||
metabox = extract_function(sources["super.c"], "erofs_init_metabox_inode")
|
||||
backend = extract_function(sources["zmap.c"], "z_erofs_map_blocks")
|
||||
|
||||
if "z_erofs_map_blocks(sbi, vi, &next)" not in common:
|
||||
raise SystemExit("common entry does not own compressed dispatch")
|
||||
if backend.count("EROFS_GET_BLOCKS_FIEMAP") != 2:
|
||||
raise SystemExit("compressed backend did not preserve FIEMAP semantics")
|
||||
if sources["data.c"].count("erofs_map_blocks(sbi, vi, &map)") != 2:
|
||||
raise SystemExit("data consumers did not both migrate")
|
||||
if zread.count("erofs_map_blocks(sbi, vi, &map)") != 1:
|
||||
raise SystemExit("z_erofs_do_read did not migrate")
|
||||
if metabox.count("erofs_map_blocks(sbi, sbi->metabox_en, &map)") != 1:
|
||||
raise SystemExit("metabox validation did not migrate")
|
||||
|
||||
require_order(
|
||||
read_data,
|
||||
[
|
||||
"map = (struct erofs_map_blocks) { .m_la = loff + done };",
|
||||
"error = erofs_map_blocks(sbi, vi, &map);",
|
||||
"if (error != 0)",
|
||||
"if (map.m_llen == 0)",
|
||||
"want = MIN((size_t)map.m_llen, len - done);",
|
||||
"(map.m_flags & EROFS_MAP_MAPPED) == 0",
|
||||
"(map.m_flags & EROFS_MAP_META) != 0",
|
||||
"erofs_read_metadata(sbi, vi->nid, map.m_pa",
|
||||
"erofs_read_physical(sbi, map.m_deviceid, map.m_pa",
|
||||
"erofs_put_metabuf(&buf);",
|
||||
"erofs_brelse(blk);",
|
||||
],
|
||||
"erofs_read_data",
|
||||
)
|
||||
require_order(
|
||||
read_uio,
|
||||
[
|
||||
"map = (struct erofs_map_blocks) { .m_la = uio->uio_offset };",
|
||||
"error = erofs_map_blocks(sbi, vi, &map);",
|
||||
"if (error != 0)",
|
||||
"if (map.m_llen == 0)",
|
||||
"want = MIN((size_t)map.m_llen, (size_t)uio->uio_resid);",
|
||||
"(map.m_flags & EROFS_MAP_MAPPED) == 0",
|
||||
"error = uiomove(zerobuf, zlen, uio);",
|
||||
"(map.m_flags & EROFS_MAP_META) != 0",
|
||||
"erofs_read_metadata(sbi, vi->nid, map.m_pa, want",
|
||||
"erofs_read_physical(sbi, map.m_deviceid, map.m_pa",
|
||||
"error = uiomove(buf.data, want, uio);",
|
||||
"erofs_put_metabuf(&buf);",
|
||||
"error = uiomove(blk, want, uio);",
|
||||
"erofs_brelse(blk);",
|
||||
],
|
||||
"erofs_read_uio",
|
||||
)
|
||||
|
||||
expected_internal = replace_once(
|
||||
old["internal.h"],
|
||||
"int z_erofs_map_blocks_iter(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
|
||||
" struct erofs_map_blocks *map, int flags);",
|
||||
"int z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
|
||||
" struct erofs_map_blocks *map);",
|
||||
"internal.h",
|
||||
)
|
||||
if sources["internal.h"] != expected_internal:
|
||||
raise SystemExit("B07c internal.h contains changes beyond backend closure")
|
||||
|
||||
expected_zdata = replace_once(
|
||||
old["zdata.c"],
|
||||
"\t\tbzero(&map, sizeof(map));\n"
|
||||
"\t\tmap.m_la = loff + done;\n"
|
||||
"\t\terror = z_erofs_map_blocks_iter(sbi, vi, &map,\n"
|
||||
"\t\t EROFS_GET_BLOCKS_FIEMAP);",
|
||||
"\t\tmap = (struct erofs_map_blocks) { .m_la = loff + done };\n"
|
||||
"\t\terror = erofs_map_blocks(sbi, vi, &map);",
|
||||
"zdata.c",
|
||||
)
|
||||
if sources["zdata.c"] != expected_zdata:
|
||||
raise SystemExit("B07c zdata.c contains changes beyond consumer migration")
|
||||
|
||||
expected_super = replace_once(
|
||||
old["super.c"],
|
||||
"\t\t\tbzero(&map, sizeof(map));\n"
|
||||
"\t\t\tmap.m_la = sbi->metabox_en->size - 1;\n"
|
||||
"\t\t\terror = z_erofs_map_blocks_iter(sbi, sbi->metabox_en, &map,\n"
|
||||
"\t\t\t EROFS_GET_BLOCKS_FIEMAP);",
|
||||
"\t\t\tmap = (struct erofs_map_blocks) {\n"
|
||||
"\t\t\t\t.m_la = sbi->metabox_en->size - 1,\n"
|
||||
"\t\t\t};\n"
|
||||
"\t\t\terror = erofs_map_blocks(sbi, sbi->metabox_en, &map);",
|
||||
"super.c",
|
||||
)
|
||||
if sources["super.c"] != expected_super:
|
||||
raise SystemExit("B07c super.c contains changes beyond metabox migration")
|
||||
|
||||
old_backend = extract_function(old["zmap.c"], "z_erofs_map_blocks_iter")
|
||||
expected_backend = replace_once(
|
||||
old_backend,
|
||||
"z_erofs_map_blocks_iter(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
|
||||
" struct erofs_map_blocks *map, int flags)",
|
||||
"z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
|
||||
" struct erofs_map_blocks *map)",
|
||||
"zmap signature",
|
||||
)
|
||||
expected_backend = expected_backend.replace(
|
||||
"z_erofs_map_blocks_ext(sbi, vi, map, flags)",
|
||||
"z_erofs_map_blocks_ext(sbi, vi, map,\n\t\t\t EROFS_GET_BLOCKS_FIEMAP)",
|
||||
)
|
||||
expected_backend = expected_backend.replace(
|
||||
"z_erofs_map_blocks_fo(sbi, vi, map, flags)",
|
||||
"z_erofs_map_blocks_fo(sbi, vi, map,\n\t\t\t EROFS_GET_BLOCKS_FIEMAP)",
|
||||
)
|
||||
if backend != expected_backend:
|
||||
raise SystemExit("compressed backend changed beyond FIEMAP interface closure")
|
||||
expected_zmap = old["zmap.c"].replace(old_backend, expected_backend)
|
||||
if sources["zmap.c"] != expected_zmap:
|
||||
raise SystemExit("B07c zmap.c contains changes outside the backend wrapper")
|
||||
|
||||
old_data_functions = function_names(old["data.c"])
|
||||
data_functions = function_names(sources["data.c"])
|
||||
if old_data_functions - data_functions != {"erofs_map_blocks_legacy"}:
|
||||
raise SystemExit("B07c data.c removed functions beyond the legacy adapter")
|
||||
if data_functions - old_data_functions:
|
||||
raise SystemExit("B07c data.c added an unexpected function")
|
||||
changed_data_functions = {
|
||||
"erofs_map_blocks",
|
||||
"erofs_read_data",
|
||||
"erofs_read_uio",
|
||||
}
|
||||
for function in sorted(data_functions - changed_data_functions):
|
||||
if extract_function(old["data.c"], function) != extract_function(
|
||||
sources["data.c"], function
|
||||
):
|
||||
raise SystemExit(f"B07c changed protected data.c function: {function}")
|
||||
|
||||
cleanup = Counter(
|
||||
(record["actual"]["acquire_count"], record["actual"]["release_count"])
|
||||
for record in tuples["records"]
|
||||
)
|
||||
if any(acquire != release for acquire, release in cleanup):
|
||||
raise SystemExit(f"metadata cleanup became unbalanced: {cleanup!r}")
|
||||
if any(record["actual"]["errno"] < 0 for record in tuples["records"]):
|
||||
raise SystemExit("negative errno observed")
|
||||
|
||||
tuple_bytes = b"".join(
|
||||
bytes.fromhex(record["tuple_hex"]) for record in tuples["records"]
|
||||
)
|
||||
summary = {
|
||||
"status": "PASS",
|
||||
"final_id": "P15-006",
|
||||
"map_cases": 80,
|
||||
"producer_cases": 50,
|
||||
"compressed_cases": 30,
|
||||
"device_cases": 13,
|
||||
"full_tuple_diff_count": len(tuple_diffs),
|
||||
"tuple_bytes": len(tuple_bytes),
|
||||
"tuple_bytes_sha256": hashlib.sha256(tuple_bytes).hexdigest(),
|
||||
"model_sha256": result["model_sha256"],
|
||||
"legacy_callsite_count": 0,
|
||||
"data_consumers": 2,
|
||||
"compressed_consumers": 2,
|
||||
"backend_fiemap_dispatches": 2,
|
||||
"cleanup_distribution": {
|
||||
f"{acquire}:{release}": count
|
||||
for (acquire, release), count in sorted(cleanup.items())
|
||||
},
|
||||
"qemu": "NOT_RUN",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
}
|
||||
with output.open("x", encoding="ascii") as stream:
|
||||
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
print("B07c PASS full=80 diff=0 device=13 legacy=0 consumers=4")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B07c consumer boundary check failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/baseline/result.json" \
|
||||
"$artifacts/baseline/tuples.json" "$artifacts/baseline/devices.json" \
|
||||
"$artifacts/candidate/result.json" "$artifacts/candidate/tuples.json" \
|
||||
"$artifacts/candidate/devices.json" "$artifacts/consumer-check.json" \
|
||||
>"$artifacts/SHA256SUMS"
|
||||
printf 'B07c PASS common consumers; legacy map interfaces absent\n'
|
||||
Executable
+907
@@ -0,0 +1,907 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
gate=$PRE15_DUT/tests/pre15/gates/P15-006.sh
|
||||
input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
|
||||
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
|
||||
map_fixture=$PRE15_DUT/tests/pre15/fixtures/B08-map-runs.json
|
||||
io_fixture=$PRE15_DUT/tests/pre15/fixtures/B08-io-caps.json
|
||||
b07c_case=$PRE15_DUT/tests/pre15/cases/B07c-map-consumers.sh
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
baseline=6673f51152a5195a8a8903aa801f820abce7936e
|
||||
b07c=55c609db13fc9b0f21a3c7c9ec5cb9574fd18276
|
||||
|
||||
for tool in cc git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B08 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b08-gate "$gate"
|
||||
pre15_record_fixture b08-input "$input"
|
||||
pre15_record_fixture b08-b07-oracle "$oracle"
|
||||
pre15_record_fixture b08-map-runs "$map_fixture"
|
||||
pre15_record_fixture b08-io-caps "$io_fixture"
|
||||
pre15_record_fixture b08-b07c-case "$b07c_case"
|
||||
pre15_record_fixture b08-data "$PRE15_DUT/src/data.c"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if "$gate" --base "$baseline" --oracle "$oracle" \
|
||||
--output "$artifacts/baseline" >"$artifacts/baseline.stdout" \
|
||||
2>"$artifacts/baseline.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B08 frozen P15-006 baseline replay failed'
|
||||
fi
|
||||
|
||||
if python3 - "$b07c_case" "$artifacts/derive-B07c.py" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
source = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||
marker = 'if python3 - "$gate" "$artifacts/P15-006-B07c.py" <<\'PY\'\n'
|
||||
start = source.index(marker) + len(marker)
|
||||
end = source.index("\nPY\nthen", start)
|
||||
Path(sys.argv[2]).write_text(source[start:end] + "\n", encoding="ascii")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B08 could not recover the proven B07c replay adapter'
|
||||
fi
|
||||
|
||||
if python3 "$artifacts/derive-B07c.py" "$gate" \
|
||||
"$artifacts/P15-006-B08.py" >"$artifacts/derive.stdout" \
|
||||
2>"$artifacts/derive.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B08 could not derive the current map replay'
|
||||
fi
|
||||
|
||||
candidate_rc=0
|
||||
python3 "$artifacts/P15-006-B08.py" "$PRE15_ROOT" "$input" \
|
||||
worktree '' "$artifacts/candidate" "$oracle" \
|
||||
>"$artifacts/candidate.stdout" 2>"$artifacts/candidate.stderr" || \
|
||||
candidate_rc=$?
|
||||
if test "$candidate_rc" -ne 1; then
|
||||
pre15_dut_fail 'B08 old-run oracle did not reject exactly the intended change'
|
||||
fi
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$oracle" "$map_fixture" \
|
||||
"$io_fixture" "$artifacts/baseline" "$artifacts/candidate" \
|
||||
"$artifacts/B08-result.json" "$b07c" "$artifacts" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
oracle_path = Path(sys.argv[3])
|
||||
map_fixture_path = Path(sys.argv[4])
|
||||
io_fixture_path = Path(sys.argv[5])
|
||||
baseline = Path(sys.argv[6])
|
||||
candidate = Path(sys.argv[7])
|
||||
output = Path(sys.argv[8])
|
||||
b07c = sys.argv[9]
|
||||
artifacts = Path(sys.argv[10])
|
||||
|
||||
|
||||
def load(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="ascii"))
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, match.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", match.end())
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def function_names(source: str) -> set[str]:
|
||||
return set(re.findall(
|
||||
r"^([A-Za-z_][A-Za-z0-9_]*)\s*\(", source, re.MULTILINE
|
||||
))
|
||||
|
||||
|
||||
def committed_data() -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{b07c}:repo-pre-15/src/data.c"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B07c data.c: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def require_order(source: str, markers: list[str], label: str) -> None:
|
||||
position = -1
|
||||
for marker in markers:
|
||||
next_position = source.find(marker, position + 1)
|
||||
if next_position < 0:
|
||||
raise SystemExit(f"{label} is missing ordered marker: {marker}")
|
||||
position = next_position
|
||||
|
||||
|
||||
def compile_and_run(name: str, program: str) -> list[str]:
|
||||
source_path = artifacts / f"{name}.c"
|
||||
binary_path = artifacts / name
|
||||
source_path.write_text(program, encoding="ascii")
|
||||
compiled = subprocess.run(
|
||||
[
|
||||
"cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
|
||||
str(source_path), "-o", str(binary_path),
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(artifacts / f"{name}.compile.stdout").write_text(
|
||||
compiled.stdout, encoding="utf-8"
|
||||
)
|
||||
(artifacts / f"{name}.compile.stderr").write_text(
|
||||
compiled.stderr, encoding="utf-8"
|
||||
)
|
||||
if compiled.returncode != 0:
|
||||
raise SystemExit(f"{name} compilation failed")
|
||||
executed = subprocess.run(
|
||||
[str(binary_path)],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(artifacts / f"{name}.stdout").write_text(
|
||||
executed.stdout, encoding="utf-8"
|
||||
)
|
||||
(artifacts / f"{name}.stderr").write_text(
|
||||
executed.stderr, encoding="utf-8"
|
||||
)
|
||||
if executed.returncode != 0:
|
||||
raise SystemExit(f"{name} execution failed")
|
||||
return executed.stdout.splitlines()
|
||||
|
||||
|
||||
oracle = load(oracle_path)
|
||||
map_fixture = load(map_fixture_path)
|
||||
io_fixture = load(io_fixture_path)
|
||||
base_result = load(baseline / "result.json")
|
||||
base_tuples = load(baseline / "tuples.json")
|
||||
base_devices = load(baseline / "devices.json")
|
||||
result = load(candidate / "result.json")
|
||||
tuples = load(candidate / "tuples.json")
|
||||
devices = load(candidate / "devices.json")
|
||||
|
||||
if sha256_bytes(oracle_path.read_bytes()) != map_fixture["b07_oracle_sha256"]:
|
||||
raise SystemExit("B08 map fixture is not anchored to the frozen B07 oracle")
|
||||
if map_fixture["schema"] != 1 or map_fixture["candidate"] != "P15-074":
|
||||
raise SystemExit("invalid B08 map fixture identity")
|
||||
if io_fixture["schema"] != 1 or io_fixture["candidate"] != "P15-074":
|
||||
raise SystemExit("invalid B08 I/O fixture identity")
|
||||
if (
|
||||
base_result["status"] != "GO"
|
||||
or base_result["oracle_equal"] is not True
|
||||
or base_result["map_case_count"] != 80
|
||||
or base_result["device_case_count"] != 13
|
||||
):
|
||||
raise SystemExit("frozen B07 control replay did not GO")
|
||||
if result["status"] != "STOP" or result["map_case_count"] != 80:
|
||||
raise SystemExit("B08 candidate did not expose the intended old-run delta")
|
||||
if result["device_case_count"] != 13 or result["model_sha256"] != oracle["model_sha256"]:
|
||||
raise SystemExit("B08 candidate changed the independent corpus denominator")
|
||||
if result["adapter_probe"]["status"] != "PASS":
|
||||
raise SystemExit("B08 common map adapter probe failed")
|
||||
if devices["status"] != "PASS" or devices["records"] != base_devices["records"]:
|
||||
raise SystemExit("B08 changed one of the 13 frozen device records")
|
||||
|
||||
tuple_fields = (
|
||||
"m_la", "m_pa", "m_llen", "m_plen", "m_deviceid", "m_flags",
|
||||
"m_algorithmformat", "errno", "acquire_count", "release_count",
|
||||
)
|
||||
tuple_layout = map_fixture["b07_tuple_layout"]
|
||||
if tuple_layout != oracle["tuple_byte_layout"]:
|
||||
raise SystemExit("B08 tuple byte layout drifted")
|
||||
oracle_records = {record["id"]: record for record in oracle["records"]}
|
||||
actual_records = {record["id"]: record for record in tuples["records"]}
|
||||
base_records = {record["id"]: record for record in base_tuples["records"]}
|
||||
transforms = {record["id"]: record for record in map_fixture["b07_transforms"]}
|
||||
if len(oracle_records) != 80 or set(actual_records) != set(oracle_records):
|
||||
raise SystemExit("B08 map tuple ID set changed")
|
||||
if set(base_records) != set(oracle_records):
|
||||
raise SystemExit("B08 baseline tuple ID set changed")
|
||||
|
||||
expected_bytes = bytearray()
|
||||
actual_bytes = bytearray()
|
||||
changed_ids = []
|
||||
for record in oracle["records"]:
|
||||
case_id = record["id"]
|
||||
if base_records[case_id]["tuple_hex"] != record["tuple_hex"]:
|
||||
raise SystemExit(f"frozen B07 baseline drifted: {case_id}")
|
||||
expected = dict(zip(
|
||||
tuple_fields,
|
||||
struct.unpack(tuple_layout, bytes.fromhex(record["tuple_hex"])),
|
||||
strict=True,
|
||||
))
|
||||
if case_id in transforms:
|
||||
expected["m_llen"] = transforms[case_id]["m_llen"]
|
||||
expected["m_plen"] = transforms[case_id]["m_plen"]
|
||||
expected_tuple = struct.pack(
|
||||
tuple_layout, *(expected[field] for field in tuple_fields)
|
||||
)
|
||||
actual_tuple = bytes.fromhex(actual_records[case_id]["tuple_hex"])
|
||||
if actual_tuple != expected_tuple:
|
||||
raise SystemExit(f"B08 transformed tuple mismatch: {case_id}")
|
||||
if actual_tuple != bytes.fromhex(record["tuple_hex"]):
|
||||
changed_ids.append(case_id)
|
||||
expected_bytes.extend(expected_tuple)
|
||||
actual_bytes.extend(actual_tuple)
|
||||
|
||||
if set(changed_ids) != set(transforms):
|
||||
raise SystemExit(f"B08 changed unexpected B07 tuples: {changed_ids!r}")
|
||||
transformed_hash = sha256_bytes(bytes(expected_bytes))
|
||||
if bytes(actual_bytes) != bytes(expected_bytes):
|
||||
raise SystemExit("B08 full transformed tuple stream mismatch")
|
||||
if transformed_hash != map_fixture["expected_transformed_tuple_sha256"]:
|
||||
raise SystemExit("B08 transformed tuple fixture digest mismatch")
|
||||
allowed_failures = []
|
||||
for failure in result["failures"]:
|
||||
if failure.get("field") == "frozen-byte-oracle" and "id" not in failure:
|
||||
continue
|
||||
if failure.get("id") in transforms and failure.get("field") in {
|
||||
"m_llen", "m_plen"
|
||||
}:
|
||||
continue
|
||||
allowed_failures.append(failure)
|
||||
if allowed_failures:
|
||||
raise SystemExit(f"B08 old oracle reported unrelated failures: {allowed_failures!r}")
|
||||
|
||||
data_source = (dut / "src/data.c").read_text(encoding="utf-8")
|
||||
old_data = committed_data()
|
||||
expected_preamble = old_data[:old_data.index("static erofs_off_t\n")].replace(
|
||||
"#include <sys/systm.h>\n",
|
||||
"#include <sys/systm.h>\n#include <sys/_maxphys.h>\n",
|
||||
)
|
||||
if data_source[:data_source.index("static erofs_off_t\n")] != expected_preamble:
|
||||
raise SystemExit("B08 data.c preamble changed beyond the MAXPHYS header")
|
||||
old_functions = function_names(old_data)
|
||||
data_functions = function_names(data_source)
|
||||
if data_functions != old_functions:
|
||||
raise SystemExit("B08 added or removed a data.c function")
|
||||
changed_functions = {
|
||||
"erofs_map_blocks_flatmode", "erofs_read_data", "erofs_read_uio"
|
||||
}
|
||||
for function in sorted(data_functions - changed_functions):
|
||||
if extract_function(data_source, function) != extract_function(
|
||||
old_data, function
|
||||
):
|
||||
raise SystemExit(f"B08 changed out-of-scope data.c function: {function}")
|
||||
for function in changed_functions:
|
||||
if extract_function(data_source, function) == extract_function(
|
||||
old_data, function
|
||||
):
|
||||
raise SystemExit(f"B08 did not change required function: {function}")
|
||||
|
||||
flatmode = extract_function(data_source, "erofs_map_blocks_flatmode")
|
||||
read_data = extract_function(data_source, "erofs_read_data")
|
||||
read_uio = extract_function(data_source, "erofs_read_uio")
|
||||
if flatmode.count("map->m_llen = remain;") != 1:
|
||||
raise SystemExit("plain mapping does not expose the EOF run")
|
||||
if flatmode.count("map->m_llen = MIN(remain, tail_start - loff);") != 1:
|
||||
raise SystemExit("pre-inline mapping does not stop at the inline tail")
|
||||
if "MAXPHYS" in flatmode:
|
||||
raise SystemExit("B08 incorrectly capped the map contract")
|
||||
if read_data.count("(uint64_t)MAXPHYS") != 1:
|
||||
raise SystemExit("erofs_read_data lacks one MAXPHYS cap")
|
||||
if read_uio.count("(uint64_t)MAXPHYS") != 1:
|
||||
raise SystemExit("erofs_read_uio lacks one MAXPHYS cap")
|
||||
require_order(
|
||||
read_data,
|
||||
[
|
||||
"if (erofs_inode_is_data_compressed(vi->datalayout))",
|
||||
"return (z_erofs_read_data(sbi, vi, loff, len, bufp));",
|
||||
"error = erofs_map_blocks(sbi, vi, &map);",
|
||||
"MIN(map.m_llen, (uint64_t)MAXPHYS)",
|
||||
"erofs_read_physical(sbi, map.m_deviceid, map.m_pa",
|
||||
"erofs_brelse(blk);",
|
||||
],
|
||||
"erofs_read_data",
|
||||
)
|
||||
require_order(
|
||||
read_uio,
|
||||
[
|
||||
"if (erofs_inode_is_data_compressed(vi->datalayout))",
|
||||
"return (z_erofs_read_uio(sbi, vi, uio));",
|
||||
"error = erofs_map_blocks(sbi, vi, &map);",
|
||||
"MIN(map.m_llen, (uint64_t)MAXPHYS)",
|
||||
"erofs_read_physical(sbi, map.m_deviceid, map.m_pa",
|
||||
"error = uiomove(blk, want, uio);",
|
||||
"erofs_brelse(blk);",
|
||||
],
|
||||
"erofs_read_uio",
|
||||
)
|
||||
|
||||
map_common = r'''
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define EOPNOTSUPP 45
|
||||
#define EOVERFLOW 84
|
||||
#define EINTEGRITY 97
|
||||
#define EROFS_NULL_ADDR UINT32_MAX
|
||||
#define EROFS_INODE_FLAT_PLAIN 0
|
||||
#define EROFS_INODE_COMPRESSED_FULL 1
|
||||
#define EROFS_INODE_FLAT_INLINE 2
|
||||
#define EROFS_INODE_COMPRESSED_COMPACT 3
|
||||
#define EROFS_INODE_CHUNK_BASED 4
|
||||
#define EROFS_MAP_MAPPED 0x0001
|
||||
#define EROFS_MAP_META 0x0002
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
#define roundup2(x, y) (((x) + ((y) - 1)) & ~((y) - 1))
|
||||
|
||||
typedef uint64_t erofs_off_t;
|
||||
typedef uint64_t erofs_blk_t;
|
||||
|
||||
struct erofs_sb_info {
|
||||
uint64_t block_size;
|
||||
unsigned int blkszbits;
|
||||
};
|
||||
|
||||
struct erofs_inode {
|
||||
uint64_t size;
|
||||
erofs_off_t inode_off;
|
||||
erofs_blk_t startblk;
|
||||
uint8_t datalayout;
|
||||
uint8_t inode_isize;
|
||||
uint32_t xattr_isize;
|
||||
};
|
||||
|
||||
struct erofs_map_blocks {
|
||||
erofs_off_t m_pa, m_la;
|
||||
uint64_t m_plen, m_llen;
|
||||
unsigned short m_deviceid;
|
||||
char m_algorithmformat;
|
||||
unsigned int m_flags;
|
||||
};
|
||||
|
||||
static bool
|
||||
erofs_inode_is_data_compressed(unsigned int layout)
|
||||
{
|
||||
return (layout == EROFS_INODE_COMPRESSED_FULL ||
|
||||
layout == EROFS_INODE_COMPRESSED_COMPACT);
|
||||
}
|
||||
|
||||
static int
|
||||
erofs_map_blocks_chunk(struct erofs_sb_info *sbi, struct erofs_inode *vi,
|
||||
struct erofs_map_blocks *map)
|
||||
{
|
||||
(void)sbi;
|
||||
(void)vi;
|
||||
(void)map;
|
||||
return (EOPNOTSUPP);
|
||||
}
|
||||
|
||||
static int
|
||||
z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,
|
||||
struct erofs_map_blocks *map)
|
||||
{
|
||||
(void)sbi;
|
||||
(void)vi;
|
||||
(void)map;
|
||||
return (EOPNOTSUPP);
|
||||
}
|
||||
'''
|
||||
map_program = map_common
|
||||
map_program += extract_function(data_source, "erofs_inline_tail_start") + "\n"
|
||||
map_program += flatmode + "\n"
|
||||
map_program += extract_function(data_source, "erofs_map_blocks") + "\n"
|
||||
map_program += "\nint\nmain(void)\n{\n\tint error;\n"
|
||||
for case in map_fixture["map_cases"]:
|
||||
inode = case["inode"]
|
||||
map_program += f'''
|
||||
{{
|
||||
struct erofs_sb_info sbi = {{ .block_size = 4096, .blkszbits = 12 }};
|
||||
struct erofs_inode vi = {{
|
||||
.size = UINT64_C({inode['size']}),
|
||||
.inode_off = UINT64_C({inode.get('inode_off', 0)}),
|
||||
.startblk = UINT64_C({inode['startblk']}),
|
||||
.datalayout = {inode['layout']},
|
||||
.inode_isize = {inode.get('inode_isize', 0)},
|
||||
.xattr_isize = {inode.get('xattr_isize', 0)},
|
||||
}};
|
||||
struct erofs_map_blocks map = {{ .m_la = UINT64_C({case['request']}) }};
|
||||
error = erofs_map_blocks(&sbi, &vi, &map);
|
||||
printf("map\\t{case['id']}\\t%llu\\t%llu\\t%llu\\t%llu\\t%u\\t%u\\t%d\\t%d\\n",
|
||||
(unsigned long long)map.m_la, (unsigned long long)map.m_pa,
|
||||
(unsigned long long)map.m_llen, (unsigned long long)map.m_plen,
|
||||
map.m_deviceid, map.m_flags, (int)map.m_algorithmformat, error);
|
||||
}}
|
||||
'''
|
||||
map_program += "\treturn (0);\n}\n"
|
||||
map_lines = compile_and_run("B08-map-extractor", map_program)
|
||||
map_actual = {}
|
||||
for line in map_lines:
|
||||
fields = line.split("\t")
|
||||
if len(fields) != 10 or fields[0] != "map":
|
||||
raise SystemExit(f"invalid B08 map extractor line: {line!r}")
|
||||
map_actual[fields[1]] = dict(zip(
|
||||
(
|
||||
"m_la", "m_pa", "m_llen", "m_plen", "m_deviceid",
|
||||
"m_flags", "m_algorithmformat", "errno",
|
||||
),
|
||||
[int(value) for value in fields[2:]],
|
||||
strict=True,
|
||||
))
|
||||
if set(map_actual) != {case["id"] for case in map_fixture["map_cases"]}:
|
||||
raise SystemExit("B08 custom map case set changed")
|
||||
positive_runs = 0
|
||||
for case in map_fixture["map_cases"]:
|
||||
actual = map_actual[case["id"]]
|
||||
expected = {**case["expected"], "m_algorithmformat": 0}
|
||||
if actual != expected:
|
||||
raise SystemExit(
|
||||
f"B08 custom map mismatch {case['id']}: {actual!r} != {expected!r}"
|
||||
)
|
||||
if (
|
||||
actual["errno"] == 0
|
||||
and case["request"] < case["inode"]["size"]
|
||||
):
|
||||
if actual["m_llen"] == 0:
|
||||
raise SystemExit(f"B08 non-EOF zero run: {case['id']}")
|
||||
positive_runs += 1
|
||||
|
||||
maxphys = io_fixture["harness_maxphys"]
|
||||
consumer_common = f'''
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define EIO 5
|
||||
#define ENOMEM 12
|
||||
#define EINVAL 22
|
||||
#define EOPNOTSUPP 45
|
||||
#define EOVERFLOW 84
|
||||
#define EINTEGRITY 97
|
||||
#define PAGE_SIZE 4096
|
||||
#define MAXPHYS {maxphys}
|
||||
#define M_EROFS 0
|
||||
#define M_WAITOK 0
|
||||
#define EROFS_INODE_FLAT_PLAIN 0
|
||||
#define EROFS_MAP_MAPPED 0x0001
|
||||
#define EROFS_MAP_META 0x0002
|
||||
#define EROFS_BUF_INITIALIZER {{ .data = NULL, .release = NULL }}
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
#define bzero(ptr, len) memset((ptr), 0, (len))
|
||||
|
||||
typedef uint64_t erofs_off_t;
|
||||
typedef uint64_t erofs_nid_t;
|
||||
|
||||
struct erofs_sb_info {{ int unused; }};
|
||||
struct erofs_inode {{
|
||||
erofs_nid_t nid;
|
||||
uint64_t size;
|
||||
uint8_t datalayout;
|
||||
}};
|
||||
struct erofs_map_blocks {{
|
||||
erofs_off_t m_pa, m_la;
|
||||
uint64_t m_plen, m_llen;
|
||||
unsigned short m_deviceid;
|
||||
char m_algorithmformat;
|
||||
unsigned int m_flags;
|
||||
}};
|
||||
struct erofs_buf {{
|
||||
void *data;
|
||||
void (*release)(void *);
|
||||
}};
|
||||
struct uio {{
|
||||
int64_t uio_offset;
|
||||
size_t uio_resid;
|
||||
unsigned char *buffer;
|
||||
size_t moved;
|
||||
}};
|
||||
|
||||
static size_t gate_allocations;
|
||||
|
||||
static void *
|
||||
gate_malloc(size_t size)
|
||||
{{
|
||||
void *buffer = malloc(size);
|
||||
if (buffer != NULL)
|
||||
++gate_allocations;
|
||||
return (buffer);
|
||||
}}
|
||||
|
||||
static void
|
||||
gate_free(void *buffer)
|
||||
{{
|
||||
if (buffer != NULL) {{
|
||||
--gate_allocations;
|
||||
free(buffer);
|
||||
}}
|
||||
}}
|
||||
|
||||
#define malloc(size, type, flags) gate_malloc(size)
|
||||
#define free(buffer, type) gate_free(buffer)
|
||||
|
||||
#define GATE_MAX_CALLS 32
|
||||
#define GATE_PHYSICAL_BASE UINT64_C(1048576)
|
||||
static bool gate_hole;
|
||||
static unsigned int gate_map_calls;
|
||||
static unsigned int gate_physical_calls;
|
||||
static erofs_off_t gate_physical_offsets[GATE_MAX_CALLS];
|
||||
static size_t gate_physical_lengths[GATE_MAX_CALLS];
|
||||
|
||||
static void
|
||||
gate_reset(bool hole)
|
||||
{{
|
||||
gate_hole = hole;
|
||||
gate_map_calls = 0;
|
||||
gate_physical_calls = 0;
|
||||
memset(gate_physical_offsets, 0, sizeof(gate_physical_offsets));
|
||||
memset(gate_physical_lengths, 0, sizeof(gate_physical_lengths));
|
||||
}}
|
||||
|
||||
static bool
|
||||
erofs_inode_is_data_compressed(unsigned int layout)
|
||||
{{
|
||||
(void)layout;
|
||||
return (false);
|
||||
}}
|
||||
|
||||
static int
|
||||
erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,
|
||||
struct erofs_map_blocks *map)
|
||||
{{
|
||||
struct erofs_map_blocks next = {{ .m_la = map->m_la }};
|
||||
(void)sbi;
|
||||
++gate_map_calls;
|
||||
if (next.m_la < vi->size) {{
|
||||
next.m_pa = GATE_PHYSICAL_BASE + next.m_la;
|
||||
next.m_llen = vi->size - next.m_la;
|
||||
next.m_plen = next.m_llen;
|
||||
if (!gate_hole)
|
||||
next.m_flags = EROFS_MAP_MAPPED;
|
||||
}}
|
||||
*map = next;
|
||||
return (0);
|
||||
}}
|
||||
|
||||
static int
|
||||
erofs_read_physical(struct erofs_sb_info *sbi, unsigned int device_id,
|
||||
erofs_off_t off, size_t len, void **bufp)
|
||||
{{
|
||||
unsigned char *buffer;
|
||||
size_t index;
|
||||
(void)sbi;
|
||||
(void)device_id;
|
||||
if (len > MAXPHYS || gate_physical_calls == GATE_MAX_CALLS)
|
||||
return (EINVAL);
|
||||
gate_physical_offsets[gate_physical_calls] = off;
|
||||
gate_physical_lengths[gate_physical_calls] = len;
|
||||
++gate_physical_calls;
|
||||
buffer = gate_malloc(len);
|
||||
if (buffer == NULL)
|
||||
return (ENOMEM);
|
||||
for (index = 0; index < len; ++index)
|
||||
buffer[index] = (unsigned char)(off - GATE_PHYSICAL_BASE + index);
|
||||
*bufp = buffer;
|
||||
return (0);
|
||||
}}
|
||||
|
||||
static int
|
||||
erofs_read_metadata(struct erofs_sb_info *sbi, erofs_nid_t nid,
|
||||
erofs_off_t off, size_t len, struct erofs_buf *buf)
|
||||
{{
|
||||
(void)sbi;
|
||||
(void)nid;
|
||||
(void)off;
|
||||
(void)len;
|
||||
(void)buf;
|
||||
return (EIO);
|
||||
}}
|
||||
|
||||
static void
|
||||
erofs_put_metabuf(struct erofs_buf *buf)
|
||||
{{
|
||||
(void)buf;
|
||||
}}
|
||||
|
||||
static void
|
||||
erofs_brelse(void *buffer)
|
||||
{{
|
||||
gate_free(buffer);
|
||||
}}
|
||||
|
||||
static int
|
||||
uiomove(const void *source, size_t len, struct uio *uio)
|
||||
{{
|
||||
if (len > uio->uio_resid)
|
||||
return (EINVAL);
|
||||
memcpy(uio->buffer + uio->moved, source, len);
|
||||
uio->moved += len;
|
||||
uio->uio_resid -= len;
|
||||
uio->uio_offset += (int64_t)len;
|
||||
return (0);
|
||||
}}
|
||||
|
||||
static int
|
||||
z_erofs_read_data(struct erofs_sb_info *sbi, struct erofs_inode *vi,
|
||||
erofs_off_t loff, size_t len, void **bufp)
|
||||
{{
|
||||
(void)sbi;
|
||||
(void)vi;
|
||||
(void)loff;
|
||||
(void)len;
|
||||
(void)bufp;
|
||||
return (EOPNOTSUPP);
|
||||
}}
|
||||
|
||||
static int
|
||||
z_erofs_read_uio(struct erofs_sb_info *sbi, struct erofs_inode *vi,
|
||||
struct uio *uio)
|
||||
{{
|
||||
(void)sbi;
|
||||
(void)vi;
|
||||
(void)uio;
|
||||
return (EOPNOTSUPP);
|
||||
}}
|
||||
|
||||
static int
|
||||
gate_check_data(const unsigned char *buffer, size_t length,
|
||||
uint64_t offset, bool hole)
|
||||
{{
|
||||
size_t index;
|
||||
for (index = 0; index < length; ++index) {{
|
||||
unsigned char expected = hole ? 0 : (unsigned char)(offset + index);
|
||||
if (buffer[index] != expected)
|
||||
return (1);
|
||||
}}
|
||||
return (0);
|
||||
}}
|
||||
|
||||
static int
|
||||
gate_fail(const char *id, const char *reason)
|
||||
{{
|
||||
fprintf(stderr, "%s: %s\\n", id, reason);
|
||||
return (1);
|
||||
}}
|
||||
'''
|
||||
consumer_program = consumer_common
|
||||
consumer_program += read_data + "\n"
|
||||
consumer_program += read_uio + "\n"
|
||||
consumer_program += "\nint\nmain(void)\n{\n"
|
||||
for scenario in io_fixture["scenarios"]:
|
||||
scenario_id = scenario["id"]
|
||||
hole = scenario["mapping"] == "hole"
|
||||
expected_offsets = scenario["expected_physical_offsets"]
|
||||
expected_lengths = scenario["expected_physical_lengths"]
|
||||
checks = []
|
||||
for index, (offset, length) in enumerate(zip(
|
||||
expected_offsets, expected_lengths, strict=True
|
||||
)):
|
||||
checks.append(
|
||||
f"\t\tif (gate_physical_offsets[{index}] != UINT64_C({offset}) || "
|
||||
f"gate_physical_lengths[{index}] != {length})\n"
|
||||
f"\t\t\treturn (gate_fail(\"{scenario_id}\", \"physical call tuple\"));\n"
|
||||
)
|
||||
call_checks = "".join(checks)
|
||||
if scenario["api"] == "read_data":
|
||||
success_check = ""
|
||||
if scenario["expected_errno"] == 0:
|
||||
success_check = f'''
|
||||
if (buffer == NULL || gate_check_data(buffer, {scenario['length']},
|
||||
UINT64_C({scenario['offset']}), {'true' if hole else 'false'}) != 0)
|
||||
return (gate_fail("{scenario_id}", "returned data"));
|
||||
'''
|
||||
else:
|
||||
success_check = f'''
|
||||
if (buffer != NULL)
|
||||
return (gate_fail("{scenario_id}", "error buffer ownership"));
|
||||
'''
|
||||
consumer_program += f'''
|
||||
{{
|
||||
struct erofs_sb_info sbi = {{ 0 }};
|
||||
struct erofs_inode vi = {{
|
||||
.nid = 1, .size = UINT64_C({scenario['inode_size']}),
|
||||
.datalayout = EROFS_INODE_FLAT_PLAIN,
|
||||
}};
|
||||
void *buffer = NULL;
|
||||
int error;
|
||||
|
||||
gate_reset({'true' if hole else 'false'});
|
||||
error = erofs_read_data(&sbi, &vi, UINT64_C({scenario['offset']}),
|
||||
{scenario['length']}, &buffer);
|
||||
if (error != {scenario['expected_errno']})
|
||||
return (gate_fail("{scenario_id}", "errno"));
|
||||
if (gate_map_calls != {scenario['expected_map_calls']} ||
|
||||
gate_physical_calls != {len(expected_lengths)})
|
||||
return (gate_fail("{scenario_id}", "call count"));
|
||||
{call_checks}{success_check} gate_free(buffer);
|
||||
if (gate_allocations != 0)
|
||||
return (gate_fail("{scenario_id}", "allocation balance"));
|
||||
printf("io\\t{scenario_id}\\tPASS\\t%u\\t%u\\n",
|
||||
gate_map_calls, gate_physical_calls);
|
||||
}}
|
||||
'''
|
||||
elif scenario["api"] == "read_uio":
|
||||
consumer_program += f'''
|
||||
{{
|
||||
struct erofs_sb_info sbi = {{ 0 }};
|
||||
struct erofs_inode vi = {{
|
||||
.nid = 1, .size = UINT64_C({scenario['inode_size']}),
|
||||
.datalayout = EROFS_INODE_FLAT_PLAIN,
|
||||
}};
|
||||
unsigned char *buffer = gate_malloc({scenario['length']});
|
||||
struct uio uio = {{
|
||||
.uio_offset = {scenario['offset']}, .uio_resid = {scenario['length']},
|
||||
.buffer = buffer, .moved = 0,
|
||||
}};
|
||||
int error;
|
||||
|
||||
if (buffer == NULL)
|
||||
return (gate_fail("{scenario_id}", "test allocation"));
|
||||
memset(buffer, 0xa5, {scenario['length']});
|
||||
gate_reset({'true' if hole else 'false'});
|
||||
error = erofs_read_uio(&sbi, &vi, &uio);
|
||||
if (error != {scenario['expected_errno']} ||
|
||||
uio.uio_offset != {scenario['expected_final_offset']} ||
|
||||
uio.uio_resid != {scenario['expected_resid']})
|
||||
return (gate_fail("{scenario_id}", "uio result"));
|
||||
if (gate_map_calls != {scenario['expected_map_calls']} ||
|
||||
gate_physical_calls != {len(expected_lengths)})
|
||||
return (gate_fail("{scenario_id}", "call count"));
|
||||
{call_checks} if (uio.moved != {scenario['length'] - scenario['expected_resid']} ||
|
||||
gate_check_data(buffer, uio.moved, UINT64_C({scenario['offset']}),
|
||||
{'true' if hole else 'false'}) != 0)
|
||||
return (gate_fail("{scenario_id}", "moved data"));
|
||||
gate_free(buffer);
|
||||
if (gate_allocations != 0)
|
||||
return (gate_fail("{scenario_id}", "allocation balance"));
|
||||
printf("io\\t{scenario_id}\\tPASS\\t%u\\t%u\\n",
|
||||
gate_map_calls, gate_physical_calls);
|
||||
}}
|
||||
'''
|
||||
else:
|
||||
raise SystemExit(f"unknown B08 I/O API: {scenario['api']}")
|
||||
consumer_program += "\treturn (0);\n}\n"
|
||||
io_lines = compile_and_run("B08-io-extractor", consumer_program)
|
||||
expected_io_ids = [scenario["id"] for scenario in io_fixture["scenarios"]]
|
||||
actual_io_ids = []
|
||||
for line in io_lines:
|
||||
fields = line.split("\t")
|
||||
if len(fields) != 5 or fields[0] != "io" or fields[2] != "PASS":
|
||||
raise SystemExit(f"invalid B08 I/O extractor line: {line!r}")
|
||||
actual_io_ids.append(fields[1])
|
||||
if actual_io_ids != expected_io_ids:
|
||||
raise SystemExit("B08 I/O scenario order or denominator changed")
|
||||
|
||||
cleanup = Counter(
|
||||
(record["actual"]["acquire_count"], record["actual"]["release_count"])
|
||||
for record in tuples["records"]
|
||||
)
|
||||
if any(acquire != release for acquire, release in cleanup):
|
||||
raise SystemExit(f"B08 metadata cleanup became unbalanced: {cleanup!r}")
|
||||
if any(record["actual"]["errno"] < 0 for record in tuples["records"]):
|
||||
raise SystemExit("B08 observed a negative errno")
|
||||
|
||||
summary = {
|
||||
"status": "PASS",
|
||||
"final_id": "P15-074",
|
||||
"b07_map_cases": 80,
|
||||
"b07_unchanged_tuples": 80 - len(changed_ids),
|
||||
"b07_intended_run_tuples": len(changed_ids),
|
||||
"b07_unexpected_tuple_differences": 0,
|
||||
"b07_device_cases": 13,
|
||||
"b07_device_differences": 0,
|
||||
"b07_model_sha256": result["model_sha256"],
|
||||
"transformed_tuple_bytes": len(actual_bytes),
|
||||
"transformed_tuple_sha256": sha256_bytes(bytes(actual_bytes)),
|
||||
"custom_map_cases": len(map_fixture["map_cases"]),
|
||||
"custom_positive_runs": positive_runs,
|
||||
"io_scenarios": len(io_fixture["scenarios"]),
|
||||
"harness_maxphys": maxphys,
|
||||
"max_observed_physical_io": max(
|
||||
length
|
||||
for scenario in io_fixture["scenarios"]
|
||||
for length in scenario["expected_physical_lengths"]
|
||||
),
|
||||
"cleanup_distribution": {
|
||||
f"{acquire}:{release}": count
|
||||
for (acquire, release), count in sorted(cleanup.items())
|
||||
},
|
||||
"compressed_scope": "UNCHANGED",
|
||||
"vnode_backed_scope": "NOT_ADDED",
|
||||
"qemu": "NOT_RUN",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
}
|
||||
with output.open("x", encoding="ascii") as stream:
|
||||
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
print(
|
||||
"B08 PASS map=80 unchanged=73 intended=7 device=13 "
|
||||
f"custom={len(map_fixture['map_cases'])} io={len(io_fixture['scenarios'])}"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B08 plain-run/MAXPHYS boundary check failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/baseline/result.json" \
|
||||
"$artifacts/baseline/tuples.json" "$artifacts/baseline/devices.json" \
|
||||
"$artifacts/candidate/result.json" "$artifacts/candidate/tuples.json" \
|
||||
"$artifacts/candidate/devices.json" "$artifacts/B08-result.json" \
|
||||
"$artifacts/B08-map-extractor" "$artifacts/B08-io-extractor" \
|
||||
>"$artifacts/SHA256SUMS"
|
||||
printf 'B08 PASS contiguous plain/pre-inline runs with MAXPHYS-capped I/O\n'
|
||||
Executable
+375
@@ -0,0 +1,375 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=8e85a8b64dd1a956376ff1cb31d2157bc73df8ad
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
inode=$PRE15_DUT/src/inode.c
|
||||
vnops=$PRE15_DUT/src/erofs_vnops.c
|
||||
super=$PRE15_DUT/src/super.c
|
||||
internal=$PRE15_DUT/src/internal.h
|
||||
namei=$PRE15_DUT/src/namei.c
|
||||
|
||||
for tool in git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B09 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b09-inode "$inode"
|
||||
pre15_record_fixture b09-vnops "$vnops"
|
||||
pre15_record_fixture b09-super "$super"
|
||||
pre15_record_fixture b09-internal "$internal"
|
||||
pre15_record_fixture b09-namei "$namei"
|
||||
pre15_record_fixture b09-case "$PRE15_DUT/tests/pre15/cases/B09-vnode.sh"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$artifacts" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
artifacts = Path(sys.argv[4])
|
||||
src = dut / "src"
|
||||
|
||||
|
||||
def committed(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B09 baseline {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def replace_once(source: str, old: str, new: str, label: str) -> str:
|
||||
count = source.count(old)
|
||||
if count != 1:
|
||||
raise SystemExit(f"{label}: expected one transform source, found {count}")
|
||||
return source.replace(old, new, 1)
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, match.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", match.end())
|
||||
if brace < 0:
|
||||
raise SystemExit(f"missing function body: {name}")
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def initializer(source: str, declaration: str, prefix: str) -> list[tuple[str, str]]:
|
||||
match = re.search(
|
||||
re.escape(declaration) + r"\s*=\s*\{(.*?)\n\};", source, re.DOTALL
|
||||
)
|
||||
if not match:
|
||||
raise SystemExit(f"initializer absent: {declaration}")
|
||||
return re.findall(
|
||||
rf"^\s*\.({prefix}_[A-Za-z0-9_]+)\s*=\s*([^,]+),",
|
||||
match.group(1),
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def require_order(source: str, markers: list[str], label: str) -> None:
|
||||
position = -1
|
||||
for marker in markers:
|
||||
position = source.find(marker, position + 1)
|
||||
if position < 0:
|
||||
raise SystemExit(f"{label} is missing ordered marker: {marker}")
|
||||
|
||||
|
||||
current = {
|
||||
name: (src / name).read_text(encoding="utf-8")
|
||||
for name in ("inode.c", "erofs_vnops.c", "super.c", "internal.h", "namei.c")
|
||||
}
|
||||
base = {name: committed(name) for name in current}
|
||||
|
||||
moved_marker = "static u_int\nerofs_vfs_hash(erofs_nid_t nid)"
|
||||
moved_offset = base["inode.c"].find(moved_marker)
|
||||
if moved_offset < 0:
|
||||
raise SystemExit("B09 baseline vnode adapter block is absent")
|
||||
moved_block = base["inode.c"][moved_offset:]
|
||||
expected_inode = base["inode.c"][:moved_offset].rstrip("\n") + "\n"
|
||||
if current["inode.c"] != expected_inode:
|
||||
raise SystemExit("inode.c changed outside the exact vnode adapter movement")
|
||||
if current["internal.h"] != base["internal.h"]:
|
||||
raise SystemExit("internal.h changed despite no B09 interface delta")
|
||||
if current["namei.c"] != base["namei.c"]:
|
||||
raise SystemExit("namecache/lookup code changed outside the B09 write set")
|
||||
|
||||
expected_vnops = replace_once(
|
||||
base["erofs_vnops.c"],
|
||||
"#include <sys/extattr.h>\n",
|
||||
"#include <sys/extattr.h>\n#include <sys/fnv_hash.h>\n",
|
||||
"vnode hash include",
|
||||
)
|
||||
old_open = """\tif (vp->v_type == VREG) {
|
||||
\t\tif (vnode_create_vobject(vp, vi->size, ap->a_td) != 0)
|
||||
\t\t\treturn (ENOMEM);
|
||||
\t}
|
||||
"""
|
||||
new_open = """\tif (vp->v_type == VREG)
|
||||
\t\tvnode_create_vobject(vp, vi->size, ap->a_td);
|
||||
"""
|
||||
expected_vnops = replace_once(expected_vnops, old_open, new_open, "pager dead branch")
|
||||
expected_vnops = replace_once(
|
||||
expected_vnops,
|
||||
"static int\nerofs_reclaim(struct vop_reclaim_args *ap)",
|
||||
moved_block + "\nstatic int\nerofs_reclaim(struct vop_reclaim_args *ap)",
|
||||
"vnode adapter placement",
|
||||
)
|
||||
if current["erofs_vnops.c"] != expected_vnops:
|
||||
raise SystemExit("erofs_vnops.c differs from the two declared B09 transforms")
|
||||
|
||||
expected_super = replace_once(
|
||||
base["super.c"], "static vfs_vget_t erofs_vgetf;\n", "", "vget declaration"
|
||||
)
|
||||
expected_super = replace_once(
|
||||
expected_super,
|
||||
"""static int
|
||||
erofs_vgetf(struct mount *mp, ino_t ino, int flags, struct vnode **vpp)
|
||||
{
|
||||
\treturn (erofs_vget(mp, ino, flags, vpp));
|
||||
}
|
||||
|
||||
""",
|
||||
"",
|
||||
"vget wrapper",
|
||||
)
|
||||
expected_super = replace_once(
|
||||
expected_super,
|
||||
"\t.vfs_vget = erofs_vgetf,",
|
||||
"\t.vfs_vget = erofs_vget,",
|
||||
"vfs_vget slot",
|
||||
)
|
||||
if current["super.c"] != expected_super:
|
||||
raise SystemExit("super.c differs from the exact direct-vget transform")
|
||||
|
||||
moved_functions = (
|
||||
"erofs_vfs_hash",
|
||||
"erofs_vfs_hash_cmp",
|
||||
"erofs_fill_vnode",
|
||||
"erofs_vget",
|
||||
)
|
||||
for name in moved_functions:
|
||||
if extract_function(base["inode.c"], name) != extract_function(
|
||||
current["erofs_vnops.c"], name
|
||||
):
|
||||
raise SystemExit(f"moved function changed semantics: {name}")
|
||||
definitions = {
|
||||
path: len(re.findall(r"^" + re.escape(name) + r"\s*\(", text, re.MULTILINE))
|
||||
for path, text in current.items()
|
||||
}
|
||||
if definitions["erofs_vnops.c"] != 1 or sum(definitions.values()) != 1:
|
||||
raise SystemExit(f"B09 function ownership is not unique: {name} {definitions}")
|
||||
|
||||
base_vnode_slots = initializer(
|
||||
base["erofs_vnops.c"], "struct vop_vector erofs_vnodeops", "vop"
|
||||
)
|
||||
base_fifo_slots = initializer(
|
||||
base["erofs_vnops.c"], "struct vop_vector erofs_fifoops", "vop"
|
||||
)
|
||||
vnode_slots = initializer(
|
||||
current["erofs_vnops.c"], "struct vop_vector erofs_vnodeops", "vop"
|
||||
)
|
||||
fifo_slots = initializer(
|
||||
current["erofs_vnops.c"], "struct vop_vector erofs_fifoops", "vop"
|
||||
)
|
||||
base_vfs_slots = initializer(base["super.c"], "static struct vfsops erofs_vfsops", "vfs")
|
||||
vfs_slots = initializer(current["super.c"], "static struct vfsops erofs_vfsops", "vfs")
|
||||
expected_vfs_slots = [
|
||||
(slot, "erofs_vget" if slot == "vfs_vget" else target)
|
||||
for slot, target in base_vfs_slots
|
||||
]
|
||||
if vnode_slots != base_vnode_slots or fifo_slots != base_fifo_slots:
|
||||
raise SystemExit("FreeBSD KOBJ VOP slot order or targets changed")
|
||||
if vfs_slots != expected_vfs_slots:
|
||||
raise SystemExit("VFS slots changed beyond direct erofs_vget registration")
|
||||
if ("vop_lookup", "vfs_cache_lookup") not in vnode_slots or (
|
||||
"vop_cachedlookup", "erofs_lookup"
|
||||
) not in vnode_slots:
|
||||
raise SystemExit("FreeBSD namecache slots were not preserved")
|
||||
if "erofs_vgetf" in current["super.c"]:
|
||||
raise SystemExit("trivial erofs_vgetf wrapper remains")
|
||||
|
||||
vget = extract_function(current["erofs_vnops.c"], "erofs_vget")
|
||||
require_order(
|
||||
vget,
|
||||
[
|
||||
"td = curthread;",
|
||||
"nid = (uint64_t)ino;",
|
||||
"shared = (flags & LK_TYPE_MASK) == LK_SHARED;",
|
||||
"hash = erofs_vfs_hash(nid);",
|
||||
"error = vfs_hash_get(",
|
||||
"if (error != 0 || *vpp != NULL)",
|
||||
"sbi = MTOE(mp);",
|
||||
"vi = malloc(sizeof(*vi), M_EROFS, M_WAITOK | M_ZERO);",
|
||||
"error = getnewvnode(\"erofs\", mp, &erofs_vnodeops, &vp);",
|
||||
"vp->v_data = vi;",
|
||||
"vi->nid = nid;",
|
||||
"lockmgr(vp->v_vnlock, LK_EXCLUSIVE, NULL);",
|
||||
"error = insmntque(vp, mp);",
|
||||
"error = vfs_hash_insert(",
|
||||
"if (error != 0 || *vpp != NULL)",
|
||||
"error = erofs_read_inode(sbi, nid, vi);",
|
||||
"vgone(vp);",
|
||||
"vput(vp);",
|
||||
"erofs_fill_vnode(sbi, vp, vi);",
|
||||
"vn_set_state(vp, VSTATE_CONSTRUCTED);",
|
||||
"if (shared)",
|
||||
"VOP_LOCK(vp, LK_DOWNGRADE);",
|
||||
"*vpp = vp;",
|
||||
],
|
||||
"erofs_vget lifecycle",
|
||||
)
|
||||
if vget.count("erofs_vfs_hash_cmp") != 2:
|
||||
raise SystemExit("vget no longer uses one comparator at both hash callsites")
|
||||
if vget.count("free(vi, M_EROFS);") != 2 or vget.count("*vpp = NULL;") != 3:
|
||||
raise SystemExit("vget allocation/insmntque/read failure cleanup changed")
|
||||
if vget.count("vgone(vp);") != 1 or vget.count("vput(vp);") != 1:
|
||||
raise SystemExit("vget failed-inode vnode cleanup changed")
|
||||
if re.search(r"return\s*\(\s*-", vget):
|
||||
raise SystemExit("negative errno entered the FreeBSD vget path")
|
||||
|
||||
reclaim = extract_function(current["erofs_vnops.c"], "erofs_reclaim")
|
||||
if reclaim != extract_function(base["erofs_vnops.c"], "erofs_reclaim"):
|
||||
raise SystemExit("reclaim/hash removal semantics changed")
|
||||
require_order(
|
||||
reclaim,
|
||||
["vi = VTOE(vp);", "vfs_hash_remove(vp);", "free(vi, M_EROFS);", "vp->v_data = NULL;"],
|
||||
"erofs_reclaim lifecycle",
|
||||
)
|
||||
|
||||
open_body = extract_function(current["erofs_vnops.c"], "erofs_open")
|
||||
if open_body.count("vnode_create_vobject(vp, vi->size, ap->a_td);") != 1:
|
||||
raise SystemExit("regular-vnode pager setup is not a single direct call")
|
||||
if "ENOMEM" in open_body or "vnode_create_vobject" not in open_body:
|
||||
raise SystemExit("dead pager errno folding remains")
|
||||
|
||||
slot_report = {
|
||||
"status": "PASS",
|
||||
"vnode": [{"slot": slot, "target": target} for slot, target in vnode_slots],
|
||||
"fifo": [{"slot": slot, "target": target} for slot, target in fifo_slots],
|
||||
"vfs": [{"slot": slot, "target": target} for slot, target in vfs_slots],
|
||||
}
|
||||
(artifacts / "B09-vop-slots.json").write_text(
|
||||
json.dumps(slot_report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
|
||||
callgraph = [
|
||||
"VFS root -> erofs_vget",
|
||||
"namecache cachedlookup -> erofs_lookup -> erofs_vget",
|
||||
"VFS vget slot -> erofs_vget",
|
||||
"erofs_vget -> vfs_hash_get[erofs_vfs_hash_cmp]",
|
||||
"erofs_vget -> getnewvnode -> LK_EXCLUSIVE -> insmntque",
|
||||
"erofs_vget -> vfs_hash_insert[erofs_vfs_hash_cmp]",
|
||||
"erofs_vget -> erofs_read_inode -> erofs_fill_vnode",
|
||||
"erofs_vget -> VSTATE_CONSTRUCTED -> optional LK_DOWNGRADE",
|
||||
"erofs_reclaim -> vfs_hash_remove -> free inode",
|
||||
]
|
||||
(artifacts / "B09-vget-callgraph.txt").write_text(
|
||||
"\n".join(callgraph) + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
result = {
|
||||
"status": "PASS",
|
||||
"final_ids": ["P15-008", "P15-050", "P15-088"],
|
||||
"baseline": baseline,
|
||||
"moved_functions": list(moved_functions),
|
||||
"moved_functions_byte_identical": True,
|
||||
"vnode_slots": len(vnode_slots),
|
||||
"fifo_slots": len(fifo_slots),
|
||||
"vfs_slots": len(vfs_slots),
|
||||
"hash_comparator_callsites": 2,
|
||||
"exclusive_construct_lock": True,
|
||||
"shared_result_downgrade": True,
|
||||
"insmntque_cleanup_preserved": True,
|
||||
"reclaim_hash_removal_preserved": True,
|
||||
"namecache_slots_preserved": True,
|
||||
"positive_errno_preserved": True,
|
||||
"b11_b15_scope": "NOT_INCLUDED",
|
||||
"qemu": "NOT_RUN",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
}
|
||||
(artifacts / "B09-result.json").write_text(
|
||||
json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
print(
|
||||
f"B09 PASS moved={len(moved_functions)} "
|
||||
f"slots={len(vnode_slots)}+{len(fifo_slots)}/{len(vfs_slots)} hash=2"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B09 vnode ownership, VOP slots, or vget graph check failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/B09-vop-slots.json" \
|
||||
"$artifacts/B09-vget-callgraph.txt" "$artifacts/B09-result.json" \
|
||||
> "$artifacts/SHA256SUMS"
|
||||
printf 'B09 PASS vnode adapter ownership and FreeBSD lifecycle preserved\n'
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=22965aea267c3fcaafb67c14f9a246a7bc6195b1
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B10-directory-spec.json
|
||||
oracle=$fixture_dir/B10-directory-oracle.py
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
report=$artifacts/B10-directory-results.json
|
||||
|
||||
for tool in git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B10 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b10-spec "$spec"
|
||||
pre15_record_fixture b10-oracle "$oracle"
|
||||
pre15_record_fixture b10-case "$PRE15_DUT/tests/pre15/cases/B10-directory.sh"
|
||||
pre15_record_fixture b10-dir "$PRE15_DUT/src/dir.c"
|
||||
pre15_record_fixture b10-namei "$PRE15_DUT/src/namei.c"
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 -B "$oracle" --root "$PRE15_ROOT" --dut "$PRE15_DUT" \
|
||||
--baseline "$baseline" --spec "$spec" --report "$report"
|
||||
then
|
||||
pre15_record_fixture b10-report "$report"
|
||||
else
|
||||
pre15_dut_fail 'B10 directory trust-boundary oracle failed'
|
||||
fi
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=e2e3fb86b6fffcb01d6fd29c17dd95628ad070de
|
||||
gate=$PRE15_DUT/tests/pre15/gates/P15-081.sh
|
||||
input=$PRE15_DUT/tests/pre15/gates/P15-081-input.json
|
||||
spec=$PRE15_DUT/tests/pre15/fixtures/B11-dtype-spec.json
|
||||
oracle=$PRE15_DUT/tests/pre15/fixtures/B11-dtype-oracle.py
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
gate_output=$artifacts/gate
|
||||
|
||||
for tool in cc dump.erofs fsck.erofs git mkfs.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B11 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b11-gate "$gate"
|
||||
pre15_record_fixture b11-gate-input "$input"
|
||||
pre15_record_fixture b11-spec "$spec"
|
||||
pre15_record_fixture b11-oracle "$oracle"
|
||||
for source in inode.c internal.h namei.c dir.c erofs_vnops.c; do
|
||||
pre15_record_fixture "b11-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
if "$gate" --base "$baseline" --output "$gate_output" \
|
||||
>"$artifacts/gate.stdout" 2>"$artifacts/gate.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'P15-081 frozen gate replay failed'
|
||||
fi
|
||||
|
||||
pre15_target_reached
|
||||
if python3 -B "$oracle" --root "$PRE15_ROOT" --dut "$PRE15_DUT" \
|
||||
--spec "$spec" --gate-output "$gate_output" --artifacts "$artifacts"
|
||||
then
|
||||
pre15_record_fixture b11-report "$artifacts/B11-dtype-report.json"
|
||||
else
|
||||
pre15_dut_fail 'B11 dtype source or fixture oracle failed'
|
||||
fi
|
||||
Executable
+217
@@ -0,0 +1,217 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
policy=$fixture_dir/B12-readahead-policy.json
|
||||
kld_builder=$fixture_dir/B12-build-kld.sh
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in awk clang file git nm python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B12 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b12-policy "$policy"
|
||||
pre15_record_fixture b12-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b12-case "$PRE15_DUT/tests/pre15/cases/B12-readahead.sh"
|
||||
for source in data.c dir.c internal.h; do
|
||||
pre15_record_fixture "b12-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$policy" \
|
||||
"$artifacts/B12-policy-report.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
policy_path = Path(sys.argv[3])
|
||||
report_path = Path(sys.argv[4])
|
||||
policy = json.loads(policy_path.read_text(encoding="ascii"))
|
||||
if policy.get("schema") != 1 or policy.get("candidate") != "P15-087":
|
||||
raise SystemExit("invalid B12 policy identity")
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
source_hashes = {
|
||||
relative: digest(dut / relative)
|
||||
for relative in policy["source_sha256"]
|
||||
}
|
||||
if source_hashes != policy["source_sha256"]:
|
||||
raise SystemExit("B12 production source differs from the accepted source set")
|
||||
|
||||
changed = subprocess.run(
|
||||
[
|
||||
"git", "-C", str(root), "diff", "--name-only",
|
||||
policy["baseline"], "--", "repo-pre-15/src",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
expected_changed = [
|
||||
"repo-pre-15/src/data.c",
|
||||
"repo-pre-15/src/dir.c",
|
||||
"repo-pre-15/src/internal.h",
|
||||
]
|
||||
if sorted(changed) != expected_changed:
|
||||
raise SystemExit(f"B12 production write set mismatch: {changed}")
|
||||
|
||||
data = (dut / "src/data.c").read_text(encoding="utf-8")
|
||||
directory = (dut / "src/dir.c").read_text(encoding="utf-8")
|
||||
internal = (dut / "src/internal.h").read_text(encoding="utf-8")
|
||||
if "cluster_read(" in data + directory + internal:
|
||||
raise SystemExit("B12 must not introduce cluster_read")
|
||||
if data.count("breadn(") != 1:
|
||||
raise SystemExit("B12 must contain one backing-vnode breadn call")
|
||||
if not re.search(r"breadn\(dif->devvp,", data):
|
||||
raise SystemExit("B12 breadn is not issued on erofs_device_info.devvp")
|
||||
required_data = (
|
||||
"#define EROFS_DIR_READAHEAD_BYTES\t(1024 * 1024)",
|
||||
"#define EROFS_DIR_READAHEAD_SLOTS\t(EROFS_DIR_READAHEAD_BYTES / PAGE_SIZE)",
|
||||
"future.m_dif != current.m_dif",
|
||||
"future.m_pa != current.m_pa + step",
|
||||
"vi->datalayout == EROFS_INODE_FLAT_PLAIN",
|
||||
"return (erofs_read_data_impl(sbi, vi, loff, len, 0, bufp));",
|
||||
)
|
||||
for marker in required_data:
|
||||
if marker not in data:
|
||||
raise SystemExit(f"missing B12 data policy marker: {marker}")
|
||||
if directory.count("erofs_read_data_readahead(") != 1:
|
||||
raise SystemExit("B12 directory hint is not limited to readdir")
|
||||
if "sequential = logical_off == 0;" not in directory:
|
||||
raise SystemExit("B12 random seek no-op predicate is absent")
|
||||
if internal.count("erofs_read_data_readahead(") != 1:
|
||||
raise SystemExit("B12 internal prototype is absent or duplicated")
|
||||
|
||||
evidence = root / policy["gate_evidence"]
|
||||
result_path = evidence / "result.json"
|
||||
rows_path = evidence / "runs.tsv"
|
||||
if digest(result_path) != policy["gate_result_sha256"]:
|
||||
raise SystemExit("B12 gate result hash mismatch")
|
||||
if digest(rows_path) != policy["gate_rows_sha256"]:
|
||||
raise SystemExit("B12 gate rows hash mismatch")
|
||||
result = json.loads(result_path.read_text(encoding="ascii"))
|
||||
if result.get("status") != "GO" or result.get("b12") != "AUTHORIZED":
|
||||
raise SystemExit("P15-087 did not authorize B12")
|
||||
if result.get("qemu") != "PASS" or not all(result.get("checks", {}).values()):
|
||||
raise SystemExit("P15-087 runtime or semantic checks are incomplete")
|
||||
thresholds = result.get("thresholds", {})
|
||||
if thresholds != {
|
||||
"maximum_extra_provider_reads_percent": policy["maximum_extra_provider_reads_percent"],
|
||||
"minimum_cold_median_improvement_percent": policy["minimum_cold_median_improvement_percent"],
|
||||
}:
|
||||
raise SystemExit("P15-087 thresholds differ from the frozen B12 policy")
|
||||
|
||||
rows = []
|
||||
with rows_path.open(encoding="ascii", newline="") as source:
|
||||
for raw in csv.reader(source, delimiter="\t"):
|
||||
if len(raw) != 10:
|
||||
raise SystemExit(f"invalid B12 gate row: {raw}")
|
||||
rows.append({
|
||||
"variant": raw[0],
|
||||
"run": raw[1],
|
||||
"mode": raw[3],
|
||||
"elapsed_ns": int(raw[4]),
|
||||
"provider_reads": int(raw[5]),
|
||||
"entry_count": int(raw[7]),
|
||||
"hash": raw[8],
|
||||
"final_cookie": int(raw[9]),
|
||||
})
|
||||
sequential = [row for row in rows if row["mode"] == "sequential"]
|
||||
random_rows = [row for row in rows if row["mode"] == "random"]
|
||||
baseline = [row for row in sequential if row["variant"] == "baseline"]
|
||||
candidate = [row for row in sequential if row["variant"] == "candidate"]
|
||||
if len(baseline) != 5 or len(candidate) != 5 or len(random_rows) != 2:
|
||||
raise SystemExit("B12 gate does not contain the complete 5+5 and 1+1 sample set")
|
||||
baseline_ns = statistics.median(row["elapsed_ns"] for row in baseline)
|
||||
candidate_ns = statistics.median(row["elapsed_ns"] for row in candidate)
|
||||
improvement = (baseline_ns - candidate_ns) * 100.0 / baseline_ns
|
||||
baseline_reads = statistics.median(row["provider_reads"] for row in baseline)
|
||||
candidate_reads = statistics.median(row["provider_reads"] for row in candidate)
|
||||
extra_reads = (candidate_reads - baseline_reads) * 100.0 / baseline_reads
|
||||
if improvement < policy["minimum_cold_median_improvement_percent"]:
|
||||
raise SystemExit("B12 recomputed cold median improvement misses the gate")
|
||||
if extra_reads > policy["maximum_extra_provider_reads_percent"]:
|
||||
raise SystemExit("B12 recomputed provider-read delta misses the gate")
|
||||
if any(
|
||||
(row["entry_count"], row["hash"], row["final_cookie"])
|
||||
!= (18002, "427bb414efa99dc0", 3880368)
|
||||
for row in sequential
|
||||
):
|
||||
raise SystemExit("B12 sequential correctness rows differ from the oracle")
|
||||
if {
|
||||
(row["provider_reads"], row["entry_count"], row["hash"], row["final_cookie"])
|
||||
for row in random_rows
|
||||
} != {(19, 292, "9805eadece9f500e", 1998932)}:
|
||||
raise SystemExit("B12 random path is not an exact no-op pair")
|
||||
|
||||
report = {
|
||||
"baseline_samples": len(baseline),
|
||||
"candidate_samples": len(candidate),
|
||||
"baseline_median_elapsed_ns": baseline_ns,
|
||||
"candidate_median_elapsed_ns": candidate_ns,
|
||||
"cold_median_improvement_percent": improvement,
|
||||
"extra_provider_reads_percent": extra_reads,
|
||||
"gate": "GO",
|
||||
"qemu_runtime": "PASS",
|
||||
"random_samples": len(random_rows),
|
||||
"source_sha256": source_hashes,
|
||||
"status": "PASS",
|
||||
"window_bytes": policy["maximum_readahead_bytes"],
|
||||
}
|
||||
report_path.write_text(
|
||||
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B12 source, write-set, or frozen G11 policy replay failed'
|
||||
fi
|
||||
pre15_record_fixture b12-policy-report "$artifacts/B12-policy-report.json"
|
||||
|
||||
module=$PRE15_CASE_TMP/B12-erofs-zstdio0.ko
|
||||
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
||||
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" \
|
||||
>"$artifacts/B12-kld-build.stdout" 2>"$artifacts/B12-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B12 exact-ABI zstdio0 KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/B12-kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B12-kld-sha256.txt"
|
||||
nm -g "$module" | LC_ALL=C sort >"$artifacts/B12-kld-nm-global.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/B12-kld-nm-u.txt"
|
||||
if ! awk '$NF == "erofs_read_data_readahead" { found = 1 } END { exit !found }' \
|
||||
"$artifacts/B12-kld-nm-global.txt"; then
|
||||
pre15_dut_fail 'B12 exact-ABI KLD lacks the directory readahead symbol'
|
||||
fi
|
||||
if grep -q ' ZSTD_' "$artifacts/B12-kld-nm-u.txt"; then
|
||||
pre15_dut_fail 'B12 zstdio0 KLD contains an unexpected Zstd symbol'
|
||||
fi
|
||||
|
||||
printf '%s\n' \
|
||||
'B12-policy PASS: exact source/write set and frozen 5+5 G11 statistics' \
|
||||
'K zstdio0 PASS: FreeBSD 15 exact-ABI cross-KLD and symbol contract' \
|
||||
'TC183 correctness/performance PASS from immutable P15-087 QEMU evidence'
|
||||
Executable
+604
@@ -0,0 +1,604 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=08a567554881f19bcec933a92a46d18cfb73d22e
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
compact_fixture=$PRE15_DUT/tests/pre15/fixtures/B13-compact-time.json
|
||||
extended_fixture=$PRE15_DUT/tests/pre15/fixtures/B13-extended-time.json
|
||||
inode=$PRE15_DUT/src/inode.c
|
||||
internal=$PRE15_DUT/src/internal.h
|
||||
super=$PRE15_DUT/src/super.c
|
||||
ondisk=$PRE15_DUT/src/erofs_fs.h
|
||||
vnops=$PRE15_DUT/src/erofs_vnops.c
|
||||
|
||||
for tool in cc git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B13 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b13-compact "$compact_fixture"
|
||||
pre15_record_fixture b13-extended "$extended_fixture"
|
||||
pre15_record_fixture b13-inode "$inode"
|
||||
pre15_record_fixture b13-internal "$internal"
|
||||
pre15_record_fixture b13-super "$super"
|
||||
pre15_record_fixture b13-ondisk "$ondisk"
|
||||
pre15_record_fixture b13-vnops "$vnops"
|
||||
pre15_record_fixture b13-case "$PRE15_DUT/tests/pre15/cases/B13-time.sh"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$compact_fixture" \
|
||||
"$extended_fixture" "$artifacts" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
compact_path = Path(sys.argv[4])
|
||||
extended_path = Path(sys.argv[5])
|
||||
artifacts = Path(sys.argv[6])
|
||||
src = dut / "src"
|
||||
eintegrity = 97
|
||||
int64_min = -(1 << 63)
|
||||
int64_max = (1 << 63) - 1
|
||||
int32_min = -(1 << 31)
|
||||
int32_max = (1 << 31) - 1
|
||||
|
||||
|
||||
def committed(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B13 baseline {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def replace_once(source: str, old: str, new: str, label: str) -> str:
|
||||
count = source.count(old)
|
||||
if count != 1:
|
||||
raise SystemExit(f"{label}: expected one transform source, found {count}")
|
||||
return source.replace(old, new, 1)
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, match.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", match.end())
|
||||
if brace < 0:
|
||||
raise SystemExit(f"missing function body: {name}")
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def load_fixture(path: Path, expected_encoding: set[str]) -> dict:
|
||||
fixture = json.loads(path.read_text(encoding="ascii"))
|
||||
if fixture.get("schema") != 1 or fixture.get("candidate") != "P15-018":
|
||||
raise SystemExit(f"invalid B13 fixture identity: {path.name}")
|
||||
if set(fixture.get("encoding", {})) != expected_encoding:
|
||||
raise SystemExit(f"invalid B13 fixture encoding keys: {path.name}")
|
||||
if not isinstance(fixture.get("cases"), list) or not fixture["cases"]:
|
||||
raise SystemExit(f"empty B13 fixture: {path.name}")
|
||||
return fixture
|
||||
|
||||
|
||||
def decode_le(raw: str, width: int, *, signed: bool) -> int:
|
||||
if not re.fullmatch(r"[0-9a-f]+", raw) or len(raw) != width * 2:
|
||||
raise SystemExit(f"invalid {width}-byte lowercase hex value: {raw!r}")
|
||||
return int.from_bytes(bytes.fromhex(raw), byteorder="little", signed=signed)
|
||||
|
||||
|
||||
def status_for_range(value: int, minimum: int, maximum: int) -> str:
|
||||
return "PASS" if minimum <= value <= maximum else "EINTEGRITY"
|
||||
|
||||
|
||||
current = {
|
||||
name: (src / name).read_text(encoding="utf-8")
|
||||
for name in ("internal.h", "inode.c", "super.c", "erofs_fs.h", "erofs_vnops.c")
|
||||
}
|
||||
base = {name: committed(name) for name in current}
|
||||
|
||||
expected_internal = replace_once(
|
||||
base["internal.h"], "\tuint64_t epoch;", "\tint64_t epoch;", "signed epoch field"
|
||||
)
|
||||
expected_internal = replace_once(
|
||||
expected_internal, "\tuint64_t mtime;", "\ttime_t mtime;", "time_t inode field"
|
||||
)
|
||||
if current["internal.h"] != expected_internal:
|
||||
raise SystemExit("internal.h differs from the two declared B13 type changes")
|
||||
|
||||
old_set_timestamp = """static int
|
||||
erofs_set_timestamp(struct erofs_inode *vi, uint64_t seconds,
|
||||
uint32_t nanoseconds)
|
||||
{
|
||||
\tif (nanoseconds >= 1000000000 || seconds > (uint64_t)INT64_MAX)
|
||||
\t\treturn (EINTEGRITY);
|
||||
\tvi->mtime = seconds;
|
||||
\tvi->mtime_nsec = nanoseconds;
|
||||
\treturn (0);
|
||||
}
|
||||
"""
|
||||
new_set_timestamp = """static int
|
||||
erofs_set_timestamp(struct erofs_inode *vi, int64_t seconds,
|
||||
uint32_t nanoseconds)
|
||||
{
|
||||
\ttime_t mtime;
|
||||
|
||||
\tif (nanoseconds >= 1000000000 ||
|
||||
\t __builtin_add_overflow(seconds, 0, &mtime))
|
||||
\t\treturn (EINTEGRITY);
|
||||
\tvi->mtime = mtime;
|
||||
\tvi->mtime_nsec = nanoseconds;
|
||||
\treturn (0);
|
||||
}
|
||||
"""
|
||||
expected_inode = replace_once(
|
||||
base["inode.c"], old_set_timestamp, new_set_timestamp, "timestamp conversion"
|
||||
)
|
||||
expected_inode = replace_once(
|
||||
expected_inode,
|
||||
"\tuint64_t addrmask, mtime;",
|
||||
"\tuint64_t addrmask;\n\tint64_t mtime;",
|
||||
"signed decoded mtime",
|
||||
)
|
||||
expected_inode = replace_once(
|
||||
expected_inode,
|
||||
"\t\t (uint64_t)le32toh(dic->i_mtime), &mtime)",
|
||||
"\t\t (int64_t)le32toh(dic->i_mtime), &mtime)",
|
||||
"compact signed checked add",
|
||||
)
|
||||
expected_inode = replace_once(
|
||||
expected_inode,
|
||||
"\t\terror = erofs_set_timestamp(vi, le64toh(die->i_mtime),\n"
|
||||
"\t\t le32toh(die->i_mtime_nsec));",
|
||||
"\t\terror = erofs_set_timestamp(vi,\n"
|
||||
"\t\t (int64_t)le64toh(die->i_mtime),\n"
|
||||
"\t\t le32toh(die->i_mtime_nsec));",
|
||||
"extended signed decode",
|
||||
)
|
||||
if current["inode.c"] != expected_inode:
|
||||
raise SystemExit("inode.c differs from the four declared B13 transforms")
|
||||
|
||||
expected_super = replace_once(
|
||||
base["super.c"],
|
||||
"\tsbi->epoch = le64toh(dsb->epoch);",
|
||||
"\tsbi->epoch = (int64_t)le64toh(dsb->epoch);",
|
||||
"super signed epoch decode",
|
||||
)
|
||||
if current["super.c"] != expected_super:
|
||||
raise SystemExit("super.c differs from the exact signed epoch transform")
|
||||
if current["erofs_fs.h"] != base["erofs_fs.h"]:
|
||||
raise SystemExit("B13 changed ondisk endian types, widths, or layout")
|
||||
if current["erofs_vnops.c"] != base["erofs_vnops.c"]:
|
||||
raise SystemExit("B13 changed FreeBSD VOP/timespec publication code")
|
||||
|
||||
ondisk_required = (
|
||||
"\t__le64 epoch;",
|
||||
"\t__le32 i_mtime;",
|
||||
"\t__le64 i_mtime;",
|
||||
"\t__le32 i_mtime_nsec;",
|
||||
)
|
||||
for declaration in ondisk_required:
|
||||
if current["erofs_fs.h"].count(declaration) != 1:
|
||||
raise SystemExit(f"ondisk timestamp declaration changed: {declaration}")
|
||||
|
||||
timestamp_helper = extract_function(current["inode.c"], "erofs_set_timestamp")
|
||||
read_inode = extract_function(current["inode.c"], "erofs_read_inode")
|
||||
if timestamp_helper.count("__builtin_add_overflow(seconds, 0, &mtime)") != 1:
|
||||
raise SystemExit("time_t checked conversion is absent or duplicated")
|
||||
compact_pattern = re.compile(
|
||||
r"__builtin_add_overflow\(sbi->epoch,\s*"
|
||||
r"\(int64_t\)le32toh\(dic->i_mtime\), &mtime\)"
|
||||
)
|
||||
if len(compact_pattern.findall(read_inode)) != 1:
|
||||
raise SystemExit("compact signed checked addition is absent or duplicated")
|
||||
if read_inode.count("(int64_t)le64toh(die->i_mtime)") != 1:
|
||||
raise SystemExit("extended signed seconds decode is absent or duplicated")
|
||||
if "(uint64_t)le32toh(dic->i_mtime)" in read_inode:
|
||||
raise SystemExit("unsigned compact timestamp arithmetic remains")
|
||||
|
||||
changed_sources = current["internal.h"] + current["inode.c"] + current["super.c"]
|
||||
if "ckd_add" in changed_sources or "stdckdint" in changed_sources:
|
||||
raise SystemExit("unsupported checked-arithmetic API entered B13")
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", changed_sources):
|
||||
raise SystemExit("negative errno entered the FreeBSD timestamp path")
|
||||
|
||||
overflow_lines = []
|
||||
for path in ("inode.c", "super.c"):
|
||||
for line_number, line in enumerate(current[path].splitlines(), start=1):
|
||||
if "__builtin_" in line and "overflow" in line:
|
||||
overflow_lines.append(f"{path}:{line_number}:{line.strip()}")
|
||||
(artifacts / "B13-overflow-scan.txt").write_text(
|
||||
"\n".join(overflow_lines) + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
compact = load_fixture(
|
||||
compact_path, {"epoch", "fixed_nsec", "mtime_delta"}
|
||||
)
|
||||
extended = load_fixture(extended_path, {"mtime", "mtime_nsec"})
|
||||
seen_ids: set[str] = set()
|
||||
decoded_compact = []
|
||||
decoded_extended = []
|
||||
|
||||
for case in compact["cases"]:
|
||||
case_id = case.get("id")
|
||||
if not isinstance(case_id, str) or case_id in seen_ids:
|
||||
raise SystemExit(f"invalid or duplicate B13 case id: {case_id!r}")
|
||||
seen_ids.add(case_id)
|
||||
epoch = decode_le(case["epoch_le"], 8, signed=True)
|
||||
delta = decode_le(case["mtime_delta_le"], 4, signed=False)
|
||||
nanoseconds = decode_le(case["fixed_nsec_le"], 4, signed=False)
|
||||
mathematical = epoch + delta
|
||||
add_status = status_for_range(mathematical, int64_min, int64_max)
|
||||
if add_status != case["expected_add"]:
|
||||
raise SystemExit(f"compact add oracle mismatch: {case_id}")
|
||||
if add_status == "PASS":
|
||||
if mathematical != case["expected_seconds"]:
|
||||
raise SystemExit(f"compact seconds oracle mismatch: {case_id}")
|
||||
if status_for_range(mathematical, int64_min, int64_max) != case["expected_time64"]:
|
||||
raise SystemExit(f"compact time64 oracle mismatch: {case_id}")
|
||||
if status_for_range(mathematical, int32_min, int32_max) != case["expected_time32"]:
|
||||
raise SystemExit(f"compact time32 oracle mismatch: {case_id}")
|
||||
nsec_status = "PASS" if nanoseconds < 1_000_000_000 else "EINTEGRITY"
|
||||
if nsec_status != case["expected_nsec"]:
|
||||
raise SystemExit(f"compact nsec oracle mismatch: {case_id}")
|
||||
elif any(case[key] != "NOT_REACHED" for key in (
|
||||
"expected_time32", "expected_time64", "expected_nsec"
|
||||
)) or case["expected_seconds"] is not None:
|
||||
raise SystemExit(f"compact overflow must stop before conversion: {case_id}")
|
||||
decoded_compact.append(
|
||||
{
|
||||
"id": case_id,
|
||||
"epoch": epoch,
|
||||
"delta": delta,
|
||||
"nanoseconds": nanoseconds,
|
||||
"mathematical_seconds": mathematical,
|
||||
"add_status": add_status,
|
||||
}
|
||||
)
|
||||
|
||||
for case in extended["cases"]:
|
||||
case_id = case.get("id")
|
||||
if not isinstance(case_id, str) or case_id in seen_ids:
|
||||
raise SystemExit(f"invalid or duplicate B13 case id: {case_id!r}")
|
||||
seen_ids.add(case_id)
|
||||
seconds = decode_le(case["mtime_le"], 8, signed=True)
|
||||
nanoseconds = decode_le(case["mtime_nsec_le"], 4, signed=False)
|
||||
if seconds != case["expected_seconds"]:
|
||||
raise SystemExit(f"extended signed decode mismatch: {case_id}")
|
||||
if status_for_range(seconds, int64_min, int64_max) != case["expected_time64"]:
|
||||
raise SystemExit(f"extended time64 oracle mismatch: {case_id}")
|
||||
if status_for_range(seconds, int32_min, int32_max) != case["expected_time32"]:
|
||||
raise SystemExit(f"extended time32 oracle mismatch: {case_id}")
|
||||
nsec_status = "PASS" if nanoseconds < 1_000_000_000 else "EINTEGRITY"
|
||||
if nsec_status != case["expected_nsec"]:
|
||||
raise SystemExit(f"extended nsec oracle mismatch: {case_id}")
|
||||
decoded_extended.append(
|
||||
{"id": case_id, "seconds": seconds, "nanoseconds": nanoseconds}
|
||||
)
|
||||
|
||||
fixture_result = {
|
||||
"status": "PASS",
|
||||
"compact": decoded_compact,
|
||||
"extended": decoded_extended,
|
||||
}
|
||||
(artifacts / "B13-decoded-fixtures.json").write_text(
|
||||
json.dumps(fixture_result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
|
||||
|
||||
def c_i64(value: int) -> str:
|
||||
if value == int64_min:
|
||||
return "INT64_MIN"
|
||||
if value == int64_max:
|
||||
return "INT64_MAX"
|
||||
if value < 0:
|
||||
return f"(-INT64_C({-value}))"
|
||||
return f"INT64_C({value})"
|
||||
|
||||
|
||||
def c_status(value: str) -> str:
|
||||
if value == "PASS":
|
||||
return "0"
|
||||
if value == "EINTEGRITY":
|
||||
return "EINTEGRITY"
|
||||
raise SystemExit(f"cannot emit non-terminal status: {value}")
|
||||
|
||||
|
||||
program = f'''#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#define EINTEGRITY {eintegrity}
|
||||
|
||||
struct erofs_inode {{
|
||||
\ttime_t mtime;
|
||||
\tuint32_t mtime_nsec;
|
||||
}};
|
||||
|
||||
{timestamp_helper}
|
||||
|
||||
static int
|
||||
checked_compact(int64_t epoch, uint32_t delta, int64_t *seconds)
|
||||
{{
|
||||
\tif (__builtin_add_overflow(epoch, (int64_t)delta, seconds))
|
||||
\t\treturn (EINTEGRITY);
|
||||
\treturn (0);
|
||||
}}
|
||||
|
||||
static int
|
||||
checked_time32(int64_t seconds, int32_t *result)
|
||||
{{
|
||||
\treturn (__builtin_add_overflow(seconds, 0, result) ? EINTEGRITY : 0);
|
||||
}}
|
||||
|
||||
static int
|
||||
checked_time64(int64_t seconds, int64_t *result)
|
||||
{{
|
||||
\treturn (__builtin_add_overflow(seconds, 0, result) ? EINTEGRITY : 0);
|
||||
}}
|
||||
|
||||
static int
|
||||
check_timestamp(const char *id, int64_t seconds, uint32_t nanoseconds,
|
||||
int expected_time32, int expected_time64, int expected_nsec)
|
||||
{{
|
||||
\tstruct erofs_inode inode = {{ 0 }};
|
||||
\tint32_t time32;
|
||||
\tint64_t time64;
|
||||
\tint error, expected;
|
||||
|
||||
\tif (checked_time32(seconds, &time32) != expected_time32 ||
|
||||
\t checked_time64(seconds, &time64) != expected_time64) {{
|
||||
\t\tfprintf(stderr, "%s: representability\\n", id);
|
||||
\t\treturn (1);
|
||||
\t}}
|
||||
\texpected = expected_nsec != 0 ? expected_nsec :
|
||||
\t (sizeof(time_t) == sizeof(int32_t) ? expected_time32 : expected_time64);
|
||||
\terror = erofs_set_timestamp(&inode, seconds, nanoseconds);
|
||||
\tif (error != expected) {{
|
||||
\t\tfprintf(stderr, "%s: timestamp errno %d != %d\\n", id, error, expected);
|
||||
\t\treturn (1);
|
||||
\t}}
|
||||
\tif (error == 0 && ((int64_t)inode.mtime != seconds ||
|
||||
\t inode.mtime_nsec != nanoseconds)) {{
|
||||
\t\tfprintf(stderr, "%s: timestamp value\\n", id);
|
||||
\t\treturn (1);
|
||||
\t}}
|
||||
\treturn (0);
|
||||
}}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{{
|
||||
\tint64_t seconds;
|
||||
\tint error;
|
||||
|
||||
\tif (sizeof(time_t) != sizeof(int32_t) && sizeof(time_t) != sizeof(int64_t))
|
||||
\t\treturn (2);
|
||||
'''
|
||||
for case, decoded in zip(compact["cases"], decoded_compact, strict=True):
|
||||
epoch = decoded["epoch"]
|
||||
delta = decoded["delta"]
|
||||
expected_add = c_status(case["expected_add"])
|
||||
program += f'''\terror = checked_compact({c_i64(epoch)}, UINT32_C({delta}), &seconds);
|
||||
\tif (error != {expected_add}) {{
|
||||
\t\tfprintf(stderr, "{case['id']}: compact add errno\\n");
|
||||
\t\treturn (1);
|
||||
\t}}
|
||||
'''
|
||||
if case["expected_add"] == "PASS":
|
||||
program += f'''\tif (seconds != {c_i64(case['expected_seconds'])} ||
|
||||
\t check_timestamp("{case['id']}", seconds, UINT32_C({decoded['nanoseconds']}),
|
||||
\t {c_status(case['expected_time32'])}, {c_status(case['expected_time64'])},
|
||||
\t {c_status(case['expected_nsec'])}) != 0)
|
||||
\t\treturn (1);
|
||||
'''
|
||||
for case, decoded in zip(extended["cases"], decoded_extended, strict=True):
|
||||
program += f'''\tif (check_timestamp("{case['id']}", {c_i64(decoded['seconds'])},
|
||||
\t UINT32_C({decoded['nanoseconds']}), {c_status(case['expected_time32'])},
|
||||
\t {c_status(case['expected_time64'])}, {c_status(case['expected_nsec'])}) != 0)
|
||||
\t\treturn (1);
|
||||
'''
|
||||
program += f'''\tprintf("B13 boundary PASS compact={len(compact['cases'])} extended={len(extended['cases'])} time_t=%zu\\n",
|
||||
\t sizeof(time_t) * 8);
|
||||
\treturn (0);
|
||||
}}
|
||||
'''
|
||||
|
||||
program_path = artifacts / "B13-time-boundary.c"
|
||||
binary_path = artifacts / "B13-time-boundary"
|
||||
program_path.write_text(program, encoding="ascii")
|
||||
compiled = subprocess.run(
|
||||
[
|
||||
"cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
|
||||
str(program_path), "-o", str(binary_path),
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(artifacts / "B13-time-boundary.compile.stdout").write_text(
|
||||
compiled.stdout, encoding="utf-8"
|
||||
)
|
||||
(artifacts / "B13-time-boundary.compile.stderr").write_text(
|
||||
compiled.stderr, encoding="utf-8"
|
||||
)
|
||||
if compiled.returncode != 0:
|
||||
raise SystemExit("B13 boundary extractor compilation failed")
|
||||
executed = subprocess.run(
|
||||
[str(binary_path)],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(artifacts / "B13-time-boundary.stdout").write_text(
|
||||
executed.stdout, encoding="utf-8"
|
||||
)
|
||||
(artifacts / "B13-time-boundary.stderr").write_text(
|
||||
executed.stderr, encoding="utf-8"
|
||||
)
|
||||
if executed.returncode != 0:
|
||||
raise SystemExit("B13 boundary extractor execution failed")
|
||||
|
||||
program32 = program.replace("#include <time.h>\n", "#define time_t int32_t\n", 1)
|
||||
program32_path = artifacts / "B13-time-boundary32.c"
|
||||
binary32_path = artifacts / "B13-time-boundary32"
|
||||
program32_path.write_text(program32, encoding="ascii")
|
||||
compiled32 = subprocess.run(
|
||||
[
|
||||
"cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
|
||||
str(program32_path), "-o", str(binary32_path),
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(artifacts / "B13-time-boundary32.compile.stdout").write_text(
|
||||
compiled32.stdout, encoding="utf-8"
|
||||
)
|
||||
(artifacts / "B13-time-boundary32.compile.stderr").write_text(
|
||||
compiled32.stderr, encoding="utf-8"
|
||||
)
|
||||
if compiled32.returncode != 0:
|
||||
raise SystemExit("B13 32-bit time_t extractor compilation failed")
|
||||
executed32 = subprocess.run(
|
||||
[str(binary32_path)],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(artifacts / "B13-time-boundary32.stdout").write_text(
|
||||
executed32.stdout, encoding="utf-8"
|
||||
)
|
||||
(artifacts / "B13-time-boundary32.stderr").write_text(
|
||||
executed32.stderr, encoding="utf-8"
|
||||
)
|
||||
if executed32.returncode != 0:
|
||||
raise SystemExit("B13 32-bit time_t extractor execution failed")
|
||||
|
||||
result = {
|
||||
"status": "PASS",
|
||||
"final_id": "P15-018",
|
||||
"baseline": baseline,
|
||||
"compact_cases": len(compact["cases"]),
|
||||
"extended_cases": len(extended["cases"]),
|
||||
"signed_add_overflows": sum(
|
||||
case["expected_add"] == "EINTEGRITY" for case in compact["cases"]
|
||||
),
|
||||
"negative_seconds_passes": sum(
|
||||
case["expected_seconds"] is not None and case["expected_seconds"] < 0
|
||||
for case in compact["cases"] + extended["cases"]
|
||||
),
|
||||
"time32_lower_rejections": sum(
|
||||
case["expected_seconds"] is not None
|
||||
and case["expected_seconds"] < int32_min
|
||||
and case["expected_time32"] == "EINTEGRITY"
|
||||
for case in compact["cases"] + extended["cases"]
|
||||
),
|
||||
"time32_upper_rejections": sum(
|
||||
case["expected_seconds"] is not None
|
||||
and case["expected_seconds"] > int32_max
|
||||
and case["expected_time32"] == "EINTEGRITY"
|
||||
for case in compact["cases"] + extended["cases"]
|
||||
),
|
||||
"nanosecond_rejections": sum(
|
||||
case["expected_nsec"] == "EINTEGRITY"
|
||||
for case in compact["cases"] + extended["cases"]
|
||||
),
|
||||
"ondisk_header_byte_identical": True,
|
||||
"ondisk_seconds_width": 64,
|
||||
"ondisk_compact_delta_width": 32,
|
||||
"time_t_checked_conversion": True,
|
||||
"extracted_helper_time_widths": [32, 64],
|
||||
"unsupported_ckd_api": False,
|
||||
"positive_errno_preserved": True,
|
||||
"qemu": "NOT_RUN",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
}
|
||||
(artifacts / "B13-result.json").write_text(
|
||||
json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
print(
|
||||
f"B13 PASS compact={len(compact['cases'])} extended={len(extended['cases'])} "
|
||||
f"signed-overflow={result['signed_add_overflows']}"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B13 signed timestamp source or boundary check failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/B13-overflow-scan.txt" \
|
||||
"$artifacts/B13-decoded-fixtures.json" \
|
||||
"$artifacts/B13-time-boundary.c" "$artifacts/B13-time-boundary" \
|
||||
"$artifacts/B13-time-boundary32.c" "$artifacts/B13-time-boundary32" \
|
||||
"$artifacts/B13-result.json" > "$artifacts/SHA256SUMS"
|
||||
printf 'B13 PASS signed timestamps and checked time_t boundaries\n'
|
||||
Executable
+226
@@ -0,0 +1,226 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=bf72cdffbc2b58c2b6468e20391eea14096b5a83
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
generator=$fixture_dir/B14-chunk-fixtures.py
|
||||
kld_builder=$fixture_dir/B14-build-kld.sh
|
||||
probe=$fixture_dir/B14-chunk-probe.c
|
||||
spec=$fixture_dir/B14-chunk-spec.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
first=$PRE15_CASE_TMP/first
|
||||
second=$PRE15_CASE_TMP/second
|
||||
|
||||
for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B14 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b14-generator "$generator"
|
||||
pre15_record_fixture b14-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b14-probe "$probe"
|
||||
pre15_record_fixture b14-spec "$spec"
|
||||
pre15_record_fixture b14-case "$PRE15_DUT/tests/pre15/cases/B14-chunk-types.sh"
|
||||
pre15_record_fixture b14-inode "$PRE15_DUT/src/inode.c"
|
||||
pre15_record_fixture b14-data "$PRE15_DUT/src/data.c"
|
||||
pre15_record_fixture b14-vnops "$PRE15_DUT/src/erofs_vnops.c"
|
||||
pre15_record_fixture b14-linux-inode "$PRE15_ROOT/src-linux/inode.c"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
if ! python3 -B "$generator" generate --output "$first" --spec "$spec" \
|
||||
>"$artifacts/generate-first.stdout" 2>"$artifacts/generate-first.stderr"; then
|
||||
pre15_runner_fail 'B14 first fixture generation failed'
|
||||
fi
|
||||
if ! python3 -B "$generator" generate --output "$second" --spec "$spec" \
|
||||
>"$artifacts/generate-second.stdout" 2>"$artifacts/generate-second.stderr"; then
|
||||
pre15_runner_fail 'B14 repeat fixture generation failed'
|
||||
fi
|
||||
if ! cmp "$first/manifest.json" "$second/manifest.json" || \
|
||||
! cmp "$first/SHA256SUMS" "$second/SHA256SUMS"; then
|
||||
pre15_runner_fail 'B14 repeat generation is not byte deterministic'
|
||||
fi
|
||||
|
||||
for name in B14-slot1.blob B14-good32.erofs B14-good48.erofs \
|
||||
B14-bad-index.erofs B14-bad-index48.erofs B14-bad-reserved.erofs \
|
||||
B14-bad-format.erofs B14-bad-48-no-index.erofs; do
|
||||
pre15_record_fixture "b14-$name" "$first/$name"
|
||||
done
|
||||
pre15_target_reached
|
||||
|
||||
if ! python3 -B "$generator" audit --root "$PRE15_ROOT" --dut "$PRE15_DUT" \
|
||||
--baseline "$baseline" --spec "$spec" --report "$artifacts/B14-source-report.json" \
|
||||
>"$artifacts/audit.stdout" 2>"$artifacts/audit.stderr"; then
|
||||
pre15_dut_fail 'B14 source or Linux/FreeBSD semantic audit failed'
|
||||
fi
|
||||
if ! python3 -B "$generator" verify --output "$first" --spec "$spec" \
|
||||
>"$artifacts/verify-first.stdout" 2>"$artifacts/verify-first.stderr"; then
|
||||
pre15_dut_fail 'B14 fixture oracle failed'
|
||||
fi
|
||||
if ! python3 -B "$generator" verify --output "$second" --spec "$spec" \
|
||||
>"$artifacts/verify-second.stdout" 2>"$artifacts/verify-second.stderr"; then
|
||||
pre15_runner_fail 'B14 repeat fixture verification failed'
|
||||
fi
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' 'TC174 host fixture/source oracle PASS' \
|
||||
'QEMU runtime NOT_RUN in host mode'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in awk cc clang file nm scp tar; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B14 QEMU tool: $tool"
|
||||
done
|
||||
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B14-fixtures.tar.gz
|
||||
module=$PRE15_CASE_TMP/B14-erofs.ko
|
||||
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B14-kld-build.stdout" \
|
||||
2>"$artifacts/B14-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B14 cross-target KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/B14-kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B14-kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/B14-kld-nm-u.txt"
|
||||
tar -C "$first" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B14-fixtures.tar.gz || \
|
||||
! pre15_scp "$probe" /root/B14-chunk-probe.c || \
|
||||
! pre15_scp "$module" /root/B14-erofs.ko; then
|
||||
pre15_infra_blocked 'could not transfer B14 module or fixtures to the guest'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/pre15-b14-fixtures && mkdir /root/pre15-b14-fixtures && tar -xzf /root/B14-fixtures.tar.gz -C /root/pre15-b14-fixtures'; then
|
||||
pre15_infra_blocked 'could not prepare B14 guest directories'
|
||||
fi
|
||||
pre15_guest_ssh_bounded sha256 /root/B14-erofs.ko \
|
||||
>"$artifacts/B14-guest-module-sha256.txt"
|
||||
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B14-erofs.ko \
|
||||
>"$artifacts/B14-kldload.stdout" 2>"$artifacts/B14-kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' \
|
||||
"$artifacts/B14-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_dut_fail 'B14 exact-source KLD failed to load'
|
||||
fi
|
||||
pre15_own_guest_kld erofs 'B14 exact-source KLD'
|
||||
if ! pre15_guest_ssh_bounded cc -Wall -Wextra -Werror \
|
||||
-o /root/B14-chunk-probe /root/B14-chunk-probe.c; then
|
||||
pre15_infra_blocked 'could not compile the B14 guest errno probe'
|
||||
fi
|
||||
|
||||
pre15_attach_md()
|
||||
{
|
||||
pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode -f "$1") || \
|
||||
pre15_dut_fail "could not attach B14 provider: $1"
|
||||
case "$pre15_md" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
|
||||
esac
|
||||
pre15_md=${pre15_md#md}
|
||||
pre15_own_guest_md "$pre15_md" "$2"
|
||||
printf '%s\n' "$pre15_md"
|
||||
}
|
||||
|
||||
pre15_mount_image()
|
||||
{
|
||||
pre15_image=$1
|
||||
pre15_mountpoint=$2
|
||||
pre15_label=$3
|
||||
pre15_primary=$(pre15_attach_md "/root/pre15-b14-fixtures/$pre15_image" \
|
||||
"$pre15_label primary")
|
||||
pre15_guest_ssh_bounded mkdir -p "$pre15_mountpoint"
|
||||
if ! pre15_guest_ssh_bounded mount -t erofs -o ro \
|
||||
-o "device.1=/dev/md$blob_md" "/dev/md$pre15_primary" \
|
||||
"$pre15_mountpoint"; then
|
||||
pre15_dut_fail "$pre15_label mount failed"
|
||||
fi
|
||||
pre15_own_guest_mount "$pre15_mountpoint" "$pre15_label mount"
|
||||
}
|
||||
|
||||
blob_md=$(pre15_attach_md /root/pre15-b14-fixtures/B14-slot1.blob \
|
||||
'B14 external slot 1')
|
||||
pre15_mount_image B14-good32.erofs /mnt/pre15-b14-good32 'B14 good32'
|
||||
pre15_guest_ssh_bounded cmp /root/pre15-b14-fixtures/source/real-dir/entry.txt \
|
||||
/mnt/pre15-b14-good32/chunk-dir/entry.txt
|
||||
pre15_guest_ssh_bounded cmp /root/pre15-b14-fixtures/source/target.txt \
|
||||
/mnt/pre15-b14-good32/chunk-link
|
||||
pre15_guest_ssh_bounded /root/B14-chunk-probe readlink-pass \
|
||||
/mnt/pre15-b14-good32/chunk-link target.txt
|
||||
pre15_guest_ssh_bounded stat -f '%HT %z' /mnt/pre15-b14-good32/chunk-char \
|
||||
>"$artifacts/B14-char-stat.txt"
|
||||
pre15_guest_ssh_bounded /root/B14-chunk-probe open-unsupported \
|
||||
/mnt/pre15-b14-good32/chunk-char
|
||||
pre15_guest_ssh_bounded ls -1A /mnt/pre15-b14-good32/chunk-dir \
|
||||
>"$artifacts/B14-directory-list.txt"
|
||||
if test "$(cat "$artifacts/B14-directory-list.txt")" != entry.txt; then
|
||||
pre15_dut_fail 'chunk directory readdir returned the wrong set'
|
||||
fi
|
||||
|
||||
pre15_mount_image B14-good48.erofs /mnt/pre15-b14-good48 'B14 good48'
|
||||
pre15_guest_ssh_bounded cmp /root/pre15-b14-fixtures/source/real-dir/entry.txt \
|
||||
/mnt/pre15-b14-good48/chunk-dir/entry.txt
|
||||
pre15_guest_ssh_bounded /root/B14-chunk-probe readlink-pass \
|
||||
/mnt/pre15-b14-good48/chunk-link target.txt
|
||||
|
||||
pre15_mount_image B14-bad-index.erofs /mnt/pre15-b14-bad-index 'B14 bad index'
|
||||
pre15_guest_ssh_bounded /root/B14-chunk-probe readlink-integrity \
|
||||
/mnt/pre15-b14-bad-index/chunk-link
|
||||
|
||||
pre15_mount_image B14-bad-index48.erofs /mnt/pre15-b14-bad-index48 \
|
||||
'B14 bad 48-bit index'
|
||||
pre15_guest_ssh_bounded /root/B14-chunk-probe readlink-integrity \
|
||||
/mnt/pre15-b14-bad-index48/chunk-link
|
||||
|
||||
pre15_mount_image B14-bad-reserved.erofs /mnt/pre15-b14-bad-reserved \
|
||||
'B14 bad reserved'
|
||||
pre15_guest_ssh_bounded /root/B14-chunk-probe stat-integrity \
|
||||
/mnt/pre15-b14-bad-reserved/chunk-dir
|
||||
|
||||
pre15_mount_image B14-bad-format.erofs /mnt/pre15-b14-bad-format \
|
||||
'B14 bad format'
|
||||
pre15_guest_ssh_bounded /root/B14-chunk-probe stat-unsupported \
|
||||
/mnt/pre15-b14-bad-format/chunk-link
|
||||
|
||||
pre15_mount_image B14-bad-48-no-index.erofs /mnt/pre15-b14-bad-48-no-index \
|
||||
'B14 bad 48-bit without indexes'
|
||||
pre15_guest_ssh_bounded /root/B14-chunk-probe stat-integrity \
|
||||
/mnt/pre15-b14-bad-48-no-index/chunk-link
|
||||
|
||||
if pre15_guest_ssh_bounded truss -f -o /root/B14-md-ebusy.truss \
|
||||
mdconfig -d -u "$blob_md"; then
|
||||
pre15_dut_fail 'external GEOM provider detached while B14 mounts were active'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded grep -Eq 'ERR#16' /root/B14-md-ebusy.truss; then
|
||||
pre15_dut_fail 'external GEOM detach did not return exact EBUSY'
|
||||
fi
|
||||
pre15_guest_ssh_bounded cat /root/B14-md-ebusy.truss \
|
||||
>"$artifacts/B14-md-ebusy.truss"
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/B14-dmesg.txt"
|
||||
printf '%s\n' 'TC174 QEMU directory/symlink/multidevice/48-bit PASS'
|
||||
Executable
+343
@@ -0,0 +1,343 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B16-symlink-spec.json
|
||||
probe=$fixture_dir/B16-symlink-probe.c
|
||||
kld_builder=$fixture_dir/B16-build-kld.sh
|
||||
gate_script=$PRE15_DUT/tests/pre15/gates/P15-019.sh
|
||||
gate_input=$PRE15_DUT/tests/pre15/gates/P15-019-input.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
gate_clone=$PRE15_CASE_TMP/gate-clone
|
||||
first=$PRE15_CASE_TMP/first
|
||||
second=$PRE15_CASE_TMP/second
|
||||
|
||||
for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B16 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b16-spec "$spec"
|
||||
pre15_record_fixture b16-probe "$probe"
|
||||
pre15_record_fixture b16-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b16-case "$PRE15_DUT/tests/pre15/cases/B16-symlink.sh"
|
||||
pre15_record_fixture b16-gate "$gate_script"
|
||||
pre15_record_fixture b16-gate-input "$gate_input"
|
||||
pre15_record_fixture b16-internal "$PRE15_DUT/src/internal.h"
|
||||
pre15_record_fixture b16-inode "$PRE15_DUT/src/inode.c"
|
||||
pre15_record_fixture b16-data "$PRE15_DUT/src/data.c"
|
||||
pre15_record_fixture b16-vnops "$PRE15_DUT/src/erofs_vnops.c"
|
||||
pre15_record_fixture b16-linux-inode "$PRE15_ROOT/src-linux/inode.c"
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if ! git clone -q --shared --no-checkout "$PRE15_ROOT" "$gate_clone" \
|
||||
>"$artifacts/gate-clone.stdout" 2>"$artifacts/gate-clone.stderr" || \
|
||||
! git -C "$gate_clone" checkout -q --detach \
|
||||
aea34aba38a298d493d0a584f3985cf3589f358e \
|
||||
>"$artifacts/gate-checkout.stdout" 2>"$artifacts/gate-checkout.stderr"; then
|
||||
pre15_runner_fail 'could not materialize the frozen P15-019 gate commit'
|
||||
fi
|
||||
|
||||
frozen_gate=$gate_clone/repo-pre-15/tests/pre15/gates/P15-019.sh
|
||||
if ! timeout -k 10 240 "$frozen_gate" \
|
||||
--base d645feb720c7022d2138d2a62eb72c022eb75351 --output "$first" \
|
||||
>"$artifacts/gate-first.stdout" 2>"$artifacts/gate-first.stderr"; then
|
||||
pre15_runner_fail 'first frozen P15-019 gate replay failed'
|
||||
fi
|
||||
if ! timeout -k 10 240 "$frozen_gate" \
|
||||
--base d645feb720c7022d2138d2a62eb72c022eb75351 --output "$second" \
|
||||
>"$artifacts/gate-second.stdout" 2>"$artifacts/gate-second.stderr"; then
|
||||
pre15_runner_fail 'second frozen P15-019 gate replay failed'
|
||||
fi
|
||||
for name in result.json cases.json commands.json semantics.json cleanup.json \
|
||||
fixtures/SHA256SUMS; do
|
||||
if ! cmp "$first/host/$name" "$second/host/$name"; then
|
||||
pre15_runner_fail "P15-019 repeat output differs: $name"
|
||||
fi
|
||||
done
|
||||
|
||||
if ! python3 -B - "$spec" "$gate_script" "$gate_input" \
|
||||
"$first/host/result.json" "$first/host/cases.json" \
|
||||
"$first/host/fixtures/SHA256SUMS" \
|
||||
"$PRE15_DUT/src/internal.h" "$PRE15_DUT/src/inode.c" \
|
||||
"$PRE15_DUT/src/data.c" "$PRE15_DUT/src/erofs_vnops.c" \
|
||||
"$PRE15_ROOT/src-linux/inode.c" \
|
||||
>"$artifacts/B16-host-audit.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def function(source: str, name: str) -> str:
|
||||
match = re.search(rf"\n{name}\([^;]*?\n\{{", source, re.DOTALL)
|
||||
if match is None:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
start = match.start() + 1
|
||||
brace = source.index("{", match.start())
|
||||
depth = 0
|
||||
for index in range(brace, len(source)):
|
||||
if source[index] == "{":
|
||||
depth += 1
|
||||
elif source[index] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
spec = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
gate = Path(sys.argv[2])
|
||||
gate_input = Path(sys.argv[3])
|
||||
result = json.loads(Path(sys.argv[4]).read_text(encoding="ascii"))
|
||||
cases = json.loads(Path(sys.argv[5]).read_text(encoding="ascii"))
|
||||
sums = Path(sys.argv[6])
|
||||
internal = Path(sys.argv[7]).read_text(encoding="utf-8")
|
||||
inode = Path(sys.argv[8]).read_text(encoding="utf-8")
|
||||
data = Path(sys.argv[9]).read_text(encoding="utf-8")
|
||||
vnops = Path(sys.argv[10]).read_text(encoding="utf-8")
|
||||
linux = Path(sys.argv[11]).read_text(encoding="utf-8")
|
||||
|
||||
if spec.get("schema") != 1 or spec.get("batch") != "B16" or spec.get("test") != "TC166-symlink-layouts":
|
||||
raise SystemExit("B16 spec identity changed")
|
||||
if sha256(gate) != spec["gate"]["script_sha256"] or sha256(gate_input) != spec["gate"]["input_sha256"]:
|
||||
raise SystemExit("tracked P15-019 gate differs from the GO decision")
|
||||
if result.get("decision") != "GO" or result.get("case_count") != 35:
|
||||
raise SystemExit("frozen P15-019 gate did not reproduce GO/35")
|
||||
if result.get("fixture_set_sha256") != spec["fixture_set_sha256"] or sha256(sums) != spec["fixture_set_sha256"]:
|
||||
raise SystemExit("B16 fixture-set hash differs from G03")
|
||||
|
||||
expected_pairs = {
|
||||
(layout, case)
|
||||
for layout in spec["layouts"]
|
||||
for case in spec["cases"]
|
||||
}
|
||||
actual_pairs = {(item["layout"], item["case"]) for item in cases}
|
||||
if actual_pairs != expected_pairs:
|
||||
raise SystemExit("B16 fixture matrix is incomplete")
|
||||
for item in cases:
|
||||
layout = spec["layouts"][item["layout"]]
|
||||
if item["inode"]["layout"] != layout["layout"]:
|
||||
raise SystemExit("B16 fixture layout differs")
|
||||
|
||||
validator = function(data, "erofs_validate_symlink_target")
|
||||
readlink = function(data, "erofs_readlink_target")
|
||||
read_inode = function(inode, "erofs_read_inode")
|
||||
markers = (
|
||||
"vi->vtype != VLNK",
|
||||
"vi->size == 0",
|
||||
"vi->size > MAXPATHLEN",
|
||||
"erofs_read_data(sbi, vi, 0, (size_t)vi->size, &target)",
|
||||
"memchr(target, '\\0', (size_t)vi->size)",
|
||||
"erofs_brelse(target)",
|
||||
)
|
||||
if not all(marker in validator for marker in markers):
|
||||
raise SystemExit("B16 bounded validator is incomplete")
|
||||
if "uiomove" in validator:
|
||||
raise SystemExit("B16 validator performs partial caller transfer")
|
||||
if not (
|
||||
read_inode.index("vi->size == 0")
|
||||
< read_inode.index("z_erofs_fill_inode(sbi, vi)")
|
||||
and read_inode.index("vi->size > MAXPATHLEN")
|
||||
< read_inode.index("z_erofs_fill_inode(sbi, vi)")
|
||||
and read_inode.index("erofs_validate_symlink_target(sbi, vi)")
|
||||
> read_inode.index("z_erofs_fill_inode(sbi, vi)")
|
||||
):
|
||||
raise SystemExit("B16 size/content validation order changed")
|
||||
if "erofs_validate_symlink_target" not in internal:
|
||||
raise SystemExit("B16 validator prototype is missing")
|
||||
if "vi->size == 0" not in readlink or "vi->size > MAXPATHLEN" not in readlink or "erofs_read_uio" not in readlink:
|
||||
raise SystemExit("B16 VOP readlink guard/path changed")
|
||||
if "return (erofs_readlink_target(ap->a_vp, ap->a_uio));" not in vnops:
|
||||
raise SystemExit("FreeBSD VOP_READLINK adapter changed")
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", internal + inode + data + vnops):
|
||||
raise SystemExit("B16 introduced Linux negative errno")
|
||||
if "i_link" in internal + inode + data + vnops or "page_get_link" in internal + inode + data + vnops:
|
||||
raise SystemExit("B16 copied the Linux page/i_link implementation")
|
||||
for marker in (
|
||||
"vi->datalayout == EROFS_INODE_FLAT_INLINE",
|
||||
"kmemdup_nul(bptr + ofs, inode->i_size, GFP_KERNEL)",
|
||||
".get_link = page_get_link",
|
||||
".get_link = simple_get_link",
|
||||
"return -EFSCORRUPTED",
|
||||
):
|
||||
if marker not in linux:
|
||||
raise SystemExit("Linux symlink semantic anchor changed")
|
||||
|
||||
report = {
|
||||
"status": "PASS",
|
||||
"candidate": "P15-019",
|
||||
"test": "TC166-symlink-layouts",
|
||||
"fixture_count": len(cases),
|
||||
"fixture_set_sha256": result["fixture_set_sha256"],
|
||||
"layouts": sorted(spec["layouts"]),
|
||||
"errno": spec["errno"],
|
||||
"linux_path": "fast inline i_link validation; page_get_link otherwise; negative errno",
|
||||
"freebsd_path": "vnode VOP_READLINK; bounded pre-publication scan; GEOM/map; positive errno",
|
||||
}
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
PY
|
||||
then
|
||||
pre15_runner_fail 'B16 independent fixture/source audit failed'
|
||||
fi
|
||||
|
||||
cp "$first/host/result.json" "$artifacts/P15-019-result.json"
|
||||
cp "$first/host/cases.json" "$artifacts/P15-019-cases.json"
|
||||
cp "$first/host/fixtures/SHA256SUMS" "$artifacts/B16-SHA256SUMS"
|
||||
pre15_record_fixture b16-generated-sums "$artifacts/B16-SHA256SUMS"
|
||||
pre15_target_reached
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'TC166 host all-layout fixture and independent source oracle PASS' \
|
||||
'QEMU runtime NOT_RUN in host mode'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in awk cc clang file nm scp tar; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B16 QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B16-fixtures.tar.gz
|
||||
module=$PRE15_CASE_TMP/B16-erofs.ko
|
||||
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B16-kld-build.stdout" \
|
||||
2>"$artifacts/B16-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B16 cross-target KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/B16-kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B16-kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/B16-kld-nm-u.txt"
|
||||
tar -C "$first/host/fixtures" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B16-fixtures.tar.gz || \
|
||||
! pre15_scp "$probe" /root/B16-symlink-probe.c || \
|
||||
! pre15_scp "$module" /root/B16-erofs.ko; then
|
||||
pre15_infra_blocked 'could not transfer B16 module or fixtures to the guest'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/pre15-b16-fixtures && mkdir /root/pre15-b16-fixtures && tar -xzf /root/B16-fixtures.tar.gz -C /root/pre15-b16-fixtures'; then
|
||||
pre15_infra_blocked 'could not prepare B16 guest fixtures'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B16-erofs.ko \
|
||||
>"$artifacts/B16-kldload.stdout" 2>"$artifacts/B16-kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' "$artifacts/B16-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_dut_fail 'B16 exact-source KLD failed to load'
|
||||
fi
|
||||
pre15_own_guest_kld erofs 'B16 exact-source KLD'
|
||||
if ! pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
|
||||
-o /root/B16-symlink-probe /root/B16-symlink-probe.c; then
|
||||
pre15_infra_blocked 'could not compile the B16 guest probe'
|
||||
fi
|
||||
|
||||
pre15_attach_md()
|
||||
{
|
||||
pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode -f "$1") || \
|
||||
pre15_dut_fail "could not attach B16 provider: $1"
|
||||
case "$pre15_md" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
|
||||
esac
|
||||
pre15_md=${pre15_md#md}
|
||||
pre15_own_guest_md "$pre15_md" "$2"
|
||||
printf '%s\n' "$pre15_md"
|
||||
}
|
||||
|
||||
pre15_mount_case()
|
||||
{
|
||||
pre15_layout=$1
|
||||
pre15_case=$2
|
||||
pre15_mountpoint=/mnt/pre15-b16-$pre15_layout-$pre15_case
|
||||
pre15_options=
|
||||
if test "$pre15_layout" = chunk; then
|
||||
pre15_blob=$(pre15_attach_md \
|
||||
"/root/pre15-b16-fixtures/$pre15_layout-$pre15_case.blob" \
|
||||
"B16 $pre15_layout $pre15_case blob")
|
||||
pre15_options="-o device.1=/dev/md$pre15_blob"
|
||||
fi
|
||||
pre15_primary=$(pre15_attach_md \
|
||||
"/root/pre15-b16-fixtures/$pre15_layout-$pre15_case.erofs" \
|
||||
"B16 $pre15_layout $pre15_case primary")
|
||||
pre15_guest_ssh_bounded mkdir -p "$pre15_mountpoint"
|
||||
if ! pre15_guest_ssh_bounded mount -t erofs -o ro $pre15_options \
|
||||
"/dev/md$pre15_primary" "$pre15_mountpoint"; then
|
||||
pre15_dut_fail "B16 $pre15_layout $pre15_case mount failed"
|
||||
fi
|
||||
pre15_own_guest_mount "$pre15_mountpoint" \
|
||||
"B16 $pre15_layout $pre15_case mount"
|
||||
printf '%s\n' "$pre15_mountpoint/link"
|
||||
}
|
||||
|
||||
for layout in chunk compressed fragment inline plain; do
|
||||
case "$layout" in
|
||||
compressed) short_length=64 ;;
|
||||
*) short_length=31 ;;
|
||||
esac
|
||||
for case_id in normal-short normal-max empty nul-middle too-long; do
|
||||
path=$(pre15_mount_case "$layout" "$case_id")
|
||||
case "$case_id" in
|
||||
normal-short)
|
||||
pre15_guest_ssh_bounded /root/B16-symlink-probe \
|
||||
readlink-pass "$path" "$short_length"
|
||||
pre15_guest_ssh_bounded /root/B16-symlink-probe \
|
||||
concurrent "$path" "$short_length" 16 20
|
||||
;;
|
||||
normal-max)
|
||||
pre15_guest_ssh_bounded /root/B16-symlink-probe \
|
||||
readlink-pass "$path" 1024
|
||||
pre15_guest_ssh_bounded /root/B16-symlink-probe \
|
||||
follow-errno "$path" 63
|
||||
;;
|
||||
empty|nul-middle)
|
||||
pre15_guest_ssh_bounded /root/B16-symlink-probe \
|
||||
readlink-errno "$path" 97
|
||||
;;
|
||||
too-long)
|
||||
pre15_guest_ssh_bounded /root/B16-symlink-probe \
|
||||
readlink-errno "$path" 63
|
||||
;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/B16-dmesg.txt"
|
||||
if grep -Eq 'panic:|Fatal trap|lock order reversal|KDB: stack backtrace' \
|
||||
"$artifacts/B16-dmesg.txt"; then
|
||||
pre15_dut_fail 'B16 runtime produced a kernel diagnostic'
|
||||
fi
|
||||
printf '%s\n' 'TC166 QEMU all-layout symlink validation PASS'
|
||||
Executable
+275
@@ -0,0 +1,275 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
fixtures_only=0
|
||||
if test "${1:-}" = --fixtures-only; then
|
||||
fixtures_only=1
|
||||
shift
|
||||
fi
|
||||
test "$#" -eq 0 || pre15_runner_fail 'B17 accepts only --fixtures-only'
|
||||
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B17-xattr-spec.json
|
||||
generator=$fixture_dir/B17-xattr-generate.py
|
||||
oracle=$fixture_dir/B17-xattr-oracle.py
|
||||
seed=$fixture_dir/B17-xattr-seed.erofs
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
first=$PRE15_CASE_TMP/first
|
||||
second=$PRE15_CASE_TMP/second
|
||||
rebuilt=$PRE15_CASE_TMP/rebuilt-seed.erofs
|
||||
rebuild_work=$PRE15_CASE_TMP/rebuild-work
|
||||
|
||||
for tool in fsck.erofs git mkfs.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B17 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b17-spec "$spec"
|
||||
pre15_record_fixture b17-generator "$generator"
|
||||
pre15_record_fixture b17-oracle "$oracle"
|
||||
pre15_record_fixture b17-seed "$seed"
|
||||
pre15_record_fixture b17-case "$PRE15_DUT/tests/pre15/cases/B17-xattr-integrity.sh"
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if python3 -B "$generator" rebuild-seed --spec "$spec" --seed "$seed" \
|
||||
--output "$rebuilt" --work "$rebuild_work" \
|
||||
>"$artifacts/rebuild-seed.json" 2>"$artifacts/rebuild-seed.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B17 tracked seed is not reproducible'
|
||||
fi
|
||||
|
||||
if python3 -B "$generator" generate --spec "$spec" --seed "$seed" \
|
||||
--output "$first" >"$artifacts/generate-first.json" \
|
||||
2>"$artifacts/generate-first.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B17 first fixture generation failed'
|
||||
fi
|
||||
if python3 -B "$generator" generate --spec "$spec" --seed "$seed" \
|
||||
--output "$second" >"$artifacts/generate-second.json" \
|
||||
2>"$artifacts/generate-second.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B17 second fixture generation failed'
|
||||
fi
|
||||
|
||||
if cmp "$first/fixture-index.json" "$second/fixture-index.json" && \
|
||||
find "$first" -type f ! -name fixture-index.json -printf '%f\n' | sort | \
|
||||
while IFS= read -r name; do cmp "$first/$name" "$second/$name" || exit 1; done
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B17 fixture generation is not byte reproducible'
|
||||
fi
|
||||
|
||||
if python3 -B "$oracle" --spec "$spec" --fixtures "$first" \
|
||||
--report "$artifacts/oracle-first.json" \
|
||||
>"$artifacts/oracle-first.stdout" 2>"$artifacts/oracle-first.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B17 independent first oracle replay failed'
|
||||
fi
|
||||
if python3 -B "$oracle" --spec "$spec" --fixtures "$second" \
|
||||
--report "$artifacts/oracle-second.json" \
|
||||
>"$artifacts/oracle-second.stdout" 2>"$artifacts/oracle-second.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B17 independent second oracle replay failed'
|
||||
fi
|
||||
|
||||
cp "$first/fixture-index.json" "$artifacts/B17-fixture-index.json"
|
||||
pre15_record_fixture b17-generated-index "$artifacts/B17-fixture-index.json"
|
||||
|
||||
if python3 - "$artifacts/B17-fixture-index.json" \
|
||||
"$artifacts/oracle-first.json" "$artifacts/B17-matrix.tsv" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
index = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
report = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
|
||||
if index["fixture_count"] != 25 or index["legal_count"] != 10 or index["damaged_count"] != 15:
|
||||
raise SystemExit("B17 fixture cardinality changed")
|
||||
if report["status"] != "PASS" or report["legal_passed"] != 10 or report["damaged_passed"] != 15:
|
||||
raise SystemExit("B17 oracle matrix is incomplete")
|
||||
lines = ["id\tclass\terrno\treject"]
|
||||
for item in report["results"]:
|
||||
lines.append(
|
||||
f"{item['id']}\t{item['class']}\t{item['actual_errno']}\t{item['actual_reject']}"
|
||||
)
|
||||
Path(sys.argv[3]).write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
print(
|
||||
f"B17 fixtures: {index['fixture_count']} "
|
||||
f"({index['legal_count']} legal, {index['damaged_count']} damaged)"
|
||||
)
|
||||
print(f"B17 fixture-set SHA256: {index['fixture_set_sha256']}")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B17 fixture matrix summary failed'
|
||||
fi
|
||||
|
||||
pre15_target_reached
|
||||
if test "$fixtures_only" -eq 1; then
|
||||
printf '%s\n' 'B17 fixture gate: READY'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$artifacts/B17-source-check.json" <<'PY'
|
||||
from pathlib import Path
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
output = Path(sys.argv[3])
|
||||
baseline = "98d76e4e73ed9a760b7f684d453b301c73167c22"
|
||||
|
||||
|
||||
def committed(name: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{name}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B17 baseline {name}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def replace_once(source: str, old: str, new: str, label: str) -> str:
|
||||
count = source.count(old)
|
||||
if count != 1:
|
||||
raise SystemExit(f"{label}: expected one baseline transform, found {count}")
|
||||
return source.replace(old, new, 1)
|
||||
|
||||
|
||||
current = {
|
||||
name: (dut / "src" / name).read_text(encoding="utf-8")
|
||||
for name in ("internal.h", "super.c", "xattr.c")
|
||||
}
|
||||
base = {name: committed(name) for name in current}
|
||||
|
||||
expected_internal = replace_once(
|
||||
base["internal.h"],
|
||||
"\tuint8_t xattr_prefix_count;\n",
|
||||
"\tuint8_t xattr_prefix_count;\n\tuint8_t xattr_filter_reserved;\n",
|
||||
"raw xattr filter state",
|
||||
)
|
||||
|
||||
expected_super = replace_once(
|
||||
base["super.c"],
|
||||
"\t/* A non-zero reserved value disables the current name-filter format. */\n"
|
||||
"\tif (erofs_sb_has_xattr_filter(sbi) && dsb->xattr_filter_reserved != 0)\n"
|
||||
"\t\tsbi->feature_compat &= ~EROFS_FEATURE_COMPAT_XATTR_FILTER;\n",
|
||||
"\t/* Preserve the raw feature declaration and gate its format at use sites. */\n"
|
||||
"\tsbi->xattr_filter_reserved = dsb->xattr_filter_reserved;\n",
|
||||
"raw feature preservation",
|
||||
)
|
||||
|
||||
expected_xattr = replace_once(
|
||||
base["xattr.c"],
|
||||
"\tif (entry_size > remaining)\n\t\treturn (EINTEGRITY);\n",
|
||||
"\tif (entry_size > remaining)\n\t\treturn (EINTEGRITY);\n"
|
||||
"\tif (memchr(entry->e_name, '\\0', entry->e_name_len) != NULL)\n"
|
||||
"\t\treturn (EINTEGRITY);\n",
|
||||
"inline name NUL validation",
|
||||
)
|
||||
expected_xattr = replace_once(
|
||||
expected_xattr,
|
||||
"\terror = erofs_xattr_read_backing(sbi, backing_en, off, entry_size,\n"
|
||||
"\t entrybuf);\n"
|
||||
"\tif (error != 0)\n"
|
||||
"\t\treturn (error);\n"
|
||||
"\tif (entry_sizep != NULL)\n",
|
||||
"\terror = erofs_xattr_read_backing(sbi, backing_en, off, entry_size,\n"
|
||||
"\t entrybuf);\n"
|
||||
"\tif (error != 0)\n"
|
||||
"\t\treturn (error);\n"
|
||||
"\terror = erofs_xattr_validate_entry(entrybuf->data, entry_size, NULL,\n"
|
||||
"\t value_sizep);\n"
|
||||
"\tif (error != 0) {\n"
|
||||
"\t\terofs_put_metabuf(entrybuf);\n"
|
||||
"\t\treturn (error);\n"
|
||||
"\t}\n"
|
||||
"\tif (entry_sizep != NULL)\n",
|
||||
"shared name NUL validation",
|
||||
)
|
||||
expected_xattr = replace_once(
|
||||
expected_xattr,
|
||||
"\tif (!erofs_sb_has_xattr_filter(sbi))\n\t\treturn (0);\n",
|
||||
"\tif (!erofs_sb_has_xattr_filter(sbi) ||\n"
|
||||
"\t sbi->xattr_filter_reserved != 0)\n"
|
||||
"\t\treturn (0);\n",
|
||||
"xattr filter use-site gate",
|
||||
)
|
||||
expected_xattr = replace_once(
|
||||
expected_xattr,
|
||||
"\t\tprefix = buf.data;\n"
|
||||
"\t\tinfix_len = len - sizeof(*prefix);\n"
|
||||
"\t\tsbi->xattr_prefixes[i].base_index = prefix->base_index;\n",
|
||||
"\t\tprefix = buf.data;\n"
|
||||
"\t\tinfix_len = len - sizeof(*prefix);\n"
|
||||
"\t\tif (memchr(prefix->infix, '\\0', infix_len) != NULL) {\n"
|
||||
"\t\t\terror = EINTEGRITY;\n"
|
||||
"\t\t\tgoto fail;\n"
|
||||
"\t\t}\n"
|
||||
"\t\tsbi->xattr_prefixes[i].base_index = prefix->base_index;\n",
|
||||
"long-prefix NUL validation",
|
||||
)
|
||||
|
||||
expected = {
|
||||
"internal.h": expected_internal,
|
||||
"super.c": expected_super,
|
||||
"xattr.c": expected_xattr,
|
||||
}
|
||||
for name in expected:
|
||||
if current[name] != expected[name]:
|
||||
raise SystemExit(f"{name} differs from the exact B17 ledger transforms")
|
||||
|
||||
diff = subprocess.run(
|
||||
["git", "-C", str(root), "diff", baseline, "--", "repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/super.c", "repo-pre-15/src/xattr.c"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout
|
||||
for forbidden in ("xxh32", "bloom", "cache owner", "cache waiter"):
|
||||
if forbidden in diff.lower():
|
||||
raise SystemExit(f"B17 source diff contains out-of-scope marker: {forbidden}")
|
||||
if "return (-E" in "".join(current.values()):
|
||||
raise SystemExit("negative errno entered the FreeBSD B17 source")
|
||||
|
||||
result = {
|
||||
"status": "PASS",
|
||||
"baseline": baseline,
|
||||
"changed_sources": sorted(expected),
|
||||
"raw_feature_preserved": True,
|
||||
"reserved_gated_at_use": True,
|
||||
"inline_name_nul": True,
|
||||
"shared_name_nul": True,
|
||||
"prefix_infix_nul": True,
|
||||
"acl_empty_suffix_preserved": True,
|
||||
}
|
||||
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print("B17 source: exact raw-feature and name-integrity transforms present")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B17 source does not match the fixture-authorized transforms'
|
||||
fi
|
||||
|
||||
printf '%s\n' 'B17 source gate: PASS'
|
||||
Executable
+359
@@ -0,0 +1,359 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=145d892afc8a425294029d4349f03f1b17cf8f92
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
xattr=$PRE15_DUT/src/xattr.c
|
||||
linux_xattr=$PRE15_ROOT/src-linux/xattr.c
|
||||
xattr_header=$PRE15_DUT/src/xattr.h
|
||||
vnops=$PRE15_DUT/src/erofs_vnops.c
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
fixture_spec=$fixture_dir/B17-xattr-spec.json
|
||||
fixture_generator=$fixture_dir/B17-xattr-generate.py
|
||||
fixture_oracle=$fixture_dir/B17-xattr-oracle.py
|
||||
fixture_seed=$fixture_dir/B17-xattr-seed.erofs
|
||||
|
||||
for tool in git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B18 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b18-freebsd-xattr "$xattr"
|
||||
pre15_record_fixture b18-linux-xattr "$linux_xattr"
|
||||
pre15_record_fixture b18-xattr-header "$xattr_header"
|
||||
pre15_record_fixture b18-vnops "$vnops"
|
||||
pre15_record_fixture b18-b17-spec "$fixture_spec"
|
||||
pre15_record_fixture b18-b17-generator "$fixture_generator"
|
||||
pre15_record_fixture b18-b17-oracle "$fixture_oracle"
|
||||
pre15_record_fixture b18-b17-seed "$fixture_seed"
|
||||
pre15_record_fixture b18-case \
|
||||
"$PRE15_DUT/tests/pre15/cases/B18-xattr-order.sh"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
|
||||
"$artifacts/B18-xattr-order.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
output = Path(sys.argv[4])
|
||||
xattr_path = dut / "src/xattr.c"
|
||||
current = xattr_path.read_text(encoding="utf-8")
|
||||
linux = (root / "src-linux/xattr.c").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def committed_bytes(path: str) -> bytes:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(
|
||||
f"cannot read B18 baseline {path}: "
|
||||
f"{completed.stderr.decode(errors='replace')}"
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def require_once(source: str, marker: str, label: str) -> int:
|
||||
count = source.count(marker)
|
||||
if count != 1:
|
||||
raise SystemExit(f"{label}: expected one marker, found {count}: {marker!r}")
|
||||
return source.index(marker)
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, match.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", match.end())
|
||||
if brace < 0:
|
||||
raise SystemExit(f"missing function body: {name}")
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def require_order(source: str, markers: tuple[str, ...], label: str) -> None:
|
||||
position = -1
|
||||
for marker in markers:
|
||||
position = source.find(marker, position + 1)
|
||||
if position < 0:
|
||||
raise SystemExit(f"{label}: missing ordered marker: {marker}")
|
||||
|
||||
|
||||
base = committed_bytes("src/xattr.c").decode("utf-8")
|
||||
iterator_start = require_once(base, "struct erofs_xattr_iter {", "iterator")
|
||||
iterator_end_marker = "\n};\n\n"
|
||||
iterator_end = base.find(iterator_end_marker, iterator_start)
|
||||
if iterator_end < 0:
|
||||
raise SystemExit("B18 baseline iterator terminator is absent")
|
||||
iterator_end += len(iterator_end_marker)
|
||||
iterator_block = base[iterator_start:iterator_end]
|
||||
|
||||
without_iterator = base[:iterator_start] + base[iterator_end:]
|
||||
acl_start = require_once(
|
||||
without_iterator,
|
||||
"static int\nerofs_inode_has_noacl(",
|
||||
"ACL/filter block",
|
||||
)
|
||||
acl_end = require_once(
|
||||
without_iterator,
|
||||
"static int\nerofs_listxattr_foreach(",
|
||||
"common iterator block",
|
||||
)
|
||||
if acl_end <= acl_start:
|
||||
raise SystemExit("B18 baseline ACL/filter block is not before the iterator core")
|
||||
acl_block = without_iterator[acl_start:acl_end]
|
||||
|
||||
expected = without_iterator[:acl_start] + without_iterator[acl_end:]
|
||||
core_start = require_once(
|
||||
expected,
|
||||
"static int\nerofs_xattr_backing_size(",
|
||||
"backing core",
|
||||
)
|
||||
expected = expected[:core_start] + iterator_block + expected[core_start:]
|
||||
acl_adapter = require_once(expected, "int\nerofs_get_acl(", "ACL adapter")
|
||||
expected = expected[:acl_adapter] + acl_block + expected[acl_adapter:]
|
||||
if current != expected:
|
||||
raise SystemExit("xattr.c differs from the two exact B18 definition moves")
|
||||
|
||||
base_names = re.findall(r"^(erofs_[A-Za-z0-9_]+)\s*\(", base, re.MULTILINE)
|
||||
current_names = re.findall(
|
||||
r"^(erofs_[A-Za-z0-9_]+)\s*\(", current, re.MULTILINE
|
||||
)
|
||||
if Counter(base_names) != Counter(current_names):
|
||||
raise SystemExit("B18 changed the xattr function definition multiset")
|
||||
|
||||
base_functions = {name: extract_function(base, name) for name in base_names}
|
||||
current_functions = {name: extract_function(current, name) for name in current_names}
|
||||
for name in base_functions:
|
||||
if current_functions[name] != base_functions[name]:
|
||||
raise SystemExit(f"B18 changed function text instead of moving it: {name}")
|
||||
if current_functions[name].startswith("static ") != base_functions[name].startswith(
|
||||
"static "
|
||||
):
|
||||
raise SystemExit(f"B18 changed symbol visibility: {name}")
|
||||
|
||||
|
||||
def direct_calls(functions: dict[str, str]) -> Counter[tuple[str, str]]:
|
||||
calls: Counter[tuple[str, str]] = Counter()
|
||||
for caller, function in functions.items():
|
||||
brace = function.find("{")
|
||||
for callee in re.findall(
|
||||
r"\b(erofs_[A-Za-z0-9_]+)\s*\(", function[brace + 1 :]
|
||||
):
|
||||
calls[(caller, callee)] += 1
|
||||
return calls
|
||||
|
||||
|
||||
base_calls = direct_calls(base_functions)
|
||||
current_calls = direct_calls(current_functions)
|
||||
if current_calls != base_calls:
|
||||
raise SystemExit("B18 changed the direct-call multiset")
|
||||
|
||||
expected_order = [
|
||||
"erofs_xattr_backing_size",
|
||||
"erofs_xattr_read_backing",
|
||||
"erofs_xattr_read_metadata",
|
||||
"erofs_xattr_move",
|
||||
"erofs_xattr_load_body",
|
||||
"erofs_xattr_validate_entry",
|
||||
"erofs_xattr_prefix",
|
||||
"erofs_xattr_namespace_prefix",
|
||||
"erofs_xattr_list_move",
|
||||
"erofs_xattr_resolve_name",
|
||||
"erofs_xattr_name_match",
|
||||
"erofs_xattr_shared_entry_offset",
|
||||
"erofs_xattr_load_shared_entry",
|
||||
"erofs_listxattr_foreach",
|
||||
"erofs_getxattr_foreach",
|
||||
"erofs_xattr_iter_inline",
|
||||
"erofs_xattr_iter_shared",
|
||||
"erofs_getxattr",
|
||||
"erofs_listxattr",
|
||||
"erofs_xattr_prefixes_cleanup",
|
||||
"erofs_xattr_prefixes_init",
|
||||
"erofs_inode_has_noacl",
|
||||
"erofs_acl_from_mode",
|
||||
"erofs_posix_acl_from_xattr",
|
||||
"erofs_get_acl",
|
||||
]
|
||||
if current_names != expected_order:
|
||||
raise SystemExit(f"B18 xattr definition order mismatch: {current_names}")
|
||||
if current.index("struct erofs_xattr_iter {") > current.index(
|
||||
"erofs_xattr_backing_size("
|
||||
):
|
||||
raise SystemExit("xattr iterator contract is not before the backing/core helpers")
|
||||
|
||||
linux_aligned_core = (
|
||||
"erofs_listxattr_foreach(",
|
||||
"erofs_getxattr_foreach(",
|
||||
"erofs_xattr_iter_inline(",
|
||||
"erofs_xattr_iter_shared(",
|
||||
"erofs_getxattr(",
|
||||
"erofs_listxattr(",
|
||||
"erofs_xattr_prefixes_cleanup(",
|
||||
"erofs_xattr_prefixes_init(",
|
||||
)
|
||||
require_order(current, linux_aligned_core, "FreeBSD common xattr core")
|
||||
require_order(linux, linux_aligned_core, "Linux common xattr core")
|
||||
|
||||
readonly_paths = (
|
||||
"src/xattr.h",
|
||||
"src/erofs_vnops.c",
|
||||
"tests/pre15/fixtures/B17-xattr-spec.json",
|
||||
"tests/pre15/fixtures/B17-xattr-generate.py",
|
||||
"tests/pre15/fixtures/B17-xattr-oracle.py",
|
||||
"tests/pre15/fixtures/B17-xattr-seed.erofs",
|
||||
)
|
||||
for path in readonly_paths:
|
||||
if (dut / path).read_bytes() != committed_bytes(path):
|
||||
raise SystemExit(f"B18 changed a read-only xattr/VOP/fixture contract: {path}")
|
||||
|
||||
vnops = (dut / "src/erofs_vnops.c").read_text(encoding="utf-8")
|
||||
getextattr = extract_function(vnops, "erofs_getextattr")
|
||||
listextattr = extract_function(vnops, "erofs_listextattr")
|
||||
require_order(
|
||||
getextattr,
|
||||
("extattr_check_cred(", "switch (ap->a_attrnamespace)", "erofs_getxattr("),
|
||||
"FreeBSD getextattr adapter",
|
||||
)
|
||||
require_order(
|
||||
listextattr,
|
||||
("extattr_check_cred(", "switch (ap->a_attrnamespace)", "erofs_listxattr("),
|
||||
"FreeBSD listextattr adapter",
|
||||
)
|
||||
if vnops.count("extattr_check_cred(") != 2:
|
||||
raise SystemExit("FreeBSD extattr credential boundary changed")
|
||||
if "ERANGE" in current:
|
||||
raise SystemExit("Linux all-or-nothing xattr buffer semantics entered FreeBSD")
|
||||
if "uiomove(value, value_size, uio)" not in current:
|
||||
raise SystemExit("FreeBSD partial extattr transfer path is absent")
|
||||
if re.search(r"return\s*(?:\(\s*)?-E[A-Z0-9_]+", current):
|
||||
raise SystemExit("Linux negative errno entered FreeBSD xattr.c")
|
||||
|
||||
load_body = extract_function(current, "erofs_xattr_load_body")
|
||||
require_order(
|
||||
load_body,
|
||||
(
|
||||
"vi->xattr_isize < sizeof(*ih)",
|
||||
"vi->xattr_isize == sizeof(*ih)",
|
||||
"error = EOPNOTSUPP",
|
||||
"header_size = sizeof(*ih)",
|
||||
),
|
||||
"exact-header compatibility",
|
||||
)
|
||||
|
||||
spec_path = dut / "tests/pre15/fixtures/B17-xattr-spec.json"
|
||||
spec = json.loads(spec_path.read_text(encoding="ascii"))
|
||||
cases = spec.get("cases", [])
|
||||
legal = sum(item.get("class") == "legal" for item in cases)
|
||||
damaged = sum(item.get("class") == "damaged" for item in cases)
|
||||
if len(cases) != 25 or legal != 10 or damaged != 15:
|
||||
raise SystemExit("B17 xattr fixture order/cardinality changed")
|
||||
seed_path = dut / "tests/pre15/fixtures" / spec["seed"]["path"]
|
||||
seed_hash = hashlib.sha256(seed_path.read_bytes()).hexdigest()
|
||||
if seed_hash != spec["seed"]["sha256"]:
|
||||
raise SystemExit("B17 xattr seed no longer matches its frozen specification")
|
||||
|
||||
result = {
|
||||
"status": "PASS",
|
||||
"baseline": baseline,
|
||||
"source_sha256": hashlib.sha256(current.encode("utf-8")).hexdigest(),
|
||||
"definition_count": len(current_names),
|
||||
"direct_call_edges": len(current_calls),
|
||||
"direct_call_sites": sum(current_calls.values()),
|
||||
"static_visibility_unchanged": True,
|
||||
"function_text_unchanged": True,
|
||||
"linux_core_order": list(linux_aligned_core),
|
||||
"freebsd_contracts": [
|
||||
"positive errno",
|
||||
"partial uiomove",
|
||||
"extattr_check_cred",
|
||||
"USER and SYSTEM namespaces",
|
||||
"exact-header EOPNOTSUPP",
|
||||
"uncached xattr lookup",
|
||||
],
|
||||
"b17_fixture_cases": len(cases),
|
||||
"b17_fixture_legal": legal,
|
||||
"b17_fixture_damaged": damaged,
|
||||
"b17_seed_sha256": seed_hash,
|
||||
}
|
||||
output.write_text(
|
||||
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
print(
|
||||
"B18 xattr order: exact moves, "
|
||||
f"{len(current_names)} definitions, "
|
||||
f"{sum(current_calls.values())} direct calls"
|
||||
)
|
||||
print(f"B18 B17 fixture contract: {len(cases)} cases, seed {seed_hash}")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B18 xattr ordering or FreeBSD contract equivalence failed'
|
||||
fi
|
||||
|
||||
printf '%s\n' 'B18 xattr order: PASS'
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "$PRE15_DUT"
|
||||
: "$PRE15_ROOT"
|
||||
: "$PRE15_CASE_TMP"
|
||||
: "$PRE15_RUN_DIR"
|
||||
: "$PRE15_LIB_DIR"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
test "$PRE15_MODE" = qemu || pre15_infra_blocked \
|
||||
'B19a focused case requires exact-ABI QEMU mode'
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
generator=$fixture_dir/B19a-xattr-generate.py
|
||||
spec=$fixture_dir/B19a-xattr-spec.json
|
||||
probe=$fixture_dir/B19a-xattr-probe.c
|
||||
kld_builder=$fixture_dir/B28-build-kld.sh
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
first=$PRE15_CASE_TMP/fixtures-first
|
||||
second=$PRE15_CASE_TMP/fixtures-second
|
||||
module=$PRE15_CASE_TMP/B19a-erofs.ko
|
||||
|
||||
for tool in cc cmp diff file mkfs.erofs nm python3 scp sha256sum tar timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B19a QEMU tool: $tool"
|
||||
done
|
||||
for value in PRE15_QEMU_CONTROL_PATH PRE15_QEMU_SSH_KEY \
|
||||
PRE15_QEMU_SSH_PORT PRE15_QEMU_SSH_USER; do
|
||||
test -n "$(eval "printf '%s' \"\$$value\"")" || \
|
||||
pre15_runner_fail "$value is required"
|
||||
done
|
||||
|
||||
for fixture in "$generator" "$spec" "$probe" "$kld_builder"; do
|
||||
pre15_record_fixture "b19a-$(basename "$fixture")" "$fixture"
|
||||
done
|
||||
for source in erofs_vnops.c internal.h xattr.c; do
|
||||
pre15_record_fixture "b19a-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if ! timeout -k 5 180 python3 -B "$generator" --spec "$spec" \
|
||||
--output "$first" --work "$PRE15_CASE_TMP/generate-first" \
|
||||
>"$artifacts/generate-first.json" 2>"$artifacts/generate-first.stderr"; then
|
||||
pre15_runner_fail 'B19a first fixture generation failed'
|
||||
fi
|
||||
if ! timeout -k 5 180 python3 -B "$generator" --spec "$spec" \
|
||||
--output "$second" --work "$PRE15_CASE_TMP/generate-second" \
|
||||
>"$artifacts/generate-second.json" 2>"$artifacts/generate-second.stderr"; then
|
||||
pre15_runner_fail 'B19a second fixture generation failed'
|
||||
fi
|
||||
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
|
||||
pre15_runner_fail 'B19a focused fixtures are not byte reproducible'
|
||||
fi
|
||||
pre15_record_fixture b19a-valid-image "$first/valid.erofs"
|
||||
tar -C "$first" -czf "$PRE15_CASE_TMP/B19a-fixtures.tar.gz" .
|
||||
|
||||
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
||||
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
|
||||
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
|
||||
pre15_dut_fail 'B19a exact-ABI KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 60 scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$PRE15_CASE_TMP/B19a-fixtures.tar.gz" \
|
||||
/root/B19a-fixtures.tar.gz || ! pre15_scp "$probe" /root/B19a-xattr-probe.c ||
|
||||
! pre15_scp "$module" /root/B19a-erofs.ko; then
|
||||
pre15_infra_blocked 'could not transfer B19a module, probe, or fixtures'
|
||||
fi
|
||||
pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/pre15-b19a-fixtures && mkdir /root/pre15-b19a-fixtures && tar -xzf /root/B19a-fixtures.tar.gz -C /root/pre15-b19a-fixtures' ||
|
||||
pre15_infra_blocked 'could not prepare B19a guest fixtures'
|
||||
pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
|
||||
-o /root/B19a-xattr-probe /root/B19a-xattr-probe.c ||
|
||||
pre15_infra_blocked 'could not compile the B19a guest probe'
|
||||
if pre15_guest_ssh_bounded kldstat -n B19a-erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'guest already has an EROFS KLD loaded'
|
||||
fi
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/dmesg-before-load.txt"
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B19a-erofs.ko \
|
||||
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
|
||||
pre15_dut_fail 'B19a exact-ABI KLD load failed'
|
||||
fi
|
||||
pre15_own_guest_kld B19a-erofs 'B19a exact-source KLD'
|
||||
pre15_guest_ssh_bounded kldstat >"$artifacts/kldstat-after-load.txt"
|
||||
|
||||
b19a_unown()
|
||||
{
|
||||
tmp=$PRE15_CASE_TMP/ownership.$$
|
||||
awk -F ' ' -v kind="$1" -v value="$2" \
|
||||
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
|
||||
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
|
||||
}
|
||||
|
||||
b19a_mount()
|
||||
{
|
||||
image=$1
|
||||
mountpoint=$2
|
||||
label=$3
|
||||
md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "/root/pre15-b19a-fixtures/$image") ||
|
||||
pre15_dut_fail "$label provider attach failed"
|
||||
case "$md" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected B19a md unit: $md" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$md" "$label provider"
|
||||
pre15_guest_ssh_bounded mkdir -p "$mountpoint"
|
||||
if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$md" \
|
||||
"$mountpoint"; then
|
||||
pre15_dut_fail "$label mount failed"
|
||||
fi
|
||||
pre15_own_guest_mount "$mountpoint" "$label mount"
|
||||
printf '%s\n' "$md"
|
||||
}
|
||||
|
||||
b19a_detach()
|
||||
{
|
||||
mountpoint=$1
|
||||
md=$2
|
||||
pre15_guest_ssh_bounded umount "$mountpoint" ||
|
||||
pre15_dut_fail "B19a unmount failed: $mountpoint"
|
||||
b19a_unown guest-mount "$mountpoint"
|
||||
pre15_guest_ssh_bounded mdconfig -d -u "$md" ||
|
||||
pre15_dut_fail "B19a md detach failed: $md"
|
||||
b19a_unown guest-md "$md"
|
||||
}
|
||||
|
||||
valid_md=$(b19a_mount valid.erofs /mnt/pre15-b19a-valid 'B19a valid')
|
||||
valid=/mnt/pre15-b19a-valid/target.bin
|
||||
pre15_guest_ssh_bounded /root/B19a-xattr-probe valid "$valid" \
|
||||
>"$artifacts/valid-first.txt"
|
||||
pre15_guest_ssh_bounded /root/B19a-xattr-probe concurrent "$valid" 16 10 \
|
||||
>"$artifacts/valid-concurrent.txt"
|
||||
pre15_guest_ssh_bounded /root/B19a-xattr-probe valid "$valid" \
|
||||
>"$artifacts/valid-repeat.txt"
|
||||
b19a_detach /mnt/pre15-b19a-valid "$valid_md"
|
||||
|
||||
for image in corrupt-shared-count.erofs corrupt-shared-id.erofs corrupt-inline-name.erofs; do
|
||||
label=$(basename "$image" .erofs)
|
||||
mountpoint=/mnt/pre15-b19a-$label
|
||||
md=$(b19a_mount "$image" "$mountpoint" "B19a $label")
|
||||
if ! pre15_guest_ssh_bounded /root/B19a-xattr-probe corrupt \
|
||||
"$mountpoint/target.bin" >"$artifacts/$label.txt" 2>&1; then
|
||||
pre15_dut_fail "$label did not return EINTEGRITY"
|
||||
fi
|
||||
pre15_guest_ssh_bounded umount "$mountpoint" ||
|
||||
pre15_dut_fail "$label unmount failed"
|
||||
b19a_unown guest-mount "$mountpoint"
|
||||
pre15_guest_ssh_bounded mdconfig -d -u "$md" ||
|
||||
pre15_dut_fail "$label md detach failed"
|
||||
b19a_unown guest-md "$md"
|
||||
done
|
||||
|
||||
normal_md=$(b19a_mount valid.erofs /mnt/pre15-b19a-normal \
|
||||
'B19a normal-unmount')
|
||||
normal_pid=$(pre15_guest_ssh_bounded \
|
||||
'cd /mnt/pre15-b19a-normal && sleep 60 >/tmp/B19a-normal.sleep 2>&1 & echo $!')
|
||||
if pre15_guest_ssh_bounded umount /mnt/pre15-b19a-normal \
|
||||
>"$artifacts/normal-unmount.stdout" 2>"$artifacts/normal-unmount.stderr"; then
|
||||
pre15_dut_fail 'normal unmount unexpectedly ignored a held vnode'
|
||||
fi
|
||||
pre15_guest_ssh_bounded kill "$normal_pid" || true
|
||||
pre15_guest_ssh_bounded umount /mnt/pre15-b19a-normal ||
|
||||
pre15_dut_fail 'normal unmount failed after releasing held vnode'
|
||||
b19a_unown guest-mount /mnt/pre15-b19a-normal
|
||||
pre15_guest_ssh_bounded mdconfig -d -u "$normal_md" ||
|
||||
pre15_dut_fail 'normal-unmount md detach failed'
|
||||
b19a_unown guest-md "$normal_md"
|
||||
|
||||
forced_md=$(b19a_mount valid.erofs /mnt/pre15-b19a-forced \
|
||||
'B19a forced-unmount')
|
||||
forced_pid=$(pre15_guest_ssh_bounded \
|
||||
'cd /mnt/pre15-b19a-forced && sleep 60 >/tmp/B19a-forced.sleep 2>&1 & echo $!')
|
||||
pre15_guest_ssh_bounded umount -f /mnt/pre15-b19a-forced ||
|
||||
pre15_dut_fail 'forced unmount failed'
|
||||
b19a_unown guest-mount /mnt/pre15-b19a-forced
|
||||
pre15_guest_ssh_bounded kill "$forced_pid" || true
|
||||
pre15_guest_ssh_bounded mdconfig -d -u "$forced_md" ||
|
||||
pre15_dut_fail 'forced-unmount md detach failed'
|
||||
b19a_unown guest-md "$forced_md"
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/dmesg-after.txt"
|
||||
diff -u "$artifacts/dmesg-before-load.txt" "$artifacts/dmesg-after.txt" \
|
||||
>"$artifacts/dmesg.diff" || true
|
||||
sed -n '/^+++ /d; /^+/s/^+/ /p' "$artifacts/dmesg.diff" \
|
||||
>"$artifacts/dmesg-added.txt"
|
||||
if grep -Eqi 'panic:|fatal trap|lock order reversal|witness.*warning|use-after-free|pager fault|undefined symbol|linker.*error' \
|
||||
"$artifacts/dmesg-added.txt"; then
|
||||
pre15_dut_fail 'B19a QEMU produced a kernel diagnostic'
|
||||
fi
|
||||
pre15_guest_ssh_bounded kldunload B19a-erofs ||
|
||||
pre15_dut_fail 'B19a KLD unload failed'
|
||||
b19a_unown guest-kld B19a-erofs
|
||||
if pre15_guest_ssh_bounded kldstat -n B19a-erofs >/dev/null 2>&1; then
|
||||
pre15_dut_fail 'B19a KLD remained loaded after cleanup'
|
||||
fi
|
||||
pre15_guest_ssh_bounded mount >"$artifacts/mount-after.txt"
|
||||
pre15_guest_ssh_bounded mdconfig -l >"$artifacts/md-after.txt"
|
||||
if grep -q 'pre15-b19a-' "$artifacts/mount-after.txt" ||
|
||||
grep -q 'B19a' "$artifacts/md-after.txt"; then
|
||||
pre15_dut_fail 'B19a guest mount or md resource remained after cleanup'
|
||||
fi
|
||||
pre15_target_reached
|
||||
printf '%s\n' 'B19a exact-ABI xattr cache lifecycle PASS'
|
||||
Executable
+220
@@ -0,0 +1,220 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B19b-xattr-spec.json
|
||||
generator=$fixture_dir/B19b-xattr-generate.py
|
||||
oracle=$fixture_dir/B19b-xattr-oracle.py
|
||||
probe=$fixture_dir/B19b-xattr-probe.c
|
||||
kld_builder=$fixture_dir/B19b-build-kld.sh
|
||||
gate_script=$PRE15_DUT/tests/pre15/gates/P15-021.sh
|
||||
gate_input=$PRE15_DUT/tests/pre15/gates/P15-021-input.json
|
||||
gate_commit=675ed9b650b3bc157b38bcc165a0cb215a2aa989
|
||||
gate_base=666e52f710363df07f7c93919eb835d41092d011
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
first=$PRE15_CASE_TMP/first
|
||||
second=$PRE15_CASE_TMP/second
|
||||
gate_clone=$PRE15_CASE_TMP/gate-clone
|
||||
gate_output=$PRE15_CASE_TMP/gate-output
|
||||
|
||||
for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B19b host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b19b-spec "$spec"
|
||||
pre15_record_fixture b19b-generator "$generator"
|
||||
pre15_record_fixture b19b-oracle "$oracle"
|
||||
pre15_record_fixture b19b-probe "$probe"
|
||||
pre15_record_fixture b19b-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b19b-case "$PRE15_DUT/tests/pre15/cases/B19b-xattr-bloom.sh"
|
||||
pre15_record_fixture b19b-gate "$gate_script"
|
||||
pre15_record_fixture b19b-gate-input "$gate_input"
|
||||
pre15_record_fixture b19b-erofs-fs "$PRE15_DUT/src/erofs_fs.h"
|
||||
pre15_record_fixture b19b-internal "$PRE15_DUT/src/internal.h"
|
||||
pre15_record_fixture b19b-xattr "$PRE15_DUT/src/xattr.c"
|
||||
pre15_record_fixture b19b-linux-erofs-fs "$PRE15_ROOT/src-linux/erofs_fs.h"
|
||||
pre15_record_fixture b19b-linux-xattr "$PRE15_ROOT/src-linux/xattr.c"
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if ! git clone -q --shared --no-checkout "$PRE15_ROOT" "$gate_clone" \
|
||||
>"$artifacts/gate-clone.stdout" 2>"$artifacts/gate-clone.stderr" || \
|
||||
! git -C "$gate_clone" checkout -q --detach "$gate_commit" \
|
||||
>"$artifacts/gate-checkout.stdout" 2>"$artifacts/gate-checkout.stderr"; then
|
||||
pre15_runner_fail 'could not materialize the committed P15-021 gate'
|
||||
fi
|
||||
frozen_gate=$gate_clone/repo-pre-15/tests/pre15/gates/P15-021.sh
|
||||
if ! timeout -k 10 240 "$frozen_gate" --base "$gate_base" \
|
||||
--output "$gate_output" >"$artifacts/gate.stdout" \
|
||||
2>"$artifacts/gate.stderr"; then
|
||||
pre15_runner_fail 'committed P15-021 gate did not replay GO'
|
||||
fi
|
||||
|
||||
if ! python3 -B "$generator" generate --spec "$spec" --output "$first" \
|
||||
--work "$PRE15_CASE_TMP/first-work" >"$artifacts/generate-first.stdout" \
|
||||
2>"$artifacts/generate-first.stderr"; then
|
||||
pre15_runner_fail 'B19b first real-fixture generation failed'
|
||||
fi
|
||||
if ! python3 -B "$generator" generate --spec "$spec" --output "$second" \
|
||||
--work "$PRE15_CASE_TMP/second-work" >"$artifacts/generate-second.stdout" \
|
||||
2>"$artifacts/generate-second.stderr"; then
|
||||
pre15_runner_fail 'B19b repeat real-fixture generation failed'
|
||||
fi
|
||||
if ! cmp "$first/SHA256SUMS" "$second/SHA256SUMS" || \
|
||||
! cmp "$first/manifest.json" "$second/manifest.json"; then
|
||||
pre15_runner_fail 'B19b fixture generation is not byte reproducible'
|
||||
fi
|
||||
for image in valid.erofs unknown-filter.erofs feature-off.erofs \
|
||||
corrupt-shared-count.erofs corrupt-shared-id.erofs; do
|
||||
if ! cmp "$first/$image" "$second/$image"; then
|
||||
pre15_runner_fail "B19b repeat image differs: $image"
|
||||
fi
|
||||
pre15_record_fixture "b19b-$image" "$first/$image"
|
||||
done
|
||||
|
||||
if ! python3 -B "$oracle" --spec "$spec" --fixtures "$first" \
|
||||
--root "$PRE15_ROOT" --dut "$PRE15_DUT" \
|
||||
--report "$artifacts/B19b-oracle.json" \
|
||||
>"$artifacts/oracle.stdout" 2>"$artifacts/oracle.stderr"; then
|
||||
pre15_dut_fail 'B19b fixture/source/Linux oracle failed'
|
||||
fi
|
||||
cp "$gate_output/result.json" "$artifacts/P15-021-gate-result.json"
|
||||
cp "$gate_output/million.json" "$artifacts/P15-021-million.json"
|
||||
cp "$gate_output/benchmark.json" "$artifacts/P15-021-benchmark.json"
|
||||
cp "$first/SHA256SUMS" "$artifacts/B19b-SHA256SUMS"
|
||||
pre15_record_fixture b19b-generated-sums "$artifacts/B19b-SHA256SUMS"
|
||||
pre15_target_reached
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'TC169 host real fixture/hash/filter/integrity/concurrency oracle PASS' \
|
||||
'QEMU runtime NOT_RUN in host mode'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in awk clang file nm scp tar; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B19b QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B19b-fixtures.tar.gz
|
||||
module=$PRE15_CASE_TMP/B19b-erofs.ko
|
||||
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B19b-kld-build.stdout" \
|
||||
2>"$artifacts/B19b-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B19b cross-target KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/B19b-kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B19b-kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/B19b-kld-nm-u.txt"
|
||||
tar -C "$first" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B19b-fixtures.tar.gz || \
|
||||
! pre15_scp "$probe" /root/B19b-xattr-probe.c || \
|
||||
! pre15_scp "$module" /root/B19b-erofs.ko; then
|
||||
pre15_infra_blocked 'could not transfer B19b module, probe, or fixtures'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/pre15-b19b-fixtures && mkdir /root/pre15-b19b-fixtures && tar -xzf /root/B19b-fixtures.tar.gz -C /root/pre15-b19b-fixtures'; then
|
||||
pre15_infra_blocked 'could not prepare B19b guest fixtures'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B19b-erofs.ko \
|
||||
>"$artifacts/B19b-kldload.stdout" 2>"$artifacts/B19b-kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' "$artifacts/B19b-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_dut_fail 'B19b exact-source KLD failed to load'
|
||||
fi
|
||||
pre15_own_guest_kld erofs 'B19b exact-source KLD'
|
||||
if ! pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
|
||||
-o /root/B19b-xattr-probe /root/B19b-xattr-probe.c; then
|
||||
pre15_infra_blocked 'could not compile the B19b guest probe'
|
||||
fi
|
||||
|
||||
pre15_mount_image()
|
||||
{
|
||||
pre15_image=$1
|
||||
pre15_mountpoint=$2
|
||||
pre15_label=$3
|
||||
pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "/root/pre15-b19b-fixtures/$pre15_image") || \
|
||||
pre15_dut_fail "could not attach B19b provider: $pre15_image"
|
||||
case "$pre15_md" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$pre15_md" "$pre15_label provider"
|
||||
pre15_guest_ssh_bounded mkdir -p "$pre15_mountpoint"
|
||||
if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$pre15_md" \
|
||||
"$pre15_mountpoint"; then
|
||||
pre15_dut_fail "$pre15_label mount failed"
|
||||
fi
|
||||
pre15_own_guest_mount "$pre15_mountpoint" "$pre15_label mount"
|
||||
}
|
||||
|
||||
pre15_mount_image valid.erofs /mnt/pre15-b19b-valid 'B19b valid'
|
||||
valid=/mnt/pre15-b19b-valid/target.bin
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe hit "$valid" attr00 0
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno "$valid" user \
|
||||
absent-00001 87
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno "$valid" user \
|
||||
absent-00000 87
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe list "$valid"
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe concurrent "$valid" attr00 \
|
||||
absent-00001 absent-00000 64 10
|
||||
|
||||
pre15_mount_image unknown-filter.erofs /mnt/pre15-b19b-unknown \
|
||||
'B19b unknown filter'
|
||||
unknown=/mnt/pre15-b19b-unknown/target.bin
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe hit "$unknown" attr00 0
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno "$unknown" user \
|
||||
absent-00001 87
|
||||
|
||||
pre15_mount_image feature-off.erofs /mnt/pre15-b19b-feature-off \
|
||||
'B19b feature off'
|
||||
feature_off=/mnt/pre15-b19b-feature-off/target.bin
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe hit "$feature_off" attr00 0
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno "$feature_off" user \
|
||||
absent-00001 87
|
||||
|
||||
pre15_mount_image corrupt-shared-count.erofs /mnt/pre15-b19b-bad-count \
|
||||
'B19b corrupt shared count'
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno \
|
||||
/mnt/pre15-b19b-bad-count/target.bin user absent-00001 97
|
||||
|
||||
pre15_mount_image corrupt-shared-id.erofs /mnt/pre15-b19b-bad-id \
|
||||
'B19b corrupt shared ID'
|
||||
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno \
|
||||
/mnt/pre15-b19b-bad-id/target.bin user attr00 97
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/B19b-dmesg.txt"
|
||||
if grep -Eq 'panic:|Fatal trap|lock order reversal|KDB: stack backtrace' \
|
||||
"$artifacts/B19b-dmesg.txt"; then
|
||||
pre15_dut_fail 'B19b runtime produced a kernel diagnostic'
|
||||
fi
|
||||
printf '%s\n' 'TC169 QEMU hit/miss/collision/fallback/integrity/concurrency PASS'
|
||||
@@ -0,0 +1,540 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=e769e4ae32d4967c1f69067dc92628b60b62b648
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B21-super-spec.json
|
||||
generator=$fixture_dir/B21-super-generate.py
|
||||
oracle=$fixture_dir/B21-super-oracle.py
|
||||
qemu_probe=$fixture_dir/B21-qemu-probe.c
|
||||
kld_builder=$fixture_dir/B28-build-kld.sh
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
first=$PRE15_CASE_TMP/first
|
||||
second=$PRE15_CASE_TMP/second
|
||||
|
||||
for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B21 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b21-spec "$spec"
|
||||
pre15_record_fixture b21-generator "$generator"
|
||||
pre15_record_fixture b21-oracle "$oracle"
|
||||
pre15_record_fixture b21-qemu-probe "$qemu_probe"
|
||||
pre15_record_fixture b21-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b21-case "$PRE15_DUT/tests/pre15/cases/B21-super.sh"
|
||||
pre15_record_fixture b21-super "$PRE15_DUT/src/super.c"
|
||||
pre15_record_fixture b21-internal "$PRE15_DUT/src/internal.h"
|
||||
pre15_record_fixture b21-xattr "$PRE15_DUT/src/xattr.c"
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if python3 -B "$generator" --spec "$spec" --output "$first" \
|
||||
--work "$PRE15_CASE_TMP/first-work" \
|
||||
>"$artifacts/generate-first.json" \
|
||||
2>"$artifacts/generate-first.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B21 first fixture generation failed'
|
||||
fi
|
||||
if python3 -B "$generator" --spec "$spec" --output "$second" \
|
||||
--work "$PRE15_CASE_TMP/second-work" \
|
||||
>"$artifacts/generate-second.json" \
|
||||
2>"$artifacts/generate-second.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B21 second fixture generation failed'
|
||||
fi
|
||||
|
||||
if cmp "$first/fixture-index.json" "$second/fixture-index.json" && \
|
||||
find "$first" -type f ! -name fixture-index.json -printf '%f\n' | sort | \
|
||||
while IFS= read -r name; do cmp "$first/$name" "$second/$name" || exit 1; done
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B21 fixture generation is not byte reproducible'
|
||||
fi
|
||||
|
||||
if python3 -B "$oracle" --fixtures "$first" \
|
||||
--report "$artifacts/oracle-first.json" \
|
||||
>"$artifacts/oracle-first.stdout" 2>"$artifacts/oracle-first.stderr" && \
|
||||
python3 -B "$oracle" --fixtures "$second" \
|
||||
--report "$artifacts/oracle-second.json" \
|
||||
>"$artifacts/oracle-second.stdout" 2>"$artifacts/oracle-second.stderr" && \
|
||||
cmp "$artifacts/oracle-first.json" "$artifacts/oracle-second.json"
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B21 independent oracle replay failed'
|
||||
fi
|
||||
|
||||
if fsck.erofs -d0 "$first/legal-control.erofs" \
|
||||
>"$artifacts/legal-fsck.stdout" 2>"$artifacts/legal-fsck.stderr"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B21 legal control is not accepted by fsck.erofs'
|
||||
fi
|
||||
|
||||
cp "$first/fixture-index.json" "$artifacts/B21-fixture-index.json"
|
||||
pre15_record_fixture b21-generated-index "$artifacts/B21-fixture-index.json"
|
||||
pre15_target_reached
|
||||
|
||||
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
|
||||
:
|
||||
else
|
||||
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
|
||||
"$artifacts/B21-source-check.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
output = Path(sys.argv[4])
|
||||
src = dut / "src"
|
||||
|
||||
|
||||
def committed(name: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{name}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B21 baseline {name}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def replace_once(source: str, old: str, new: str, label: str) -> str:
|
||||
count = source.count(old)
|
||||
if count != 1:
|
||||
raise SystemExit(f"{label}: expected one transform source, found {count}")
|
||||
return source.replace(old, new, 1)
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, match.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", match.end())
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def require_order(source: str, markers: list[str], label: str) -> None:
|
||||
position = -1
|
||||
for marker in markers:
|
||||
position = source.find(marker, position + 1)
|
||||
if position < 0:
|
||||
raise SystemExit(f"{label} is missing ordered marker: {marker}")
|
||||
|
||||
|
||||
current = {
|
||||
name: (src / name).read_text(encoding="utf-8")
|
||||
for name in ("internal.h", "super.c", "xattr.c")
|
||||
}
|
||||
base = {name: committed(name) for name in current}
|
||||
|
||||
if current["internal.h"] != base["internal.h"]:
|
||||
raise SystemExit("B21 changed internal.h despite all required helpers existing")
|
||||
for helper in (
|
||||
"EROFS_FEATURE_FUNCS(fragments, incompat, INCOMPAT_FRAGMENTS)",
|
||||
"EROFS_FEATURE_FUNCS(sb_chksum, compat, COMPAT_SB_CHKSUM)",
|
||||
"EROFS_FEATURE_FUNCS(plain_xattr_pfx, compat, COMPAT_PLAIN_XATTR_PFX)",
|
||||
):
|
||||
if current["internal.h"].count(helper) != 1:
|
||||
raise SystemExit(f"B21 feature helper is missing or duplicated: {helper}")
|
||||
|
||||
expected_super = replace_once(
|
||||
base["super.c"],
|
||||
"\tif ((le32toh(dsb->feature_compat) & EROFS_FEATURE_COMPAT_SB_CHKSUM) == 0)\n",
|
||||
"\tif (!erofs_sb_has_sb_chksum(sbi))\n",
|
||||
"checksum helper",
|
||||
)
|
||||
expected_super = replace_once(
|
||||
expected_super,
|
||||
"\tif ((sbi->feature_incompat & EROFS_FEATURE_INCOMPAT_FRAGMENTS) != 0 &&\n"
|
||||
"\t sbi->packed_nid > 0) {\n",
|
||||
"\tif (erofs_sb_has_fragments(sbi) && sbi->packed_nid > 0) {\n",
|
||||
"fragments helper",
|
||||
)
|
||||
old_read = extract_function(expected_super, "erofs_read_superblock")
|
||||
new_read = extract_function(current["super.c"], "erofs_read_superblock")
|
||||
expected_read = replace_once(
|
||||
old_read,
|
||||
"\tif (dsb->dirblkbits != 0)\n"
|
||||
"\t\treturn (EOPNOTSUPP);\n"
|
||||
"\tsbi->feature_compat = le32toh(dsb->feature_compat);\n",
|
||||
"\tsbi->blkszbits = dsb->blkszbits;\n"
|
||||
"\tsbi->block_size = 1u << sbi->blkszbits;\n"
|
||||
"\tsbi->feature_compat = le32toh(dsb->feature_compat);\n"
|
||||
"\terror = erofs_superblock_csum_verify(sbi, dsb);\n"
|
||||
"\tif (error != 0)\n"
|
||||
"\t\treturn (error);\n\n"
|
||||
"\tif (dsb->dirblkbits != 0)\n"
|
||||
"\t\treturn (EOPNOTSUPP);\n",
|
||||
"checksum trust order",
|
||||
)
|
||||
dead_exception = """\t/*
|
||||
\t * Narrowly allow one extra combination: long xattr prefixes enabled
|
||||
\t * with non-plain prefix table stored in a packed inode, which adds
|
||||
\t * the FRAGMENTS (0x20) incompat bit. This is NOT a declaration of
|
||||
\t * general fragments support; per-inode data layout is still gated
|
||||
\t * by plain/inline checks in erofs_read_inode().
|
||||
\t */
|
||||
\tif (unsupported != 0) {
|
||||
\t\tif (unsupported != EROFS_FEATURE_INCOMPAT_FRAGMENTS ||
|
||||
\t\t (sbi->feature_incompat &
|
||||
\t\t\tEROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) == 0 ||
|
||||
\t\t (sbi->feature_compat &
|
||||
\t\t\tEROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) != 0 ||
|
||||
\t\t sbi->packed_nid == 0)
|
||||
\t\t\treturn (EOPNOTSUPP);
|
||||
\t}
|
||||
\tsbi->blkszbits = dsb->blkszbits;
|
||||
\tsbi->block_size = 1u << sbi->blkszbits;
|
||||
"""
|
||||
expected_read = replace_once(
|
||||
expected_read,
|
||||
dead_exception,
|
||||
"\tif (unsupported != 0)\n\t\treturn (EOPNOTSUPP);\n",
|
||||
"dead fragments exception",
|
||||
)
|
||||
old_checksum_site = (
|
||||
"\terror = erofs_superblock_csum_verify(sbi, dsb);\n"
|
||||
"\tif (error != 0)\n"
|
||||
"\t\treturn (error);\n"
|
||||
)
|
||||
if expected_read.count(old_checksum_site) != 2:
|
||||
raise SystemExit("checksum trust-order transform did not produce two sites")
|
||||
old_checksum_offset = expected_read.rfind(old_checksum_site)
|
||||
expected_read = (
|
||||
expected_read[:old_checksum_offset]
|
||||
+ expected_read[old_checksum_offset + len(old_checksum_site) :]
|
||||
)
|
||||
if new_read != expected_read:
|
||||
raise SystemExit("erofs_read_superblock differs from exact B21 transforms")
|
||||
expected_super = expected_super.replace(old_read, expected_read, 1)
|
||||
if current["super.c"] != expected_super:
|
||||
raise SystemExit("super.c changed outside exact B21 transforms")
|
||||
|
||||
expected_xattr = replace_once(
|
||||
base["xattr.c"],
|
||||
"\tif ((sbi->feature_incompat & EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) ==\n"
|
||||
"\t\t0 ||\n"
|
||||
"\t sbi->xattr_prefix_count == 0)\n",
|
||||
"\tif (!erofs_sb_has_xattr_prefixes(sbi) || sbi->xattr_prefix_count == 0)\n",
|
||||
"xattr-prefix helper",
|
||||
)
|
||||
expected_xattr = replace_once(
|
||||
expected_xattr,
|
||||
"\tif ((sbi->feature_compat & EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) == 0) {\n",
|
||||
"\tif (!erofs_sb_has_plain_xattr_pfx(sbi)) {\n",
|
||||
"plain-prefix helper",
|
||||
)
|
||||
if current["xattr.c"] != expected_xattr:
|
||||
raise SystemExit("xattr.c changed outside exact B21 helper conversions")
|
||||
|
||||
read_super = new_read
|
||||
checksum_call = read_super.index("erofs_superblock_csum_verify(sbi, dsb)")
|
||||
before_checksum = read_super[:checksum_call]
|
||||
for protected in (
|
||||
"dirblkbits",
|
||||
"feature_incompat",
|
||||
"packed_nid",
|
||||
"extra_devices",
|
||||
"sb_extslots",
|
||||
"meta_blkaddr",
|
||||
"xattr_blkaddr",
|
||||
"xattr_prefix_start",
|
||||
"xattr_prefix_count",
|
||||
"blocks_root",
|
||||
):
|
||||
if protected in before_checksum:
|
||||
raise SystemExit(f"protected field used before checksum: {protected}")
|
||||
require_order(
|
||||
read_super,
|
||||
[
|
||||
"dsb->magic",
|
||||
"dsb->blkszbits < 9",
|
||||
"sbi->feature_compat = le32toh(dsb->feature_compat)",
|
||||
"erofs_superblock_csum_verify(sbi, dsb)",
|
||||
"dsb->dirblkbits",
|
||||
"sbi->feature_incompat = le32toh(dsb->feature_incompat)",
|
||||
"unsupported = sbi->feature_incompat",
|
||||
"sbi->sb_size = 128 + dsb->sb_extslots",
|
||||
"sbi->xattr_prefix_start = le32toh(dsb->xattr_prefix_start)",
|
||||
"erofs_validate_device_size",
|
||||
"erofs_load_generation_seed",
|
||||
"z_erofs_parse_cfgs",
|
||||
],
|
||||
"superblock trust order",
|
||||
)
|
||||
|
||||
mountfs = extract_function(current["super.c"], "erofs_mountfs")
|
||||
require_order(
|
||||
mountfs,
|
||||
[
|
||||
"erofs_read_superblock",
|
||||
"erofs_scan_devices",
|
||||
"erofs_init_packed_inode",
|
||||
"erofs_init_metabox_inode",
|
||||
"erofs_read_inode(sbi, sbi->root_nid",
|
||||
"erofs_xattr_prefixes_init",
|
||||
"mp->mnt_data = sbi",
|
||||
],
|
||||
"FreeBSD mount publication",
|
||||
)
|
||||
if "fail:\n\terofs_sb_free(sbi);\n\treturn (error);" not in mountfs:
|
||||
raise SystemExit("mount failure no longer funnels through erofs_sb_free")
|
||||
sb_free = extract_function(current["super.c"], "erofs_sb_free")
|
||||
require_order(
|
||||
sb_free,
|
||||
[
|
||||
"z_erofs_extent_cache_fini",
|
||||
"erofs_xattr_prefixes_cleanup",
|
||||
"erofs_drop_internal_inodes",
|
||||
"erofs_free_dev_context",
|
||||
"erofs_release_device_info(&sbi->dif0)",
|
||||
"free(sbi, M_EROFS)",
|
||||
],
|
||||
"mount cleanup",
|
||||
)
|
||||
release = extract_function(current["super.c"], "erofs_release_device_info")
|
||||
require_order(
|
||||
release,
|
||||
["g_topology_lock", "g_vfs_close", "g_topology_unlock", "vrele", "dev_rel"],
|
||||
"GEOM release",
|
||||
)
|
||||
if any("return (-E" in current[name] for name in current):
|
||||
raise SystemExit("negative errno entered the FreeBSD B21 write set")
|
||||
|
||||
result = {
|
||||
"baseline": baseline,
|
||||
"checksum_before_protected_fields": True,
|
||||
"dead_fragments_exception_removed": True,
|
||||
"feature_helpers": True,
|
||||
"geom_release_preserved": True,
|
||||
"internal_h_unchanged": True,
|
||||
"mount_cleanup_preserved": True,
|
||||
"mount_publication_preserved": True,
|
||||
"positive_errno_preserved": True,
|
||||
"status": "PASS",
|
||||
}
|
||||
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print("B21 source: exact trust-order and feature-helper transforms present")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B21 source contract failed'
|
||||
fi
|
||||
fi
|
||||
|
||||
if python3 - "$artifacts/B21-fixture-index.json" \
|
||||
"$artifacts/oracle-first.json" "$artifacts/B21-matrix.tsv" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
index = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
oracle = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
|
||||
if index["case_count"] != 13 or index["legal_count"] != 1 or index["damaged_count"] != 12:
|
||||
raise SystemExit("B21 fixture cardinality changed")
|
||||
if oracle["status"] != "PASS" or oracle["passed_count"] != 13:
|
||||
raise SystemExit("B21 oracle matrix is incomplete")
|
||||
lines = ["id\tauthenticated\terrno\treject\tsha256"]
|
||||
for fixture, result in zip(index["cases"], oracle["results"], strict=True):
|
||||
lines.append(
|
||||
f"{fixture['id']}\t{str(fixture['authenticated']).lower()}\t"
|
||||
f"{result['actual_errno']}\t{result['actual_reject']}\t{fixture['sha256']}"
|
||||
)
|
||||
Path(sys.argv[3]).write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
print(
|
||||
f"B21 fixtures: {index['case_count']} "
|
||||
f"({index['legal_count']} legal, {index['damaged_count']} damaged)"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'B21 fixture matrix summary failed'
|
||||
fi
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'B21 super gate: host PASS' \
|
||||
'TC172 QEMU runtime NOT_RUN in host mode'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in awk clang file nm scp tar timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B21 QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B21-fixtures.tar.gz
|
||||
module=$PRE15_CASE_TMP/B21-erofs-zstdio0.ko
|
||||
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
||||
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
|
||||
>"$artifacts/B21-kld-build.stdout" 2>"$artifacts/B21-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B21 cross-target zstdio0 KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/B21-kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B21-kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/B21-kld-nm-u.txt"
|
||||
tar -C "$first" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B21-fixtures.tar.gz || \
|
||||
! pre15_scp "$qemu_probe" /root/B21-qemu-probe.c || \
|
||||
! pre15_scp "$module" /root/B21-erofs-zstdio0.ko; then
|
||||
pre15_infra_blocked 'could not transfer B21 module, probe, or fixtures'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/B21-fixtures && mkdir /root/B21-fixtures && tar -xzf /root/B21-fixtures.tar.gz -C /root/B21-fixtures && cc -O2 -Wall -Wextra -Werror -std=c17 -o /root/B21-qemu-probe /root/B21-qemu-probe.c'; then
|
||||
pre15_infra_blocked 'could not prepare B21 guest fixtures/probe'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B21-erofs-zstdio0.ko \
|
||||
>"$artifacts/B21-kldload.stdout" 2>"$artifacts/B21-kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' \
|
||||
"$artifacts/B21-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_dut_fail 'B21 exact-source zstdio0 KLD failed to load'
|
||||
fi
|
||||
pre15_own_guest_kld erofs 'B21 exact-source KLD'
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/B21-dmesg-before.txt"
|
||||
pre15_guest_ssh_bounded mkdir -p /mnt/pre15-b21
|
||||
|
||||
b21_unown()
|
||||
{
|
||||
kind=$1
|
||||
value=$2
|
||||
tmp=$PRE15_CASE_TMP/ownership.$$
|
||||
awk -F '\t' -v kind="$kind" -v value="$value" \
|
||||
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
|
||||
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
|
||||
}
|
||||
|
||||
printf 'id\texpected_errno\tactual_errno\treject\n' \
|
||||
>"$artifacts/B21-qemu-matrix.tsv"
|
||||
while IFS="$(printf '\t')" read -r id authenticated expected_errno reject fixture_hash; do
|
||||
test "$id" != id || continue
|
||||
b21_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "/root/B21-fixtures/$id.erofs") || \
|
||||
pre15_dut_fail "$id md attach failed"
|
||||
case "$b21_md" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected B21 md unit: $b21_md" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$b21_md" "$id md"
|
||||
result=$(pre15_guest_ssh_bounded /root/B21-qemu-probe \
|
||||
"/dev/$b21_md" /mnt/pre15-b21) || \
|
||||
pre15_infra_blocked "$id guest mount probe failed"
|
||||
actual_errno=$(printf '%s\n' "$result" | sed -n 's/.* errno=\([0-9][0-9]*\).*/\1/p')
|
||||
test -n "$actual_errno" || pre15_runner_fail "$id mount probe did not report errno"
|
||||
if test "$actual_errno" = 0; then
|
||||
pre15_own_guest_mount /mnt/pre15-b21 "$id mount"
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\n' "$id" "$expected_errno" \
|
||||
"$actual_errno" "$reject" >>"$artifacts/B21-qemu-matrix.tsv"
|
||||
if test "$actual_errno" != "$expected_errno"; then
|
||||
pre15_dut_fail "$id expected errno $expected_errno, received $actual_errno"
|
||||
fi
|
||||
if test "$actual_errno" = 0; then
|
||||
pre15_guest_ssh_bounded find /mnt/pre15-b21 -mindepth 1 -maxdepth 1 \
|
||||
-print >"$artifacts/B21-legal-readdir.txt"
|
||||
pre15_guest_ssh_bounded umount /mnt/pre15-b21 || \
|
||||
pre15_dut_fail "$id unmount failed"
|
||||
b21_unown guest-mount /mnt/pre15-b21
|
||||
fi
|
||||
pre15_guest_ssh_bounded mdconfig -d -u "$b21_md" || \
|
||||
pre15_dut_fail "$id md detach failed"
|
||||
b21_unown guest-md "$b21_md"
|
||||
done <"$artifacts/B21-matrix.tsv"
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/B21-dmesg-after.txt"
|
||||
diff -u "$artifacts/B21-dmesg-before.txt" "$artifacts/B21-dmesg-after.txt" \
|
||||
>"$artifacts/B21-dmesg.diff" || true
|
||||
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/B21-dmesg.diff" \
|
||||
>"$artifacts/B21-dmesg-added.txt"
|
||||
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free' \
|
||||
"$artifacts/B21-dmesg-added.txt"; then
|
||||
pre15_dut_fail 'TC172 produced panic, WITNESS, or UAF evidence'
|
||||
fi
|
||||
printf '%s\n' \
|
||||
'TC172-super-trust B21 QEMU PASS' \
|
||||
'13/13 legal/damaged images returned exact mount errno with checksum trust order preserved'
|
||||
Executable
+570
@@ -0,0 +1,570 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=8a8761af91654a025fd991d14da906c0b612bd7d
|
||||
fixture=$PRE15_DUT/tests/pre15/fixtures/B22-zmap-arithmetic.json
|
||||
kld_builder=$PRE15_DUT/tests/pre15/fixtures/B22-build-kld.sh
|
||||
zmap=$PRE15_DUT/src/zmap.c
|
||||
linux_zmap=$PRE15_ROOT/src-linux/zmap.c
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in cc git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B22 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b22-arithmetic "$fixture"
|
||||
pre15_record_fixture b22-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b22-case "$PRE15_DUT/tests/pre15/cases/B22-zmap-arithmetic.sh"
|
||||
pre15_record_fixture b22-zmap "$zmap"
|
||||
pre15_record_fixture b22-linux-zmap "$linux_zmap"
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$fixture" \
|
||||
"$artifacts" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
fixture_path = Path(sys.argv[4])
|
||||
artifacts = Path(sys.argv[5])
|
||||
src = dut / "src"
|
||||
u32_max = (1 << 32) - 1
|
||||
u64_max = (1 << 64) - 1
|
||||
eintegrity = 97
|
||||
|
||||
|
||||
def committed(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B22 baseline {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if match is None:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, match.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", match.end())
|
||||
if brace < 0:
|
||||
raise SystemExit(f"missing function body: {name}")
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def add(left: int, right: int) -> tuple[str, int | None]:
|
||||
value = left + right
|
||||
if value > u64_max:
|
||||
return ("EINTEGRITY", None)
|
||||
return ("PASS", value)
|
||||
|
||||
|
||||
def multiply(left: int, right: int) -> tuple[str, int | None]:
|
||||
value = left * right
|
||||
if value > u64_max:
|
||||
return ("EINTEGRITY", None)
|
||||
return ("PASS", value)
|
||||
|
||||
|
||||
def model(case: dict[str, object]) -> tuple[str, int | None]:
|
||||
operation = case["operation"]
|
||||
if operation == "compact_pblk":
|
||||
return add(int(case["base"]), int(case["nblk"]))
|
||||
if operation == "index_base":
|
||||
status, end = add(int(case["inode_off"]), int(case["inode_isize"]))
|
||||
if status != "PASS":
|
||||
return (status, None)
|
||||
status, end = add(int(end), int(case.get("xattr_isize", 0)))
|
||||
if status != "PASS":
|
||||
return (status, None)
|
||||
status, end = add(int(end), 7)
|
||||
if status != "PASS":
|
||||
return (status, None)
|
||||
end = int(end) & ~7
|
||||
return add(end, 8)
|
||||
if operation == "index_advance":
|
||||
status, delta = multiply(int(case["count"]), int(case["unit"]))
|
||||
if status != "PASS":
|
||||
return (status, None)
|
||||
return add(int(case["position"]), int(delta))
|
||||
if operation == "lcluster_count":
|
||||
bits = int(case["lclusterbits"])
|
||||
if bits >= 64:
|
||||
return ("EINTEGRITY", None)
|
||||
size = int(case["size"])
|
||||
count = size >> bits
|
||||
if size & ((1 << bits) - 1):
|
||||
return add(count, 1)
|
||||
return ("PASS", count)
|
||||
if operation == "lcluster_pos":
|
||||
bits = int(case["lclusterbits"])
|
||||
if bits >= 64:
|
||||
return ("EINTEGRITY", None)
|
||||
status, base = multiply(int(case["lcn"]), 1 << bits)
|
||||
if status != "PASS":
|
||||
return (status, None)
|
||||
return add(int(base), int(case["clusterofs"]))
|
||||
if operation == "lcn_advance":
|
||||
return add(int(case["lcn"]), int(case["delta"]))
|
||||
if operation == "physical_end":
|
||||
status, pend = add(int(case["pa"]), int(case["plen"]))
|
||||
if status != "PASS":
|
||||
return (status, None)
|
||||
limit = (1 << 48) * int(case["block_size"])
|
||||
if limit <= u64_max and int(pend) > limit:
|
||||
return ("EINTEGRITY", None)
|
||||
return ("PASS", None)
|
||||
if operation == "post_eof":
|
||||
la = int(case["la"])
|
||||
size = int(case["size"])
|
||||
if la < size:
|
||||
return ("EINTEGRITY", None)
|
||||
length = la - size + 1
|
||||
return ("PASS", min(length, u64_max))
|
||||
if operation == "fragment_offset":
|
||||
low = int(case["low"])
|
||||
high = int(case["high"])
|
||||
if low > u32_max or high > u32_max:
|
||||
return ("EINTEGRITY", None)
|
||||
return ("PASS", low | (high << 32))
|
||||
raise SystemExit(f"unknown B22 operation: {operation}")
|
||||
|
||||
|
||||
fixture = json.loads(fixture_path.read_text(encoding="ascii"))
|
||||
if fixture.get("schema") != 1 or fixture.get("batch") != "B22":
|
||||
raise SystemExit("invalid B22 fixture identity")
|
||||
if fixture.get("test") != "TC170-zmap-arithmetic":
|
||||
raise SystemExit("invalid B22 test identity")
|
||||
if fixture.get("candidates") != [
|
||||
"P15-047",
|
||||
"P15-059",
|
||||
"P15-071",
|
||||
"P15-079",
|
||||
"P15-084",
|
||||
]:
|
||||
raise SystemExit("B22 candidate set changed")
|
||||
if fixture.get("errno") != {
|
||||
"corruption": eintegrity,
|
||||
"provider_io": "unchanged",
|
||||
"sign": "positive",
|
||||
}:
|
||||
raise SystemExit("B22 errno contract changed")
|
||||
|
||||
cases = fixture.get("cases")
|
||||
if not isinstance(cases, list) or not cases:
|
||||
raise SystemExit("B22 fixture has no cases")
|
||||
names: set[str] = set()
|
||||
markers: set[str] = set()
|
||||
operations: set[str] = set()
|
||||
decoded: list[dict[str, object]] = []
|
||||
for case in cases:
|
||||
if not isinstance(case, dict):
|
||||
raise SystemExit("B22 fixture case is not an object")
|
||||
name = str(case.get("name", ""))
|
||||
marker = str(case.get("target_marker", ""))
|
||||
if not name or name in names or not marker.startswith("TC170:"):
|
||||
raise SystemExit(f"invalid B22 case identity: {name!r}")
|
||||
names.add(name)
|
||||
markers.add(marker)
|
||||
operations.add(str(case.get("operation", "")))
|
||||
status, value = model(case)
|
||||
if status != case.get("status"):
|
||||
raise SystemExit(f"B22 independent status mismatch: {name}")
|
||||
if status == "PASS" and value is not None and value != case.get("expected"):
|
||||
raise SystemExit(f"B22 independent value mismatch: {name}")
|
||||
mutated = case.get("mutated_fields")
|
||||
if status == "EINTEGRITY" and (
|
||||
not isinstance(mutated, list) or len(mutated) != 1
|
||||
):
|
||||
raise SystemExit(f"B22 negative is not single-field: {name}")
|
||||
if status == "PASS" and mutated != []:
|
||||
raise SystemExit(f"B22 positive declares a mutation: {name}")
|
||||
decoded.append({"name": name, "status": status, "value": value})
|
||||
|
||||
required_markers = {
|
||||
"TC170:compact-pblk-32bit-crossing",
|
||||
"TC170:index-position",
|
||||
"TC170:delta-lcn",
|
||||
"TC170:physical-end-48bit",
|
||||
"TC170:post-eof",
|
||||
"TC170:fragment-high-bits",
|
||||
}
|
||||
if markers != required_markers:
|
||||
raise SystemExit("B22 target-marker set is incomplete")
|
||||
if operations != {
|
||||
"compact_pblk",
|
||||
"fragment_offset",
|
||||
"index_advance",
|
||||
"index_base",
|
||||
"lcluster_count",
|
||||
"lcluster_pos",
|
||||
"lcn_advance",
|
||||
"physical_end",
|
||||
"post_eof",
|
||||
}:
|
||||
raise SystemExit("B22 operation set is incomplete")
|
||||
|
||||
current = (src / "zmap.c").read_text(encoding="utf-8")
|
||||
base = committed("src/zmap.c")
|
||||
linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8")
|
||||
helpers = [
|
||||
"z_erofs_index_base",
|
||||
"z_erofs_index_advance",
|
||||
"z_erofs_lcluster_count",
|
||||
"z_erofs_lcluster_pos",
|
||||
"z_erofs_lcn_advance",
|
||||
"z_erofs_compact_pblk",
|
||||
"z_erofs_fragment_offset",
|
||||
"z_erofs_post_eof_len",
|
||||
"z_erofs_physical_end",
|
||||
]
|
||||
for helper in helpers:
|
||||
if helper in base or current.count(helper) < 2:
|
||||
raise SystemExit(f"B22 helper is missing, duplicated, or pre-existing: {helper}")
|
||||
|
||||
old_freebsd = (
|
||||
"m->pblk = le32dec(in + packsize - sizeof(uint32_t)) + nblk;",
|
||||
"map->m_llen = map->m_la + 1 - vi->size;",
|
||||
"vi->z_fragmentoff |= map->m_pa << 32;",
|
||||
"lcn += m->delta[1];",
|
||||
"pos += lcn << amortizedshift;",
|
||||
"(pend >> sbi->blkszbits) >= (1ULL << 48)",
|
||||
)
|
||||
if not all(anchor in base for anchor in old_freebsd):
|
||||
raise SystemExit("B22 baseline arithmetic anchors changed")
|
||||
if any(anchor in current for anchor in old_freebsd):
|
||||
raise SystemExit("B22 left an unchecked baseline arithmetic anchor")
|
||||
linux_anchors = (
|
||||
"m->pblk = le32_to_cpu(*(__le32 *)in) + nblk;",
|
||||
"map->m_llen = map->m_la + 1 - inode->i_size;",
|
||||
"vi->z_fragmentoff |= map->m_pa << 32;",
|
||||
"lcn += m->delta[1];",
|
||||
"(pend >> sbi->blkszbits) >= BIT_ULL(48)",
|
||||
)
|
||||
if not all(anchor in linux for anchor in linux_anchors):
|
||||
raise SystemExit("Linux zmap semantic anchor changed")
|
||||
if function(current, "z_erofs_read_index") != function(base, "z_erofs_read_index"):
|
||||
raise SystemExit("B22 changed the FreeBSD metadata/provider reader")
|
||||
if current.count("erofs_read_metadata") != base.count("erofs_read_metadata"):
|
||||
raise SystemExit("B22 changed metadata I/O call count")
|
||||
if current.count("erofs_put_metabuf") != base.count("erofs_put_metabuf"):
|
||||
raise SystemExit("B22 changed metadata release call count")
|
||||
for public in ("z_erofs_fill_inode", "z_erofs_map_blocks"):
|
||||
old_signature = function(base, public).split("{", 1)[0]
|
||||
new_signature = function(current, public).split("{", 1)[0]
|
||||
if old_signature != new_signature:
|
||||
raise SystemExit(f"B22 changed public zmap ABI: {public}")
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", current):
|
||||
raise SystemExit("B22 introduced Linux negative errno")
|
||||
|
||||
legacy_mismatches: set[str] = set()
|
||||
for case in cases:
|
||||
operation = case["operation"]
|
||||
status = case["status"]
|
||||
expected = case.get("expected")
|
||||
if operation == "compact_pblk" and status == "PASS":
|
||||
old = (int(case["base"]) + int(case["nblk"])) & u32_max
|
||||
if old != expected:
|
||||
legacy_mismatches.add("compact-pblk")
|
||||
elif operation == "index_advance" and status == "EINTEGRITY":
|
||||
old = (int(case["position"]) + int(case["count"]) * int(case["unit"])) & u64_max
|
||||
if old <= u64_max:
|
||||
legacy_mismatches.add("index-position")
|
||||
elif operation == "lcluster_pos" and status == "EINTEGRITY":
|
||||
old = ((int(case["lcn"]) << int(case["lclusterbits"])) + int(case["clusterofs"])) & u64_max
|
||||
if old <= u64_max:
|
||||
legacy_mismatches.add("delta-lcn")
|
||||
elif operation == "physical_end" and case["name"] == "physical-last-48bit-block":
|
||||
pend = int(case["pa"]) + int(case["plen"])
|
||||
if (pend >> 12) >= (1 << 48):
|
||||
legacy_mismatches.add("physical-end")
|
||||
elif operation == "post_eof" and case["name"] == "post-eof-saturates-empty-inode":
|
||||
old = ((int(case["la"]) + 1) & u64_max) - int(case["size"])
|
||||
if old != expected:
|
||||
legacy_mismatches.add("post-eof")
|
||||
elif operation == "fragment_offset" and status == "EINTEGRITY":
|
||||
old = int(case["low"]) | ((int(case["high"]) << 32) & u64_max)
|
||||
if old <= u64_max:
|
||||
legacy_mismatches.add("fragment-high")
|
||||
if legacy_mismatches != {
|
||||
"compact-pblk",
|
||||
"delta-lcn",
|
||||
"fragment-high",
|
||||
"index-position",
|
||||
"physical-end",
|
||||
"post-eof",
|
||||
}:
|
||||
raise SystemExit("B22 fixtures do not distinguish every legacy bug")
|
||||
|
||||
helper_source = "\n\n".join(function(current, name) for name in helpers)
|
||||
c_lines = [
|
||||
"#include <stdint.h>",
|
||||
"#include <stdio.h>",
|
||||
"#define EINTEGRITY 97",
|
||||
"#define rounddown2(x, y) ((x) & ~((y) - 1))",
|
||||
"typedef uint64_t erofs_off_t;",
|
||||
"typedef uint64_t erofs_blk_t;",
|
||||
"struct z_erofs_map_header { uint8_t bytes[8]; };",
|
||||
"struct erofs_inode { erofs_off_t inode_off; unsigned int inode_isize; unsigned int xattr_isize; };",
|
||||
helper_source,
|
||||
"int main(void)",
|
||||
"{",
|
||||
"\tuint64_t value;",
|
||||
"\tint error, failures = 0;",
|
||||
]
|
||||
|
||||
|
||||
def u64(value: object) -> str:
|
||||
return f"UINT64_C({int(value)})"
|
||||
|
||||
|
||||
for case in cases:
|
||||
name = str(case["name"])
|
||||
operation = case["operation"]
|
||||
expected_error = 0 if case["status"] == "PASS" else eintegrity
|
||||
c_lines.append(f"\t/* {name} */")
|
||||
if operation == "compact_pblk":
|
||||
call = f"z_erofs_compact_pblk(UINT32_C({int(case['base'])}), {int(case['nblk'])}U, &value)"
|
||||
elif operation == "index_base":
|
||||
c_lines.extend([
|
||||
"\t{",
|
||||
"\t\tstruct erofs_inode vi = {",
|
||||
f"\t\t\t.inode_off = {u64(case['inode_off'])},",
|
||||
f"\t\t\t.inode_isize = {int(case['inode_isize'])}U,",
|
||||
f"\t\t\t.xattr_isize = {int(case.get('xattr_isize', 0))}U,",
|
||||
"\t\t};",
|
||||
])
|
||||
call = "z_erofs_index_base(&vi, &value)"
|
||||
elif operation == "index_advance":
|
||||
c_lines.append(f"\tvalue = {u64(case['position'])};")
|
||||
call = f"z_erofs_index_advance(&value, {u64(case['count'])}, {u64(case['unit'])})"
|
||||
elif operation == "lcluster_count":
|
||||
call = f"z_erofs_lcluster_count({u64(case['size'])}, {int(case['lclusterbits'])}U, &value)"
|
||||
elif operation == "lcluster_pos":
|
||||
call = f"z_erofs_lcluster_pos({u64(case['lcn'])}, {int(case['lclusterbits'])}U, {u64(case['clusterofs'])}, &value)"
|
||||
elif operation == "lcn_advance":
|
||||
c_lines.append(f"\tvalue = {u64(case['lcn'])};")
|
||||
call = f"z_erofs_lcn_advance(&value, {u64(case['delta'])})"
|
||||
elif operation == "physical_end":
|
||||
call = f"z_erofs_physical_end({u64(case['pa'])}, {u64(case['plen'])}, {u64(case['block_size'])})"
|
||||
elif operation == "post_eof":
|
||||
call = f"z_erofs_post_eof_len({u64(case['la'])}, {u64(case['size'])}, &value)"
|
||||
elif operation == "fragment_offset":
|
||||
call = f"z_erofs_fragment_offset({u64(case['low'])}, {u64(case['high'])}, &value)"
|
||||
else:
|
||||
raise SystemExit(f"cannot generate C for operation: {operation}")
|
||||
c_lines.append(f"\terror = {call};")
|
||||
c_lines.append(f"\tif (error != {expected_error}) {{")
|
||||
c_lines.append(f"\t\tfprintf(stderr, \"{name}: error=%d\\n\", error);")
|
||||
c_lines.append("\t\tfailures++;\n\t}")
|
||||
if case["status"] == "PASS" and "expected" in case:
|
||||
c_lines.append(f"\tif (value != {u64(case['expected'])}) {{")
|
||||
c_lines.append(f"\t\tfprintf(stderr, \"{name}: value mismatch\\n\");")
|
||||
c_lines.append("\t\tfailures++;\n\t}")
|
||||
if operation == "index_base":
|
||||
c_lines.append("\t}")
|
||||
c_lines.extend([
|
||||
"\tif (failures != 0)",
|
||||
"\t\treturn (1);",
|
||||
f"\tprintf(\"B22 extracted arithmetic PASS cases={len(cases)}\\n\");",
|
||||
"\treturn (0);",
|
||||
"}",
|
||||
])
|
||||
c_path = artifacts / "B22-zmap-arithmetic.c"
|
||||
binary = artifacts / "B22-zmap-arithmetic"
|
||||
c_path.write_text("\n".join(c_lines) + "\n", encoding="ascii")
|
||||
compiled = subprocess.run(
|
||||
["cc", "-std=gnu11", "-Wall", "-Wextra", "-Werror", str(c_path), "-o", str(binary)],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(artifacts / "B22-compile.stdout").write_text(compiled.stdout, encoding="utf-8")
|
||||
(artifacts / "B22-compile.stderr").write_text(compiled.stderr, encoding="utf-8")
|
||||
if compiled.returncode != 0:
|
||||
raise SystemExit("B22 extracted arithmetic compilation failed")
|
||||
executed = subprocess.run(
|
||||
[str(binary)],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(artifacts / "B22-extracted.stdout").write_text(executed.stdout, encoding="utf-8")
|
||||
(artifacts / "B22-extracted.stderr").write_text(executed.stderr, encoding="utf-8")
|
||||
if executed.returncode != 0:
|
||||
raise SystemExit("B22 extracted arithmetic execution failed")
|
||||
|
||||
result = {
|
||||
"status": "PASS",
|
||||
"batch": "B22",
|
||||
"test": "TC170-zmap-arithmetic",
|
||||
"baseline": baseline,
|
||||
"case_count": len(cases),
|
||||
"negative_count": sum(case["status"] == "EINTEGRITY" for case in cases),
|
||||
"target_markers": sorted(markers),
|
||||
"legacy_mismatches": sorted(legacy_mismatches),
|
||||
"freebsd_positive_errno": True,
|
||||
"linux_algorithm_locations_audited": True,
|
||||
"provider_io_calls_unchanged": True,
|
||||
"metadata_release_calls_unchanged": True,
|
||||
"decompressor_abi_unchanged": True,
|
||||
"ondisk_abi_unchanged": True,
|
||||
"qemu_scope": "exact-source KLD load plus extracted helper replay when PRE15_MODE=qemu",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
}
|
||||
(artifacts / "B22-decoded-cases.json").write_text(
|
||||
json.dumps(decoded, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
(artifacts / "B22-result.json").write_text(
|
||||
json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
print(
|
||||
f"B22 host PASS cases={len(cases)} negatives={result['negative_count']} "
|
||||
f"markers={len(markers)}"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B22 source, fixture, or extracted arithmetic check failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/B22-zmap-arithmetic.c" \
|
||||
"$artifacts/B22-zmap-arithmetic" \
|
||||
"$artifacts/B22-decoded-cases.json" "$artifacts/B22-result.json" \
|
||||
> "$artifacts/SHA256SUMS"
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'TC170 host extracted zmap arithmetic and fixture oracle PASS' \
|
||||
'QEMU exact-source KLD load and FreeBSD helper replay NOT_RUN in host mode'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in awk clang file nm scp; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B22 QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
module=$PRE15_CASE_TMP/B22-erofs.ko
|
||||
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B22-kld-build.stdout" \
|
||||
2>"$artifacts/B22-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B22 cross-target KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" > "$artifacts/B22-kld-file.txt"
|
||||
sha256sum "$module" > "$artifacts/B22-kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort > "$artifacts/B22-kld-nm-u.txt"
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$module" /root/B22-erofs.ko || \
|
||||
! pre15_scp "$artifacts/B22-zmap-arithmetic.c" \
|
||||
/root/B22-zmap-arithmetic.c; then
|
||||
pre15_infra_blocked 'could not transfer B22 module or arithmetic source'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B22-erofs.ko \
|
||||
>"$artifacts/B22-kldload.stdout" 2>"$artifacts/B22-kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' \
|
||||
"$artifacts/B22-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_dut_fail 'B22 exact-source KLD failed to load'
|
||||
fi
|
||||
pre15_own_guest_kld erofs 'B22 exact-source KLD'
|
||||
if ! pre15_guest_ssh_bounded cc -std=gnu11 -Wall -Wextra -Werror \
|
||||
/root/B22-zmap-arithmetic.c -o /root/B22-zmap-arithmetic; then
|
||||
pre15_infra_blocked 'could not compile B22 arithmetic helper in the guest'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded /root/B22-zmap-arithmetic \
|
||||
>"$artifacts/B22-guest-arithmetic.stdout" \
|
||||
2>"$artifacts/B22-guest-arithmetic.stderr"; then
|
||||
pre15_dut_fail 'B22 FreeBSD arithmetic helper replay failed'
|
||||
fi
|
||||
pre15_guest_ssh_bounded kldstat -n erofs > "$artifacts/B22-kldstat.txt"
|
||||
printf '%s\n' \
|
||||
'TC170 QEMU PASS exact-source KLD load and FreeBSD arithmetic helper replay' \
|
||||
'No full feature suite or shared GEOM provider was used'
|
||||
@@ -0,0 +1,412 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=083acc65f842c409c5a2e089e50060d0062b677c
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B23-explicit-spec.json
|
||||
generator=$fixture_dir/B23-explicit-generate.py
|
||||
oracle=$fixture_dir/B23-explicit-oracle.py
|
||||
probe=$fixture_dir/B23-explicit-probe.c
|
||||
kld_builder=$fixture_dir/B23-build-kld.sh
|
||||
zmap=$PRE15_DUT/src/zmap.c
|
||||
linux_zmap=$PRE15_ROOT/src-linux/zmap.c
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
first=$PRE15_CASE_TMP/first
|
||||
second=$PRE15_CASE_TMP/second
|
||||
|
||||
for tool in cc diff git mkfs.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B23 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b23-spec "$spec"
|
||||
pre15_record_fixture b23-generator "$generator"
|
||||
pre15_record_fixture b23-oracle "$oracle"
|
||||
pre15_record_fixture b23-probe "$probe"
|
||||
pre15_record_fixture b23-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b23-case "$PRE15_DUT/tests/pre15/cases/B23-explicit-table.sh"
|
||||
pre15_record_fixture b23-zmap "$zmap"
|
||||
pre15_record_fixture b23-linux-zmap "$linux_zmap"
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if ! python3 -B "$generator" --spec "$spec" --output "$first" \
|
||||
>"$artifacts/B23-generate-first.stdout" \
|
||||
2>"$artifacts/B23-generate-first.stderr"; then
|
||||
pre15_runner_fail 'B23 first fixture generation failed'
|
||||
fi
|
||||
if ! python3 -B "$generator" --spec "$spec" --output "$second" \
|
||||
>"$artifacts/B23-generate-second.stdout" \
|
||||
2>"$artifacts/B23-generate-second.stderr"; then
|
||||
pre15_runner_fail 'B23 repeat fixture generation failed'
|
||||
fi
|
||||
if ! diff -u "$first/SHA256SUMS" "$second/SHA256SUMS" \
|
||||
>"$artifacts/B23-repeat-sums.diff" || \
|
||||
! diff -u "$first/fixture-manifest.json" "$second/fixture-manifest.json" \
|
||||
>"$artifacts/B23-repeat-manifest.diff"; then
|
||||
pre15_runner_fail 'B23 fixture generation is not byte deterministic'
|
||||
fi
|
||||
if ! python3 -B "$oracle" --spec "$spec" --fixtures "$first" \
|
||||
--report "$artifacts/B23-oracle-first.json" \
|
||||
>"$artifacts/B23-oracle-first.stdout" \
|
||||
2>"$artifacts/B23-oracle-first.stderr"; then
|
||||
pre15_runner_fail 'B23 independent fixture oracle failed'
|
||||
fi
|
||||
if ! python3 -B "$oracle" --spec "$spec" --fixtures "$second" \
|
||||
--report "$artifacts/B23-oracle-second.json" \
|
||||
>"$artifacts/B23-oracle-second.stdout" \
|
||||
2>"$artifacts/B23-oracle-second.stderr"; then
|
||||
pre15_runner_fail 'B23 repeat fixture oracle failed'
|
||||
fi
|
||||
cp "$first/SHA256SUMS" "$artifacts/B23-fixture-SHA256SUMS"
|
||||
cp "$first/fixture-manifest.json" "$artifacts/B23-fixture-manifest.json"
|
||||
pre15_record_fixture b23-generated-sums "$artifacts/B23-fixture-SHA256SUMS"
|
||||
|
||||
if ! cc -std=c11 -Wall -Wextra -Werror -c "$probe" \
|
||||
-o "$PRE15_CASE_TMP/B23-explicit-probe.o" \
|
||||
>"$artifacts/B23-probe-compile.stdout" \
|
||||
2>"$artifacts/B23-probe-compile.stderr"; then
|
||||
pre15_runner_fail 'B23 guest probe does not compile on the host'
|
||||
fi
|
||||
|
||||
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$spec" \
|
||||
"$artifacts/B23-source-audit.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
spec_path = Path(sys.argv[4])
|
||||
report_path = Path(sys.argv[5])
|
||||
|
||||
|
||||
def committed(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B23 baseline {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if match is None:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
start = source.rfind("\n", 0, source.rfind("\n", 0, match.start())) + 1
|
||||
brace = source.find("{", match.end())
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
spec = json.loads(spec_path.read_text(encoding="ascii"))
|
||||
current = (dut / "src/zmap.c").read_text(encoding="utf-8")
|
||||
before = committed("src/zmap.c")
|
||||
validator = function(current, "z_erofs_validate_extent_table")
|
||||
old_validator = function(before, "z_erofs_validate_extent_table")
|
||||
if validator == old_validator:
|
||||
raise SystemExit("B23 validator did not change")
|
||||
if function(current, "z_erofs_read_extent") != function(before, "z_erofs_read_extent"):
|
||||
raise SystemExit("B23 changed the local record decoder")
|
||||
for name in ("z_erofs_map_blocks_ext", "z_erofs_fill_inode"):
|
||||
if function(current, name) != function(before, name):
|
||||
raise SystemExit(f"B23 changed {name}")
|
||||
if "#define Z_EROFS_EXTENT_VALIDATE_CHUNK_SIZE (64 * 1024)" not in current:
|
||||
raise SystemExit("B23 fixed validation chunk bound is missing")
|
||||
required = (
|
||||
"z_erofs_read_extent(sbi, vi, record_pos, recsz, &ext)",
|
||||
"while (scan_pos < table_end)",
|
||||
"Z_EROFS_EXTENT_VALIDATE_CHUNK_SIZE",
|
||||
"erofs_read_metadata(sbi, vi->nid, scan_pos, chunk_len, &buf)",
|
||||
"memcpy(&ext, (const char *)buf.data + offset, recsz)",
|
||||
"z_erofs_extent_lstart(&ext, recsz)",
|
||||
"erofs_put_metabuf(&buf)",
|
||||
"return (index == vi->z_extents ? 0 : EINTEGRITY)",
|
||||
)
|
||||
if not all(marker in validator for marker in required):
|
||||
raise SystemExit("B23 validator is missing a bounded-scan contract marker")
|
||||
if validator.count("z_erofs_read_extent(") != 1:
|
||||
raise SystemExit("B23 short tail probe count changed")
|
||||
if validator.count("erofs_read_metadata(") != 1:
|
||||
raise SystemExit("B23 long-table reader is not chunk-scoped")
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", validator):
|
||||
raise SystemExit("B23 introduced Linux negative errno")
|
||||
|
||||
allowed = {
|
||||
"repo-pre-15/src/zmap.c",
|
||||
"repo-pre-15/tests/pre15/cases/B23-explicit-table.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B23-build-kld.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B23-explicit-generate.py",
|
||||
"repo-pre-15/tests/pre15/fixtures/B23-explicit-oracle.py",
|
||||
"repo-pre-15/tests/pre15/fixtures/B23-explicit-probe.c",
|
||||
"repo-pre-15/tests/pre15/fixtures/B23-explicit-spec.json",
|
||||
}
|
||||
changed = set(
|
||||
subprocess.run(
|
||||
["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
changed.update(
|
||||
subprocess.run(
|
||||
[
|
||||
"git", "-C", str(root), "ls-files", "--others",
|
||||
"--exclude-standard", "--", "repo-pre-15",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
if changed != allowed:
|
||||
raise SystemExit(f"B23 write set differs: {sorted(changed ^ allowed)}")
|
||||
for path in (
|
||||
"src/erofs_fs.h",
|
||||
"src/data.c",
|
||||
"src/decompressor.c",
|
||||
"src/decompressor_lz4.c",
|
||||
"src/decompressor_lzma.c",
|
||||
"src/decompressor_deflate.c",
|
||||
"src/decompressor_zstd.c",
|
||||
"src/internal.h",
|
||||
):
|
||||
if (dut / path).read_text(encoding="utf-8") != committed(path):
|
||||
raise SystemExit(f"B23 changed an excluded provider/codec/ABI file: {path}")
|
||||
|
||||
linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8")
|
||||
for marker in (
|
||||
"recsz <= offsetof(struct z_erofs_extent, pstart_hi)",
|
||||
"le64_to_cpu(*(__le64 *)ext)",
|
||||
"le32_to_cpu(ext->pstart_lo)",
|
||||
"le32_to_cpu(ext->pstart_hi) << 32",
|
||||
"le32_to_cpu(ext->lstart_hi) << 32",
|
||||
"erofs_inode_in_metabox(inode)",
|
||||
):
|
||||
if marker not in linux:
|
||||
raise SystemExit(f"Linux explicit-table anchor changed: {marker}")
|
||||
|
||||
report = {
|
||||
"baseline": baseline,
|
||||
"batch": "B23",
|
||||
"candidates": spec["candidates"],
|
||||
"chunk_size": spec["chunk_size"],
|
||||
"decompressor_files_unchanged": True,
|
||||
"freebsd_positive_errno": True,
|
||||
"linux_record_decode_audited": True,
|
||||
"ondisk_header_unchanged": True,
|
||||
"primary_metabox_reader_unchanged": True,
|
||||
"status": "PASS",
|
||||
"test": spec["test"],
|
||||
"write_set": sorted(changed),
|
||||
}
|
||||
report_path.write_text(
|
||||
json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
print("B23 source/write-set/Linux-FreeBSD audit PASS")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B23 source, write-set, or cross-tree audit failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/B23-fixture-SHA256SUMS" \
|
||||
"$artifacts/B23-fixture-manifest.json" \
|
||||
"$artifacts/B23-oracle-first.json" \
|
||||
"$artifacts/B23-oracle-second.json" \
|
||||
"$artifacts/B23-source-audit.json" \
|
||||
>"$artifacts/SHA256SUMS"
|
||||
pre15_target_reached
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'TC171 host real-fixture, independent oracle, and bounded source audit PASS' \
|
||||
'QEMU exact-source mapped/backing runtime NOT_RUN in host mode' \
|
||||
'Full feature suite NOT_RUN'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in awk clang file nm scp tar; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B23 QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B23-fixtures.tar.gz
|
||||
module=$PRE15_CASE_TMP/B23-erofs.ko
|
||||
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B23-kld-build.stdout" \
|
||||
2>"$artifacts/B23-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B23 cross-target zstdio0 KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/B23-kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B23-kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/B23-kld-nm-u.txt"
|
||||
tar -C "$first" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B23-fixtures.tar.gz || \
|
||||
! pre15_scp "$probe" /root/B23-explicit-probe.c || \
|
||||
! pre15_scp "$module" /root/B23-erofs.ko; then
|
||||
pre15_infra_blocked 'could not transfer B23 module, probe, or fixtures'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/pre15-b23-fixtures && mkdir /root/pre15-b23-fixtures && tar -xzf /root/B23-fixtures.tar.gz -C /root/pre15-b23-fixtures'; then
|
||||
pre15_infra_blocked 'could not prepare B23 guest fixtures'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B23-erofs.ko \
|
||||
>"$artifacts/B23-kldload.stdout" 2>"$artifacts/B23-kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' "$artifacts/B23-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_dut_fail 'B23 exact-source KLD failed to load'
|
||||
fi
|
||||
pre15_own_guest_kld erofs 'B23 exact-source KLD'
|
||||
if ! pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
|
||||
-o /root/B23-explicit-probe /root/B23-explicit-probe.c; then
|
||||
pre15_infra_blocked 'could not compile the B23 guest probe'
|
||||
fi
|
||||
|
||||
python3 -B - "$first/fixture-manifest.json" >"$PRE15_CASE_TMP/B23-qemu-cases.tsv" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
manifest = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
for name, case in sorted(manifest["cases"].items()):
|
||||
offsets = ",".join(str(value) for value in case["probe_offsets"])
|
||||
print(
|
||||
name,
|
||||
case["image"],
|
||||
case["expected_errno"],
|
||||
case["file_size"],
|
||||
offsets,
|
||||
case["target_marker"],
|
||||
sep="\t",
|
||||
)
|
||||
PY
|
||||
|
||||
pre15_attach_md()
|
||||
{
|
||||
pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode -f "$1") || \
|
||||
pre15_dut_fail "could not attach B23 provider: $1"
|
||||
case "$pre15_md" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
|
||||
esac
|
||||
pre15_md=${pre15_md#md}
|
||||
pre15_own_guest_md "$pre15_md" "$2"
|
||||
printf '%s\n' "$pre15_md"
|
||||
}
|
||||
|
||||
while IFS="$(printf '\t')" read -r case_id image_name expected_errno \
|
||||
file_size offsets target_marker; do
|
||||
mountpoint=/mnt/pre15-b23-$case_id
|
||||
unit=$(pre15_attach_md "/root/pre15-b23-fixtures/$image_name" \
|
||||
"B23 $case_id primary")
|
||||
pre15_guest_ssh_bounded mkdir -p "$mountpoint"
|
||||
if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/md$unit" \
|
||||
"$mountpoint"; then
|
||||
pre15_dut_fail "B23 $case_id mount failed before $target_marker"
|
||||
fi
|
||||
pre15_own_guest_mount "$mountpoint" "B23 $case_id mount"
|
||||
if test "$expected_errno" -eq 0; then
|
||||
old_ifs=$IFS
|
||||
IFS=,
|
||||
set -- $offsets
|
||||
IFS=$old_ifs
|
||||
if ! pre15_guest_ssh_bounded /root/B23-explicit-probe pass \
|
||||
"$mountpoint/target.bin" "$file_size" "$@"; then
|
||||
pre15_dut_fail "B23 mapped positive failed at $target_marker"
|
||||
fi
|
||||
else
|
||||
if ! pre15_guest_ssh_bounded /root/B23-explicit-probe errno \
|
||||
"$mountpoint/target.bin" "$expected_errno"; then
|
||||
pre15_dut_fail "B23 corruption result failed at $target_marker"
|
||||
fi
|
||||
fi
|
||||
done <"$PRE15_CASE_TMP/B23-qemu-cases.tsv"
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/B23-dmesg.txt"
|
||||
if grep -Eq 'panic:|Fatal trap|lock order reversal|KDB: stack backtrace' \
|
||||
"$artifacts/B23-dmesg.txt"; then
|
||||
pre15_dut_fail 'B23 runtime produced a kernel diagnostic'
|
||||
fi
|
||||
printf '%s\n' \
|
||||
'TC171 QEMU explicit primary/metabox mapped and corruption runtime PASS' \
|
||||
'All errno values are positive FreeBSD ABI values; full feature suite NOT_RUN'
|
||||
Executable
+337
@@ -0,0 +1,337 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=fb79d17edba7c47fae6e60b2a92771a8f3a2fcdf
|
||||
zmap=$PRE15_DUT/src/zmap.c
|
||||
internal=$PRE15_DUT/src/internal.h
|
||||
linux_zmap=$PRE15_ROOT/src-linux/zmap.c
|
||||
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
|
||||
gate_input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B24 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b24-zmap "$zmap"
|
||||
pre15_record_fixture b24-internal "$internal"
|
||||
pre15_record_fixture b24-linux-zmap "$linux_zmap"
|
||||
pre15_record_fixture b24-map-oracle "$oracle"
|
||||
pre15_record_fixture b24-gate-input "$gate_input"
|
||||
pre15_record_fixture b24-case \
|
||||
"$PRE15_DUT/tests/pre15/cases/B24-zmap-order.sh"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$oracle" \
|
||||
"$gate_input" "$artifacts/B24-zmap-order.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
oracle_path = Path(sys.argv[4])
|
||||
gate_input_path = Path(sys.argv[5])
|
||||
report_path = Path(sys.argv[6])
|
||||
|
||||
|
||||
def committed(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B24 baseline {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if match is None:
|
||||
raise SystemExit(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, match.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", match.end())
|
||||
if brace < 0:
|
||||
raise SystemExit(f"missing function body: {name}")
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
function_order = [
|
||||
"z_erofs_index_base",
|
||||
"z_erofs_index_advance",
|
||||
"z_erofs_lcluster_count",
|
||||
"z_erofs_lcluster_pos",
|
||||
"z_erofs_lcn_advance",
|
||||
"z_erofs_compact_pblk",
|
||||
"z_erofs_fragment_offset",
|
||||
"z_erofs_post_eof_len",
|
||||
"z_erofs_physical_end",
|
||||
"z_erofs_read_index",
|
||||
"z_erofs_load_full_lcluster",
|
||||
"decode_compactedbits",
|
||||
"get_compacted_la_distance",
|
||||
"z_erofs_load_compact_lcluster",
|
||||
"z_erofs_load_lcluster_from_disk",
|
||||
"z_erofs_extent_lookback",
|
||||
"z_erofs_get_extent_compressedlen",
|
||||
"z_erofs_get_extent_decompressedlen",
|
||||
"z_erofs_extent_add",
|
||||
"z_erofs_extent_roundup",
|
||||
"z_erofs_extent_table_pos",
|
||||
"z_erofs_extent_record_pos",
|
||||
"z_erofs_read_extent",
|
||||
"z_erofs_extent_lstart",
|
||||
"z_erofs_validate_extent_table",
|
||||
"z_erofs_map_blocks_fo",
|
||||
"z_erofs_map_blocks_ext",
|
||||
"z_erofs_fill_inode",
|
||||
"z_erofs_map_sanity_check",
|
||||
"z_erofs_map_blocks",
|
||||
]
|
||||
linux_algorithm_order = [
|
||||
"z_erofs_load_full_lcluster",
|
||||
"decode_compactedbits",
|
||||
"get_compacted_la_distance",
|
||||
"z_erofs_load_compact_lcluster",
|
||||
"z_erofs_load_lcluster_from_disk",
|
||||
"z_erofs_extent_lookback",
|
||||
"z_erofs_get_extent_compressedlen",
|
||||
"z_erofs_get_extent_decompressedlen",
|
||||
"z_erofs_map_blocks_fo",
|
||||
"z_erofs_map_blocks_ext",
|
||||
"z_erofs_fill_inode",
|
||||
"z_erofs_map_sanity_check",
|
||||
]
|
||||
|
||||
current = (dut / "src/zmap.c").read_text(encoding="utf-8")
|
||||
before = committed("src/zmap.c")
|
||||
current_internal = (dut / "src/internal.h").read_text(encoding="utf-8")
|
||||
before_internal = committed("src/internal.h")
|
||||
linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8")
|
||||
|
||||
moved_start = before.index("static int\nz_erofs_extent_add(")
|
||||
moved_end = before.index("static int\nz_erofs_map_blocks_ext(", moved_start)
|
||||
moved = before[moved_start:moved_end]
|
||||
without_moved = before[:moved_start] + before[moved_end:]
|
||||
insert_at = without_moved.index("static int\nz_erofs_map_blocks_fo(")
|
||||
expected = without_moved[:insert_at] + moved + without_moved[insert_at:]
|
||||
if current != expected:
|
||||
raise SystemExit("zmap.c differs from the exact B24 validator-group move")
|
||||
|
||||
before_functions = {name: function(before, name) for name in function_order}
|
||||
current_functions = {name: function(current, name) for name in function_order}
|
||||
for name in function_order:
|
||||
if current_functions[name] != before_functions[name]:
|
||||
raise SystemExit(f"B24 changed function text instead of moving it: {name}")
|
||||
|
||||
def definition_position(source: str, name: str) -> int:
|
||||
match = re.search(
|
||||
r"^(?:" + re.escape(name) + r"|(?:static\s+)?"
|
||||
r"[A-Za-z_][A-Za-z0-9_ *]*\b" + re.escape(name) + r")\s*\(",
|
||||
source,
|
||||
re.MULTILINE,
|
||||
)
|
||||
if match is None:
|
||||
raise SystemExit(f"missing definition position: {name}")
|
||||
return match.start()
|
||||
|
||||
|
||||
positions = [definition_position(current, name) for name in function_order]
|
||||
if positions != sorted(positions):
|
||||
raise SystemExit("B24 zmap definition order does not match the reviewed order")
|
||||
for source, label in ((current, "FreeBSD"), (linux, "Linux")):
|
||||
positions = [definition_position(source, name) for name in linux_algorithm_order]
|
||||
if positions != sorted(positions):
|
||||
raise SystemExit(f"{label} common zmap algorithm order drifted")
|
||||
|
||||
def direct_calls(functions: dict[str, str]) -> Counter[tuple[str, str]]:
|
||||
calls: Counter[tuple[str, str]] = Counter()
|
||||
for caller, body in functions.items():
|
||||
brace = body.find("{")
|
||||
for callee in re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", body[brace + 1 :]):
|
||||
if callee not in {"if", "for", "while", "switch", "return", "sizeof"}:
|
||||
calls[(caller, callee)] += 1
|
||||
return calls
|
||||
|
||||
|
||||
before_calls = direct_calls(before_functions)
|
||||
current_calls = direct_calls(current_functions)
|
||||
if current_calls != before_calls:
|
||||
raise SystemExit("B24 changed the direct-call multiset")
|
||||
|
||||
global_definitions = sorted(
|
||||
name for name, body in current_functions.items() if not body.startswith("static ")
|
||||
)
|
||||
if global_definitions != ["z_erofs_fill_inode", "z_erofs_map_blocks"]:
|
||||
raise SystemExit(f"unexpected zmap global definitions: {global_definitions}")
|
||||
if current_internal != before_internal:
|
||||
raise SystemExit("B24 changed internal.h despite no removable zmap global")
|
||||
inode_source = (dut / "src/inode.c").read_text(encoding="utf-8")
|
||||
data_source = (dut / "src/data.c").read_text(encoding="utf-8")
|
||||
if inode_source.count("z_erofs_fill_inode(sbi, vi)") != 1:
|
||||
raise SystemExit("eager z_erofs_fill_inode no longer has its inode consumer")
|
||||
if data_source.count("z_erofs_map_blocks(sbi, vi, &next)") != 1:
|
||||
raise SystemExit("compressed map backend no longer has its common adapter consumer")
|
||||
for prototype in (
|
||||
"int z_erofs_fill_inode(struct erofs_sb_info *sbi, struct erofs_inode *vi);",
|
||||
"int z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
|
||||
" struct erofs_map_blocks *map);",
|
||||
):
|
||||
if current_internal.count(prototype) != 1:
|
||||
raise SystemExit(f"required FreeBSD zmap prototype drifted: {prototype!r}")
|
||||
if "int z_erofs_map_blocks_iter(struct inode *inode, struct erofs_map_blocks *map," not in linux:
|
||||
raise SystemExit("Linux zmap iterator naming anchor changed")
|
||||
|
||||
for marker in ("erofs_read_metadata(", "erofs_put_metabuf(", "goto out;"):
|
||||
if current.count(marker) != before.count(marker):
|
||||
raise SystemExit(f"B24 changed provider I/O or cleanup marker count: {marker}")
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", current):
|
||||
raise SystemExit("B24 introduced Linux negative errno")
|
||||
|
||||
oracle = json.loads(oracle_path.read_text(encoding="ascii"))
|
||||
gate_input = json.loads(gate_input_path.read_text(encoding="ascii"))
|
||||
records = oracle.get("records", [])
|
||||
if oracle.get("map_case_count") != 80 or len(records) != 80:
|
||||
raise SystemExit("frozen G02/B07 map tuple denominator changed")
|
||||
if gate_input.get("expected_device_case_count") != 13:
|
||||
raise SystemExit("frozen G02/B07 device denominator changed")
|
||||
tuple_bytes = b"".join(bytes.fromhex(record["tuple_hex"]) for record in records)
|
||||
tuple_digest = hashlib.sha256(tuple_bytes).hexdigest()
|
||||
if tuple_digest != oracle.get("tuple_bytes_sha256"):
|
||||
raise SystemExit("frozen G02/B07 tuple bytes no longer match their digest")
|
||||
if len({record["id"] for record in records}) != 80:
|
||||
raise SystemExit("frozen G02/B07 tuple IDs are not unique")
|
||||
|
||||
changed = set(
|
||||
subprocess.run(
|
||||
["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
changed.update(
|
||||
subprocess.run(
|
||||
[
|
||||
"git", "-C", str(root), "ls-files", "--others", "--exclude-standard",
|
||||
"--", "repo-pre-15",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
allowed = {
|
||||
"repo-pre-15/src/zmap.c",
|
||||
"repo-pre-15/tests/pre15/cases/B24-zmap-order.sh",
|
||||
}
|
||||
if changed != allowed:
|
||||
raise SystemExit(f"B24 write set differs: {sorted(changed ^ allowed)}")
|
||||
|
||||
function_digest = hashlib.sha256(
|
||||
"\n\n".join(current_functions[name] for name in function_order).encode("utf-8")
|
||||
).hexdigest()
|
||||
report = {
|
||||
"baseline": baseline,
|
||||
"batch": "B24",
|
||||
"candidate": "P15-016",
|
||||
"direct_call_edges": sum(current_calls.values()),
|
||||
"freebsd_backend_name": "z_erofs_map_blocks",
|
||||
"freebsd_positive_errno": True,
|
||||
"function_count": len(function_order),
|
||||
"function_text_sha256": function_digest,
|
||||
"global_definitions": global_definitions,
|
||||
"linux_iterator_name": "z_erofs_map_blocks_iter",
|
||||
"map_cases": len(records),
|
||||
"device_cases": gate_input["expected_device_case_count"],
|
||||
"provider_io_unchanged": True,
|
||||
"metadata_cleanup_unchanged": True,
|
||||
"status": "PASS",
|
||||
"tuple_bytes_sha256": tuple_digest,
|
||||
"write_set": sorted(changed),
|
||||
}
|
||||
report_path.write_text(
|
||||
json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
print(
|
||||
"B24 order/callgraph PASS "
|
||||
f"functions={len(function_order)} calls={sum(current_calls.values())} globals=2"
|
||||
)
|
||||
print(
|
||||
"B24 map tuples PASS "
|
||||
f"maps={len(records)} devices={gate_input['expected_device_case_count']} "
|
||||
f"sha256={tuple_digest}"
|
||||
)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B24 order, callgraph, visibility, or tuple proof failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/B24-zmap-order.json" >"$artifacts/SHA256SUMS"
|
||||
printf '%s\n' \
|
||||
'B24 host PASS exact validator move and Linux algorithm order' \
|
||||
'B24 host PASS frozen G02 map tuples; QEMU and full feature suite NOT_RUN'
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=c750850264cbc3c52ef4407aaf0c8d02ec26e973
|
||||
fixture=$PRE15_DUT/tests/pre15/fixtures/B25-codec-errors.json
|
||||
oracle=$PRE15_DUT/tests/pre15/fixtures/B25-codec-oracle.py
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in cc git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B25 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b25-case "$PRE15_DUT/tests/pre15/cases/B25-codec-errors.sh"
|
||||
pre15_record_fixture b25-fixture "$fixture"
|
||||
pre15_record_fixture b25-oracle "$oracle"
|
||||
for source in compress.h decompressor.c decompressor_lz4.c \
|
||||
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
|
||||
pre15_record_fixture "b25-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 -B "$oracle" --root "$PRE15_ROOT" --dut "$PRE15_DUT" \
|
||||
--baseline "$baseline" --fixture "$fixture" --artifacts "$artifacts"
|
||||
then
|
||||
pre15_record_fixture b25-report "$artifacts/B25-codec-errors.json"
|
||||
else
|
||||
pre15_dut_fail 'B25 typed codec errno oracle failed'
|
||||
fi
|
||||
Executable
+680
@@ -0,0 +1,680 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B27-stream-tail.json
|
||||
generator=$fixture_dir/B27-stream-fixtures.py
|
||||
probe=$fixture_dir/B27-stream-probe.c
|
||||
compact_probe=$fixture_dir/B27-compact-finalization.c
|
||||
kld_builder=$fixture_dir/B27-build-kld.sh
|
||||
gate_script=$PRE15_DUT/tests/pre15/gates/P15-083.sh
|
||||
gate_input=$PRE15_DUT/tests/pre15/gates/P15-083-input.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
gate_clone=$PRE15_CASE_TMP/gate-clone
|
||||
gate_output=$PRE15_CASE_TMP/gate-output
|
||||
first=$PRE15_CASE_TMP/fixtures-first
|
||||
second=$PRE15_CASE_TMP/fixtures-second
|
||||
|
||||
for tool in cc cmp diff dump.erofs fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B27 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b27-case "$PRE15_DUT/tests/pre15/cases/B27-stream-tail.sh"
|
||||
pre15_record_fixture b27-spec "$spec"
|
||||
pre15_record_fixture b27-generator "$generator"
|
||||
pre15_record_fixture b27-probe "$probe"
|
||||
pre15_record_fixture b27-compact-finalization "$compact_probe"
|
||||
pre15_record_fixture b27-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b27-gate "$gate_script"
|
||||
pre15_record_fixture b27-gate-input "$gate_input"
|
||||
for source in decompressor_deflate.c decompressor_lzma.c decompressor_zstd.c \
|
||||
decompressor.c zdata.c zmap.c compress.h; do
|
||||
pre15_record_fixture "b27-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if ! git clone -q --shared --no-checkout "$PRE15_ROOT" "$gate_clone" \
|
||||
>"$artifacts/gate-clone.stdout" 2>"$artifacts/gate-clone.stderr" || \
|
||||
! git -C "$gate_clone" checkout -q --detach \
|
||||
338ba8daf81c1461b5d28ca9c5345686e4e0f2eb \
|
||||
>"$artifacts/gate-checkout.stdout" 2>"$artifacts/gate-checkout.stderr"; then
|
||||
pre15_runner_fail 'could not materialize the frozen P15-083 gate commit'
|
||||
fi
|
||||
frozen_gate=$gate_clone/repo-pre-15/tests/pre15/gates/P15-083.sh
|
||||
if ! (umask 022 && timeout -k 10 240 "$frozen_gate" \
|
||||
--base 68bbe94c44e35d53cec8ab55d007f40b01cf0502 \
|
||||
--output "$gate_output") >"$artifacts/gate.stdout" \
|
||||
2>"$artifacts/gate.stderr"; then
|
||||
pre15_runner_fail 'frozen P15-083 gate replay failed'
|
||||
fi
|
||||
|
||||
if ! (umask 022 && python3 -B "$generator" --spec "$spec" --output "$first") \
|
||||
>"$artifacts/generate-first.json" 2>"$artifacts/generate-first.stderr"; then
|
||||
pre15_runner_fail 'B27 first real EROFS fixture generation failed'
|
||||
fi
|
||||
if ! (umask 022 && python3 -B "$generator" --spec "$spec" --output "$second") \
|
||||
>"$artifacts/generate-second.json" 2>"$artifacts/generate-second.stderr"; then
|
||||
pre15_runner_fail 'B27 second real EROFS fixture generation failed'
|
||||
fi
|
||||
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
|
||||
pre15_runner_fail 'B27 real EROFS fixtures are not byte reproducible'
|
||||
fi
|
||||
|
||||
cp "$gate_output/result.json" "$artifacts/P15-083-result.json"
|
||||
cp "$gate_output/codec-results.json" "$artifacts/P15-083-codec-results.json"
|
||||
cp "$first/fixture-index.json" "$artifacts/B27-fixture-index.json"
|
||||
|
||||
if ! cc -std=c11 -Wall -Wextra -Werror "$compact_probe" \
|
||||
-o "$PRE15_CASE_TMP/B27-compact-finalization" \
|
||||
>"$artifacts/B27-compact-finalization-build.stdout" \
|
||||
2>"$artifacts/B27-compact-finalization-build.stderr" || \
|
||||
! "$PRE15_CASE_TMP/B27-compact-finalization" \
|
||||
>"$artifacts/B27-compact-finalization.stdout" \
|
||||
2>"$artifacts/B27-compact-finalization.stderr"; then
|
||||
pre15_dut_fail 'B27 compact HEAD finalization regression failed'
|
||||
fi
|
||||
if ! python3 -B - "$PRE15_DUT/src/zmap.c" \
|
||||
"$PRE15_DUT/src/decompressor_zstd.c" \
|
||||
"$artifacts/B27-compact-finalization-source.json" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
source = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||
zstd = Path(sys.argv[2]).read_text(encoding="utf-8")
|
||||
rejected = """\t\t\t\tif ((lo & Z_EROFS_LI_D0_CBLKCNT) != 0) {
|
||||
\t\t\t\t\tif (i == 0) {
|
||||
\t\t\t\t\t\terofs_put_metabuf(&buf);
|
||||
\t\t\t\t\t\treturn (EINTEGRITY);
|
||||
\t\t\t\t\t}
|
||||
"""
|
||||
accepted = """\t\t\t\tif ((lo & Z_EROFS_LI_D0_CBLKCNT) != 0) {
|
||||
\t\t\t\t\t--i;
|
||||
\t\t\t\t\tnblk += lo & ~Z_EROFS_LI_D0_CBLKCNT;
|
||||
\t\t\t\t\tcontinue;
|
||||
"""
|
||||
if rejected in source or source.count(accepted) != 1:
|
||||
raise SystemExit("B27 compact HEAD finalization source mismatch")
|
||||
if zstd.count("\t.supports_subextent = 0,\n") != 1:
|
||||
raise SystemExit("B27 Zstd ordinary subextent fallback is disabled")
|
||||
Path(sys.argv[3]).write_text(json.dumps({
|
||||
"cblkcnt_first_slot": "accepted",
|
||||
"linux_completion_semantics": True,
|
||||
"status": "PASS",
|
||||
"zstd_ordinary_subextent": False,
|
||||
"zstd_partial_reference_completion": True,
|
||||
}, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
PY
|
||||
then
|
||||
pre15_dut_fail 'B27 compact HEAD finalization source audit failed'
|
||||
fi
|
||||
|
||||
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
|
||||
pre15_target_reached
|
||||
else
|
||||
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" \
|
||||
"$gate_script" "$gate_input" "$gate_output/result.json" \
|
||||
"$gate_output/codec-results.json" "$first/fixture-index.json" \
|
||||
"$artifacts/B27-source-audit.json" "$PRE15_CASE_TMP/finish-harness.c" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
spec = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
|
||||
gate_script = Path(sys.argv[4])
|
||||
gate_input = Path(sys.argv[5])
|
||||
result = json.loads(Path(sys.argv[6]).read_text(encoding="ascii"))
|
||||
codec_results = json.loads(Path(sys.argv[7]).read_text(encoding="ascii"))
|
||||
fixture_index = json.loads(Path(sys.argv[8]).read_text(encoding="ascii"))
|
||||
report_path = Path(sys.argv[9])
|
||||
harness_path = Path(sys.argv[10])
|
||||
baseline = spec["baseline"]
|
||||
implementation = "0eda2fcfc94aba1e51d3f9ee9885260969240de1"
|
||||
runtime_base = "445da30f913dc292666f340588236498ea793db3"
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(message)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def committed(commit: str, name: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{commit}:repo-pre-15/src/{name}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
fail(f"cannot read B27 source {commit}:{name}: {completed.stderr.strip()}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def replace_once(source: str, old: str, new: str, label: str) -> str:
|
||||
count = source.count(old)
|
||||
if count != 1:
|
||||
fail(f"{label}: expected one baseline occurrence, found {count}")
|
||||
return source.replace(old, new, 1)
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if match is None:
|
||||
fail(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, match.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", match.end())
|
||||
depth = 0
|
||||
for index in range(brace, len(source)):
|
||||
if source[index] == "{":
|
||||
depth += 1
|
||||
elif source[index] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
fail(f"unterminated function: {name}")
|
||||
|
||||
|
||||
if (
|
||||
spec.get("schema") != 1
|
||||
or spec.get("batch") != "B27"
|
||||
or spec.get("candidate") != "P15-083"
|
||||
or spec.get("test") != "TC176-stream-runtime"
|
||||
):
|
||||
fail("B27 fixture identity changed")
|
||||
if sha256(gate_script) != spec["gate"]["script_sha256"]:
|
||||
fail("tracked P15-083 gate script differs from G04 GO")
|
||||
if sha256(gate_input) != spec["gate"]["input_sha256"]:
|
||||
fail("tracked P15-083 gate input differs from G04 GO")
|
||||
if not (
|
||||
result.get("status") == "GO"
|
||||
and result.get("b27") == "AUTHORIZED"
|
||||
and result.get("cleanup") == "PASS"
|
||||
and result.get("typed_errno") == "PASS"
|
||||
and result.get("codecs") == {"deflate": "GO", "lzma": "GO", "zstd": "GO"}
|
||||
):
|
||||
fail("frozen P15-083 gate did not reproduce B27 authorization")
|
||||
|
||||
observed = {item["codec"]: item for item in codec_results}
|
||||
if set(observed) != set(spec["codecs"]):
|
||||
fail("G04 codec result set changed")
|
||||
for codec, expected in spec["codecs"].items():
|
||||
item = observed[codec]
|
||||
gate = expected["gate"]
|
||||
extent = expected["extent"]
|
||||
if any(item["extent"][key] != extent[key] for key in (
|
||||
"leading_zero_bytes", "logical_length", "logical_offset",
|
||||
"physical_length", "physical_offset", "stream_bytes"
|
||||
)):
|
||||
fail(f"{codec} legal leading-padding extent changed")
|
||||
checks = (
|
||||
item["full"]["consumed"] == gate["full_consumed"],
|
||||
item["full"]["policy_errno"] == 0,
|
||||
item["partial"]["consumed"] == gate["partial_consumed"],
|
||||
item["partial"]["requested_bytes"] == extent["partial_size"],
|
||||
item["partial"]["policy_errno"] == 0,
|
||||
item["partial"]["corrupt_policy_errno"] == 0,
|
||||
item["tail"]["consumed"] == gate["tail_consumed"],
|
||||
item["tail"]["policy_errno"] == 97,
|
||||
item["tail"]["fsck"]["exit"] == gate["tail_fsck_exit"],
|
||||
item["truncated"]["consumed"] == gate["truncated_consumed"],
|
||||
item["truncated"]["policy_errno"] == 97,
|
||||
item["corruption"]["full_errno"] == 97,
|
||||
item["corruption"]["starts_at_stream_byte"] == expected["corruption_start"],
|
||||
item["corruption"]["starts_after_partial_consumed"] is True,
|
||||
)
|
||||
if not all(checks):
|
||||
fail(f"{codec} G04 completion/trailing/partial contract changed")
|
||||
for phase in ("full", "partial", "tail", "truncated"):
|
||||
if item[phase]["cleanup"] != 1 or item[phase]["guards"] != 1:
|
||||
fail(f"{codec} {phase} oracle cleanup/guard failed")
|
||||
|
||||
if fixture_index.get("fixture_count") != 12 or fixture_index.get("status") != "READY":
|
||||
fail("B27 runtime fixture matrix is incomplete")
|
||||
classes = {(item["codec"], item["class"]) for item in fixture_index["fixtures"]}
|
||||
if classes != {
|
||||
(codec, kind)
|
||||
for codec in spec["codecs"]
|
||||
for kind in ("valid", "tail", "truncated", "corrupt")
|
||||
}:
|
||||
fail("B27 runtime fixture class set changed")
|
||||
|
||||
base = {
|
||||
name: committed(baseline, name)
|
||||
for name in (
|
||||
"compress.h", "decompressor.c", "decompressor_deflate.c",
|
||||
"decompressor_lzma.c", "decompressor_zstd.c", "zdata.c"
|
||||
)
|
||||
}
|
||||
implemented = {name: committed(implementation, name) for name in base}
|
||||
for name in ("compress.h", "decompressor.c", "zdata.c"):
|
||||
if implemented[name] != base[name]:
|
||||
fail(f"B27 changed excluded provider/GEOM/buffer-lifetime file: {name}")
|
||||
|
||||
deflate_helper = '''static int
|
||||
z_erofs_deflate_finish(const struct z_erofs_decompress_req *rq, int ret,
|
||||
uInt avail_in)
|
||||
{
|
||||
|
||||
\tif (rq->partial_decoding)
|
||||
\t\treturn (0);
|
||||
\tif (ret != Z_STREAM_END || avail_in != 0)
|
||||
\t\treturn (EINTEGRITY);
|
||||
\treturn (0);
|
||||
}
|
||||
|
||||
'''
|
||||
expected = replace_once(
|
||||
base["decompressor_deflate.c"],
|
||||
"static int\nz_erofs_deflate_decompress",
|
||||
deflate_helper + "static int\nz_erofs_deflate_decompress",
|
||||
"Deflate completion helper",
|
||||
)
|
||||
expected = replace_once(
|
||||
expected,
|
||||
"\telse if (error == 0 && !rq->partial_decoding &&\n"
|
||||
"\t (ret != Z_STREAM_END || strm.avail_in != 0))\n"
|
||||
"\t\terror = EINTEGRITY;",
|
||||
"\telse if (error == 0)\n"
|
||||
"\t\terror = z_erofs_deflate_finish(rq, ret, strm.avail_in);",
|
||||
"Deflate completion call",
|
||||
)
|
||||
if implemented["decompressor_deflate.c"] != expected:
|
||||
fail("Deflate differs from the exact B27 transform")
|
||||
|
||||
lzma_helper = '''static int
|
||||
z_erofs_lzma_finish(const struct z_erofs_decompress_req *rq, enum xz_ret ret,
|
||||
size_t input_pos)
|
||||
{
|
||||
|
||||
\tif (rq->partial_decoding &&
|
||||
\t (ret == XZ_OK || ret == XZ_STREAM_END))
|
||||
\t\treturn (0);
|
||||
\tif (!rq->partial_decoding && ret == XZ_STREAM_END &&
|
||||
\t input_pos == rq->inputsize)
|
||||
\t\treturn (0);
|
||||
\treturn (z_erofs_lzma_error(ret));
|
||||
}
|
||||
|
||||
'''
|
||||
expected = replace_once(
|
||||
base["decompressor_lzma.c"],
|
||||
"static int\nz_erofs_lzma_decompress",
|
||||
lzma_helper + "static int\nz_erofs_lzma_decompress",
|
||||
"LZMA completion helper",
|
||||
)
|
||||
expected = replace_once(
|
||||
expected,
|
||||
"\telse if (rq->partial_decoding &&\n"
|
||||
"\t (ret == XZ_OK || ret == XZ_STREAM_END))\n"
|
||||
"\t\terror = 0;\n"
|
||||
"\telse if (!rq->partial_decoding && ret == XZ_STREAM_END &&\n"
|
||||
"\t buffer.in_pos == rq->inputsize)\n"
|
||||
"\t\terror = 0;\n"
|
||||
"\telse\n"
|
||||
"\t\terror = z_erofs_lzma_error(ret);",
|
||||
"\telse\n"
|
||||
"\t\terror = z_erofs_lzma_finish(rq, ret, buffer.in_pos);",
|
||||
"LZMA completion call",
|
||||
)
|
||||
if implemented["decompressor_lzma.c"] != expected:
|
||||
fail("LZMA differs from the exact B27 transform")
|
||||
|
||||
zstd_helper = '''static int
|
||||
z_erofs_zstd_finish(const struct z_erofs_decompress_req *rq, size_t ret,
|
||||
size_t input_pos, size_t input_size)
|
||||
{
|
||||
|
||||
\tif (rq->partial_decoding)
|
||||
\t\treturn (0);
|
||||
\tif (ret != 0 || input_pos != input_size)
|
||||
\t\treturn (EINTEGRITY);
|
||||
\treturn (0);
|
||||
}
|
||||
|
||||
'''
|
||||
expected = replace_once(
|
||||
base["decompressor_zstd.c"],
|
||||
"static const ZSTD_customMem zstd_erofs_alloc = {\n"
|
||||
"\t.customAlloc = zstd_alloc,\n"
|
||||
"\t.customFree = zstd_free,\n"
|
||||
"\t.opaque = M_EROFS,\n"
|
||||
"};\n\n"
|
||||
"static int\nz_erofs_zstd_decompress",
|
||||
"static const ZSTD_customMem zstd_erofs_alloc = {\n"
|
||||
"\t.customAlloc = zstd_alloc,\n"
|
||||
"\t.customFree = zstd_free,\n"
|
||||
"\t.opaque = M_EROFS,\n"
|
||||
"};\n\n" + zstd_helper + "static int\nz_erofs_zstd_decompress",
|
||||
"Zstd completion helper",
|
||||
)
|
||||
expected = replace_once(
|
||||
expected,
|
||||
"\telse if (error == 0 && !rq->partial_decoding &&\n"
|
||||
"\t (ret != 0 || input.pos != input.size))\n"
|
||||
"\t\terror = EINTEGRITY;",
|
||||
"\telse if (error == 0)\n"
|
||||
"\t\terror = z_erofs_zstd_finish(rq, ret, input.pos, input.size);",
|
||||
"Zstd completion call",
|
||||
)
|
||||
if implemented["decompressor_zstd.c"] != expected:
|
||||
fail("Zstd differs from the exact B27 transform")
|
||||
|
||||
runtime_names = (
|
||||
"decompressor_deflate.c", "decompressor_lzma.c",
|
||||
"decompressor_zstd.c", "zmap.c",
|
||||
)
|
||||
runtime_before = {name: committed(runtime_base, name) for name in runtime_names}
|
||||
current = {
|
||||
name: (dut / "src" / name).read_text(encoding="utf-8")
|
||||
for name in runtime_names
|
||||
}
|
||||
if current["decompressor_deflate.c"] != runtime_before["decompressor_deflate.c"]:
|
||||
fail("B27 remediation changed Deflate beyond the accepted implementation")
|
||||
if current["decompressor_lzma.c"] != runtime_before["decompressor_lzma.c"]:
|
||||
fail("B27 remediation changed LZMA beyond the accepted implementation")
|
||||
if current["decompressor_zstd.c"] != runtime_before["decompressor_zstd.c"]:
|
||||
fail("B27 remediation Zstd completion differs from the exact transform")
|
||||
expected = replace_once(
|
||||
runtime_before["zmap.c"],
|
||||
"\t\t\t\t\tif (i == 0) {\n"
|
||||
"\t\t\t\t\t\terofs_put_metabuf(&buf);\n"
|
||||
"\t\t\t\t\t\treturn (EINTEGRITY);\n"
|
||||
"\t\t\t\t\t}\n",
|
||||
"",
|
||||
"compact HEAD first-slot CBLKCNT completion",
|
||||
)
|
||||
if current["zmap.c"] != expected:
|
||||
fail("B27 remediation compact HEAD completion differs from the exact transform")
|
||||
|
||||
changed_source = set(
|
||||
subprocess.run(
|
||||
["git", "-C", str(root), "diff", "--name-only", runtime_base, "--", "repo-pre-15/src"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
if changed_source != {
|
||||
"repo-pre-15/src/zmap.c",
|
||||
}:
|
||||
fail(f"B27 source write set changed: {sorted(changed_source)}")
|
||||
|
||||
all_changed = set(
|
||||
subprocess.run(
|
||||
["git", "-C", str(root), "diff", "--name-only", runtime_base, "--", "repo-pre-15"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
all_changed.update(
|
||||
subprocess.run(
|
||||
["git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "--", "repo-pre-15"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
expected_write_set = {
|
||||
"repo-pre-15/src/zmap.c",
|
||||
"repo-pre-15/tests/pre15/cases/B27-stream-tail.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B27-compact-finalization.c",
|
||||
}
|
||||
if all_changed != expected_write_set:
|
||||
fail(f"B27 exact write set differs: {sorted(all_changed ^ expected_write_set)}")
|
||||
|
||||
joined = "\n".join(current.values())
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", joined):
|
||||
fail("negative Linux errno entered B27")
|
||||
if "ZSTD_getErrorCode" in current["decompressor_zstd.c"]:
|
||||
fail("B27 expanded the optional Zstd provider ABI")
|
||||
|
||||
linux = {
|
||||
codec: (root / f"src-linux/decompressor_{codec}.c").read_text(encoding="utf-8")
|
||||
for codec in spec["codecs"]
|
||||
}
|
||||
for codec, marker in {
|
||||
"deflate": "if (zerr == Z_STREAM_END && !rq->outputsize)",
|
||||
"lzma": "xz_dec_microlzma_reset(strm->state, rq->inputsize, rq->outputsize",
|
||||
"zstd": "zerr = zstd_decompress_stream(stream, &out_buf, &in_buf);",
|
||||
}.items():
|
||||
if marker not in linux[codec]:
|
||||
fail(f"Linux {codec} stream-tail anchor changed")
|
||||
|
||||
harness = f'''#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define EINTEGRITY 97
|
||||
#define ENOMEM 12
|
||||
#define EOPNOTSUPP 95
|
||||
#define Z_STREAM_END 1
|
||||
typedef unsigned int uInt;
|
||||
|
||||
enum xz_ret {{
|
||||
XZ_OK, XZ_STREAM_END, XZ_UNSUPPORTED_CHECK, XZ_MEM_ERROR,
|
||||
XZ_MEMLIMIT_ERROR, XZ_FORMAT_ERROR, XZ_OPTIONS_ERROR,
|
||||
XZ_DATA_ERROR, XZ_BUF_ERROR
|
||||
}};
|
||||
|
||||
struct z_erofs_decompress_req {{
|
||||
int partial_decoding;
|
||||
size_t inputsize;
|
||||
}};
|
||||
|
||||
{extract_function(current["decompressor_lzma.c"], "z_erofs_lzma_error")}
|
||||
|
||||
{extract_function(current["decompressor_deflate.c"], "z_erofs_deflate_finish")}
|
||||
|
||||
{extract_function(current["decompressor_lzma.c"], "z_erofs_lzma_finish")}
|
||||
|
||||
{extract_function(current["decompressor_zstd.c"], "z_erofs_zstd_finish")}
|
||||
|
||||
static int failures;
|
||||
|
||||
static void
|
||||
check(const char *name, int actual, int expected)
|
||||
{{
|
||||
if (actual != expected) {{
|
||||
fprintf(stderr, "%s: actual=%d expected=%d\\n", name, actual, expected);
|
||||
failures++;
|
||||
}}
|
||||
}}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{{
|
||||
struct z_erofs_decompress_req full = {{ 0, 548 }};
|
||||
struct z_erofs_decompress_req partial = {{ 1, 548 }};
|
||||
|
||||
check("deflate exact", z_erofs_deflate_finish(&full, Z_STREAM_END, 0), 0);
|
||||
check("deflate tail", z_erofs_deflate_finish(&full, Z_STREAM_END, 8), EINTEGRITY);
|
||||
check("deflate no end", z_erofs_deflate_finish(&full, 0, 0), EINTEGRITY);
|
||||
check("deflate partial", z_erofs_deflate_finish(&partial, 0, 8), 0);
|
||||
check("lzma exact", z_erofs_lzma_finish(&full, XZ_STREAM_END, 548), 0);
|
||||
check("lzma tail", z_erofs_lzma_finish(&full, XZ_STREAM_END, 540), EINTEGRITY);
|
||||
check("lzma no end", z_erofs_lzma_finish(&full, XZ_OK, 548), EINTEGRITY);
|
||||
check("lzma partial", z_erofs_lzma_finish(&partial, XZ_OK, 352), 0);
|
||||
check("lzma partial error", z_erofs_lzma_finish(&partial, XZ_DATA_ERROR, 352), EINTEGRITY);
|
||||
check("zstd exact", z_erofs_zstd_finish(&full, 0, 548, 548), 0);
|
||||
check("zstd tail", z_erofs_zstd_finish(&full, 0, 540, 548), EINTEGRITY);
|
||||
check("zstd no end", z_erofs_zstd_finish(&full, 1, 548, 548), EINTEGRITY);
|
||||
check("zstd partial", z_erofs_zstd_finish(&partial, 1, 352, 548), 0);
|
||||
return failures != 0;
|
||||
}}
|
||||
'''
|
||||
harness_path.write_text(harness, encoding="ascii")
|
||||
|
||||
report = {
|
||||
"baseline": baseline,
|
||||
"batch": "B27",
|
||||
"candidate": "P15-083",
|
||||
"cleanup_and_guards": True,
|
||||
"codecs": sorted(spec["codecs"]),
|
||||
"fixture_count": fixture_index["fixture_count"],
|
||||
"freebsd_positive_errno": True,
|
||||
"gate": "GO",
|
||||
"legal_leading_padding_preserved": True,
|
||||
"linux_tail_semantics_audited": True,
|
||||
"optional_zstd_abi_preserved": True,
|
||||
"provider_geom_and_buffer_lifetime_unchanged": True,
|
||||
"runtime_base": runtime_base,
|
||||
"status": "PASS",
|
||||
"test": "TC176-stream-runtime",
|
||||
"write_set": sorted(all_changed),
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
PY
|
||||
then
|
||||
pre15_runner_fail 'B27 gate/source/write-set audit failed'
|
||||
fi
|
||||
|
||||
if ! cc -std=c11 -Wall -Wextra -Werror "$PRE15_CASE_TMP/finish-harness.c" \
|
||||
-o "$PRE15_CASE_TMP/finish-harness" >"$artifacts/finish-harness-build.stdout" \
|
||||
2>"$artifacts/finish-harness-build.stderr" || \
|
||||
! "$PRE15_CASE_TMP/finish-harness" >"$artifacts/finish-harness.stdout" \
|
||||
2>"$artifacts/finish-harness.stderr"; then
|
||||
pre15_dut_fail 'B27 extracted completion policy harness failed'
|
||||
fi
|
||||
sha256sum "$artifacts/P15-083-result.json" \
|
||||
"$artifacts/P15-083-codec-results.json" \
|
||||
"$artifacts/B27-fixture-index.json" \
|
||||
"$artifacts/B27-source-audit.json" >"$artifacts/SHA256SUMS"
|
||||
fi
|
||||
pre15_target_reached
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'TC176 B27 host real LZMA/Deflate/Zstd stream-tail subset PASS' \
|
||||
'Legal padding, trailing garbage, truncation, partial decode, corruption, positive errno, and cleanup PASS' \
|
||||
'QEMU runtime NOT_RUN in host mode' \
|
||||
'Full feature suite NOT_RUN'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in awk clang file nm scp tar; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B27 QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B27-fixtures.tar.gz
|
||||
module=$PRE15_CASE_TMP/B27-erofs-zstdio1.ko
|
||||
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work" 1 >"$artifacts/B27-kld-build.stdout" \
|
||||
2>"$artifacts/B27-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B27 cross-target zstdio1 KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/B27-kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B27-kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/B27-kld-nm-u.txt"
|
||||
tar -C "$first" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B27-fixtures.tar.gz || \
|
||||
! pre15_scp "$probe" /root/B27-stream-probe.c || \
|
||||
! pre15_scp "$module" /root/B27-erofs-zstdio1.ko; then
|
||||
pre15_infra_blocked 'could not transfer B27 module, probe, or fixtures'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/B27-fixtures && mkdir /root/B27-fixtures && tar -xzf /root/B27-fixtures.tar.gz -C /root/B27-fixtures && cc -O2 -Wall -Wextra -Werror -o /root/B27-stream-probe /root/B27-stream-probe.c'; then
|
||||
pre15_infra_blocked 'could not prepare B27 guest fixtures/probe'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B27-erofs-zstdio1.ko \
|
||||
>"$artifacts/B27-kldload.stdout" 2>"$artifacts/B27-kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' \
|
||||
"$artifacts/B27-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
if grep -Eq 'link_elf|symbol|not defined' "$artifacts/B27-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel lacks the planned ZSTDIO provider ABI'
|
||||
fi
|
||||
pre15_dut_fail 'B27 exact-source zstdio1 KLD failed to load'
|
||||
fi
|
||||
pre15_own_guest_kld erofs 'B27 exact-source KLD'
|
||||
|
||||
attach_mount()
|
||||
{
|
||||
image=$1
|
||||
label=$2
|
||||
B27_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "/root/B27-fixtures/images/$image") || \
|
||||
pre15_dut_fail "$label md attach failed"
|
||||
case "$B27_MD" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected B27 md unit: $B27_MD" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$B27_MD" "$label md"
|
||||
B27_MOUNT=/mnt/pre15-b27-$label
|
||||
pre15_guest_ssh_bounded mkdir -p "$B27_MOUNT"
|
||||
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B27_MD" "$B27_MOUNT" || \
|
||||
pre15_dut_fail "$label mount failed"
|
||||
pre15_own_guest_mount "$B27_MOUNT" "$label mount"
|
||||
}
|
||||
|
||||
reference=/root/B27-fixtures/source/payload.bin
|
||||
for codec in deflate lzma zstd; do
|
||||
attach_mount "$codec-valid.erofs" "$codec-valid"
|
||||
pre15_guest_ssh_bounded cmp "$B27_MOUNT/payload.bin" "$reference" || \
|
||||
pre15_dut_fail "$codec legal leading-padding read mismatch"
|
||||
for kind in tail truncated; do
|
||||
attach_mount "$codec-$kind.erofs" "$codec-$kind"
|
||||
pre15_guest_ssh_bounded /root/B27-stream-probe errno \
|
||||
"$B27_MOUNT/payload.bin" 97 || \
|
||||
pre15_dut_fail "$codec $kind did not return EINTEGRITY"
|
||||
done
|
||||
case "$codec" in
|
||||
deflate) logical_offset=137204; partial_size=3587 ;;
|
||||
lzma|zstd) logical_offset=0; partial_size=4096 ;;
|
||||
esac
|
||||
attach_mount "$codec-corrupt.erofs" "$codec-corrupt"
|
||||
pre15_guest_ssh_bounded /root/B27-stream-probe range \
|
||||
"$B27_MOUNT/payload.bin" "$reference" "$logical_offset" \
|
||||
"$partial_size" || \
|
||||
pre15_dut_fail "$codec range-before-corruption partial decode failed"
|
||||
pre15_guest_ssh_bounded /root/B27-stream-probe errno \
|
||||
"$B27_MOUNT/payload.bin" 97 || \
|
||||
pre15_dut_fail "$codec full corruption did not return EINTEGRITY"
|
||||
done
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/B27-dmesg.txt"
|
||||
printf '%s\n' \
|
||||
'TC176 B27 QEMU real LZMA/Deflate/Zstd stream-tail subset PASS' \
|
||||
'valid=3 trailing=3 truncated=3 partial-before-corruption=3 full-corruption=3 errno=97' \
|
||||
'Full feature suite NOT_RUN'
|
||||
Executable
+667
@@ -0,0 +1,667 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B28-partial.json
|
||||
generator=$fixture_dir/B28-partial-fixtures.py
|
||||
probe=$fixture_dir/B28-partial-probe.c
|
||||
kld_builder=$fixture_dir/B28-build-kld.sh
|
||||
gate_script=$PRE15_DUT/tests/pre15/gates/P15-086.sh
|
||||
gate_input=$PRE15_DUT/tests/pre15/gates/P15-086-input.json
|
||||
gate_result=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/result.json
|
||||
gate_capabilities=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/authorized-capabilities.json
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
first=$PRE15_CASE_TMP/fixtures-first
|
||||
second=$PRE15_CASE_TMP/fixtures-second
|
||||
action=${1:-runtime}
|
||||
|
||||
case "$action" in
|
||||
runtime|k2) ;;
|
||||
*) pre15_fail_usage "unknown B28 action: $action" ;;
|
||||
esac
|
||||
|
||||
if test "$action" = k2; then
|
||||
for tool in awk clang cmp diff file nm sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B28 K2 tool: $tool"
|
||||
done
|
||||
pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh"
|
||||
pre15_record_fixture b28-kld-builder "$kld_builder"
|
||||
for source in compress.h decompressor.c decompressor_lz4.c \
|
||||
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
|
||||
pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
mkdir -p "$artifacts"
|
||||
for config in 0 1; do
|
||||
module=$PRE15_CASE_TMP/B28-zstdio$config.ko
|
||||
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
||||
"$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work-$config" "$config" \
|
||||
>"$artifacts/B28-build-zstdio$config.stdout" \
|
||||
2>"$artifacts/B28-build-zstdio$config.stderr"; then
|
||||
pre15_dut_fail "B28 zstdio$config cross-target KLD build failed"
|
||||
fi
|
||||
file "$module" >"$artifacts/B28-zstdio$config-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B28-zstdio$config-sha256.txt"
|
||||
nm -g "$module" | LC_ALL=C sort \
|
||||
>"$artifacts/B28-zstdio$config-nm-global.txt"
|
||||
nm -u "$module" | LC_ALL=C sort \
|
||||
>"$artifacts/B28-zstdio$config-nm-u.txt"
|
||||
done
|
||||
awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio0-nm-global.txt" \
|
||||
>"$PRE15_CASE_TMP/B28-zstdio0-global-names.txt"
|
||||
awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio1-nm-global.txt" \
|
||||
>"$PRE15_CASE_TMP/B28-zstdio1-global-names.txt"
|
||||
if ! diff -u "$PRE15_CASE_TMP/B28-zstdio0-global-names.txt" \
|
||||
"$PRE15_CASE_TMP/B28-zstdio1-global-names.txt" \
|
||||
>"$artifacts/B28-config-nm-global.diff"; then
|
||||
pre15_dut_fail 'B28 K2 global symbols differ by ZSTDIO configuration'
|
||||
fi
|
||||
if ! grep -q ' z_erofs_decompress_supports_subextent$' \
|
||||
"$artifacts/B28-zstdio0-nm-global.txt"; then
|
||||
pre15_dut_fail 'B28 K2 partial capability symbol is absent'
|
||||
fi
|
||||
pre15_target_reached
|
||||
printf '%s\n' \
|
||||
'B28 targeted FreeBSD 15 K2 PASS' \
|
||||
'zstdio0/zstdio1 link, defined-global parity, optional Zstd ABI, and partial capability symbol PASS' \
|
||||
'KLD and object files removed by owned case cleanup'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in cc cmp diff file fsck.erofs git mkfs.erofs nm python3 sha256sum tar timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B28 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh"
|
||||
pre15_record_fixture b28-spec "$spec"
|
||||
pre15_record_fixture b28-generator "$generator"
|
||||
pre15_record_fixture b28-probe "$probe"
|
||||
pre15_record_fixture b28-kld-builder "$kld_builder"
|
||||
pre15_record_fixture b28-gate "$gate_script"
|
||||
pre15_record_fixture b28-gate-input "$gate_input"
|
||||
pre15_record_fixture b28-gate-result "$gate_result"
|
||||
pre15_record_fixture b28-gate-capabilities "$gate_capabilities"
|
||||
for source in compress.h decompressor.c decompressor_lz4.c \
|
||||
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
|
||||
pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \
|
||||
--output "$first" >"$artifacts/generate-first.stdout" \
|
||||
2>"$artifacts/generate-first.stderr"; then
|
||||
pre15_runner_fail 'B28 first real EROFS fixture generation failed'
|
||||
fi
|
||||
if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \
|
||||
--output "$second" >"$artifacts/generate-second.stdout" \
|
||||
2>"$artifacts/generate-second.stderr"; then
|
||||
pre15_runner_fail 'B28 second real EROFS fixture generation failed'
|
||||
fi
|
||||
if ! diff -ru "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
|
||||
pre15_runner_fail 'B28 real EROFS fixtures are not byte reproducible'
|
||||
fi
|
||||
cp "$first/fixture-index.json" "$artifacts/B28-fixture-index.json"
|
||||
|
||||
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
|
||||
pre15_target_reached
|
||||
else
|
||||
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" "$gate_result" \
|
||||
"$gate_capabilities" "$artifacts/B28-source-audit.json" \
|
||||
"$PRE15_CASE_TMP/decision-harness.c" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
spec_path = Path(sys.argv[3])
|
||||
gate_result_path = Path(sys.argv[4])
|
||||
gate_capabilities_path = Path(sys.argv[5])
|
||||
report_path = Path(sys.argv[6])
|
||||
harness_path = Path(sys.argv[7])
|
||||
spec = json.loads(spec_path.read_text(encoding="ascii"))
|
||||
baseline = spec["baseline"]
|
||||
implementation = "7a0d7e4a4047ac6d6130a3be5cb677cb94979fe7"
|
||||
runtime_base = "d09924362eb29819bd393e1e86602eb38c7608c6"
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(message)
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def committed(commit: str, name: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{commit}:repo-pre-15/src/{name}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
fail(f"cannot read B28 source {commit}:{name}: {completed.stderr.strip()}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def function(source: str, name: str) -> str:
|
||||
marker = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if marker is None:
|
||||
fail(f"missing function: {name}")
|
||||
name_line = source.rfind("\n", 0, marker.start()) + 1
|
||||
start = source.rfind("\n", 0, name_line - 1) + 1
|
||||
brace = source.find("{", marker.end())
|
||||
if brace < 0:
|
||||
fail(f"missing function body: {name}")
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
fail(f"unterminated function: {name}")
|
||||
|
||||
|
||||
if (
|
||||
spec.get("schema") != 1
|
||||
or spec.get("batch") != "B28"
|
||||
or spec.get("candidate") != "P15-086"
|
||||
or spec.get("test") != "TC176-stream-runtime"
|
||||
):
|
||||
fail("B28 fixture identity changed")
|
||||
for name, commit in (
|
||||
("baseline", baseline),
|
||||
("implementation", implementation),
|
||||
("runtime base", runtime_base),
|
||||
):
|
||||
if subprocess.check_output(
|
||||
["git", "-C", str(root), "rev-parse", commit], text=True
|
||||
).strip() != commit:
|
||||
fail(f"B28 {name} identity changed")
|
||||
gate = json.loads(gate_result_path.read_text(encoding="ascii"))
|
||||
capabilities = json.loads(gate_capabilities_path.read_text(encoding="ascii"))
|
||||
if (
|
||||
gate.get("status") != "GO"
|
||||
or gate.get("g04") != "GO"
|
||||
or gate.get("g05") != "GO"
|
||||
or gate.get("b28") != "AUTHORIZED"
|
||||
or gate.get("resolved_base") != "6bf5724619be70f805bbe7d1ba77dd70cdced69f"
|
||||
):
|
||||
fail("frozen P15-086 gate no longer authorizes B28")
|
||||
if capabilities != {
|
||||
"full_fallback": ["zstd"],
|
||||
"partial": ["deflate", "lz4", "lzma"],
|
||||
"persistent_state_added": False,
|
||||
"schema": 1,
|
||||
}:
|
||||
fail("P15-086 per-codec authorization changed")
|
||||
for name, expected in (
|
||||
("gate script", spec["gate"]["script_sha256"]),
|
||||
("gate input", spec["gate"]["input_sha256"]),
|
||||
("gate result", spec["gate"]["result_sha256"]),
|
||||
):
|
||||
path = {
|
||||
"gate script": dut / "tests/pre15/gates/P15-086.sh",
|
||||
"gate input": dut / "tests/pre15/gates/P15-086-input.json",
|
||||
"gate result": gate_result_path,
|
||||
}[name]
|
||||
if sha256_path(path) != expected:
|
||||
fail(f"{name} hash changed")
|
||||
|
||||
source_names = (
|
||||
"compress.h",
|
||||
"decompressor.c",
|
||||
"decompressor_lz4.c",
|
||||
"decompressor_lzma.c",
|
||||
"decompressor_deflate.c",
|
||||
"decompressor_zstd.c",
|
||||
"zdata.c",
|
||||
)
|
||||
baseline_sources = {name: committed(baseline, name) for name in source_names}
|
||||
implemented_sources = {name: committed(implementation, name) for name in source_names}
|
||||
sources = {
|
||||
name: (dut / "src" / name).read_text(encoding="utf-8")
|
||||
for name in source_names
|
||||
}
|
||||
if implemented_sources["decompressor_lz4.c"] != baseline_sources["decompressor_lz4.c"]:
|
||||
fail("B28 changed the already-capable LZ4 backend implementation")
|
||||
if "bool supports_subextent;" not in implemented_sources["compress.h"]:
|
||||
fail("B28 descriptor capability is absent")
|
||||
if implemented_sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1:
|
||||
fail("B28 capability prototype count changed")
|
||||
if "bool supports_subextent;" not in sources["compress.h"]:
|
||||
fail("current tree lost the B28 descriptor capability")
|
||||
if sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1:
|
||||
fail("current B28 capability prototype count changed")
|
||||
|
||||
def read_descriptor_decisions(source_set: dict[str, str]) -> dict[str, bool]:
|
||||
decisions = {}
|
||||
for codec in ("lzma", "deflate", "zstd"):
|
||||
value = re.search(
|
||||
rf"const struct z_erofs_decompressor z_erofs_{codec}_decomp = \{{.*?\.supports_subextent = ([01]),",
|
||||
source_set[f"decompressor_{codec}.c"],
|
||||
re.DOTALL,
|
||||
)
|
||||
if value is None:
|
||||
fail(f"missing {codec} descriptor capability")
|
||||
decisions[codec] = value.group(1) == "1"
|
||||
lz4 = re.search(
|
||||
r"static const struct z_erofs_decompressor z_erofs_lz4_decomp = \{.*?\.supports_subextent = ([01]),",
|
||||
source_set["decompressor.c"],
|
||||
re.DOTALL,
|
||||
)
|
||||
if lz4 is None:
|
||||
fail("missing LZ4 descriptor capability")
|
||||
decisions["lz4"] = lz4.group(1) == "1"
|
||||
return decisions
|
||||
|
||||
|
||||
implemented_decisions = read_descriptor_decisions(implemented_sources)
|
||||
if implemented_decisions != {
|
||||
"lz4": True, "lzma": True, "deflate": True, "zstd": False
|
||||
}:
|
||||
fail(f"B28 implementation decisions differ from G04/G05: {implemented_decisions}")
|
||||
descriptor_decisions = read_descriptor_decisions(sources)
|
||||
if descriptor_decisions != {
|
||||
"lz4": True, "lzma": True, "deflate": True, "zstd": False
|
||||
}:
|
||||
fail(f"current partial descriptor decisions changed: {descriptor_decisions}")
|
||||
expected_zstd = committed(runtime_base, "decompressor_zstd.c")
|
||||
if function(sources["decompressor_zstd.c"], "z_erofs_zstd_finish") != \
|
||||
function(expected_zstd, "z_erofs_zstd_finish"):
|
||||
fail("current Zstd completion policy differs from the accepted transform")
|
||||
|
||||
unchanged_zdata = (
|
||||
"z_erofs_extent_cache_match",
|
||||
"z_erofs_extent_cache_copy",
|
||||
"z_erofs_extent_cache_publish",
|
||||
"z_erofs_extent_cache_init",
|
||||
"z_erofs_extent_cache_fini",
|
||||
"z_erofs_extent_cache_eligible",
|
||||
"z_erofs_read_data",
|
||||
"z_erofs_read_uio",
|
||||
)
|
||||
baseline_zdata = baseline_sources["zdata.c"]
|
||||
for name in unchanged_zdata:
|
||||
if function(implemented_sources["zdata.c"], name) != function(baseline_zdata, name):
|
||||
fail(f"B28 changed unrelated zdata function {name}")
|
||||
implemented_decode_length = function(implemented_sources["zdata.c"], "z_erofs_decode_length")
|
||||
implemented_read_extent = function(implemented_sources["zdata.c"], "z_erofs_read_extent")
|
||||
implemented_do_read = function(implemented_sources["zdata.c"], "z_erofs_do_read")
|
||||
decode_length = function(sources["zdata.c"], "z_erofs_decode_length")
|
||||
decode_extent = function(sources["zdata.c"], "z_erofs_decode_extent")
|
||||
do_read = function(sources["zdata.c"], "z_erofs_do_read")
|
||||
if decode_length != implemented_decode_length:
|
||||
fail("current tree changed the B28 decode-length transform")
|
||||
if decode_extent[decode_extent.index("{") :] != implemented_read_extent[
|
||||
implemented_read_extent.index("{") :
|
||||
]:
|
||||
fail("current tree changed B28 provider or decoded-buffer ownership")
|
||||
for anchor in (
|
||||
"z_erofs_decompress_supports_subextent(map)",
|
||||
"*partial = (map->m_flags & EROFS_MAP_PARTIAL_REF) != 0;",
|
||||
"*decoded_len = end;",
|
||||
"*partial = true;",
|
||||
):
|
||||
if anchor not in implemented_decode_length:
|
||||
fail(f"B28 decode-length anchor missing: {anchor}")
|
||||
for anchor in (
|
||||
"erofs_read_metadata(",
|
||||
"erofs_read_physical(",
|
||||
"erofs_put_metabuf(&buf);",
|
||||
"erofs_brelse(compressed);",
|
||||
"free(decoded, M_EROFS);",
|
||||
"*bufp = decoded;",
|
||||
):
|
||||
if implemented_read_extent.count(anchor) != function(baseline_zdata, "z_erofs_read_extent").count(anchor):
|
||||
fail(f"B28 changed provider or cleanup count: {anchor}")
|
||||
if not (
|
||||
implemented_do_read.index("z_erofs_extent_cache_copy")
|
||||
< implemented_do_read.index("z_erofs_decode_length")
|
||||
< implemented_do_read.index("z_erofs_read_extent")
|
||||
< implemented_do_read.index("cache_eligible && !partial")
|
||||
< implemented_do_read.index("z_erofs_extent_cache_publish")
|
||||
):
|
||||
fail("B28 cache/decode/publication order changed")
|
||||
if "partial, &decoded" not in implemented_do_read:
|
||||
fail("B28 does not pass the selected partial mode to extent decode")
|
||||
if not (
|
||||
do_read.index("z_erofs_decode_length")
|
||||
< do_read.index("cache_eligible = !partial")
|
||||
< do_read.index("z_erofs_decode_extent")
|
||||
):
|
||||
fail("current tree no longer excludes partial decodes from cache admission")
|
||||
if "partial, &decoded" not in do_read:
|
||||
fail("current tree does not pass B28 partial mode to extent decode")
|
||||
|
||||
changed = set(
|
||||
subprocess.check_output(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(root),
|
||||
"diff-tree",
|
||||
"--no-commit-id",
|
||||
"--name-only",
|
||||
"-r",
|
||||
implementation,
|
||||
],
|
||||
text=True,
|
||||
).splitlines()
|
||||
)
|
||||
expected_write_set = {
|
||||
"repo-pre-15/src/compress.h",
|
||||
"repo-pre-15/src/decompressor.c",
|
||||
"repo-pre-15/src/decompressor_deflate.c",
|
||||
"repo-pre-15/src/decompressor_lzma.c",
|
||||
"repo-pre-15/src/decompressor_zstd.c",
|
||||
"repo-pre-15/src/zdata.c",
|
||||
"repo-pre-15/tests/pre15/cases/B28-partial.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B28-build-kld.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B28-partial-fixtures.py",
|
||||
"repo-pre-15/tests/pre15/fixtures/B28-partial-probe.c",
|
||||
"repo-pre-15/tests/pre15/fixtures/B28-partial.json",
|
||||
}
|
||||
if changed != expected_write_set:
|
||||
fail(f"B28 exact write set differs: {sorted(changed ^ expected_write_set)}")
|
||||
if any(re.search(r"return\s*\(\s*-E[A-Z0-9_]+", source) for source in sources.values()):
|
||||
fail("negative Linux errno entered B28")
|
||||
|
||||
harness = r'''#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#define EINTEGRITY 97
|
||||
#define EOVERFLOW 84
|
||||
#define EROFS_MAP_PARTIAL_REF 0x20
|
||||
typedef uint64_t erofs_off_t;
|
||||
struct erofs_map_blocks {
|
||||
uint64_t m_llen;
|
||||
unsigned int m_flags;
|
||||
unsigned int m_algorithmformat;
|
||||
};
|
||||
static bool z_erofs_decompress_supports_subextent(const struct erofs_map_blocks *map)
|
||||
{
|
||||
return map->m_algorithmformat <= 2;
|
||||
}
|
||||
''' + decode_length + r'''
|
||||
static int failures;
|
||||
static void check(const char *name, struct erofs_map_blocks map, uint64_t off,
|
||||
size_t want, int expected_error, size_t expected_length, bool expected_partial)
|
||||
{
|
||||
size_t length = 0;
|
||||
bool partial = false;
|
||||
int error = z_erofs_decode_length(&map, off, want, &length, &partial);
|
||||
if (error != expected_error || (!error &&
|
||||
(length != expected_length || partial != expected_partial))) {
|
||||
fprintf(stderr, "%s error=%d length=%zu partial=%d\n", name,
|
||||
error, length, partial);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
check("lz4 prefix", (struct erofs_map_blocks){1038906, 0, 0}, 0,
|
||||
4096, 0, 4096, true);
|
||||
check("lzma middle", (struct erofs_map_blocks){151552, 0, 1}, 8192,
|
||||
4096, 0, 12288, true);
|
||||
check("deflate cross", (struct erofs_map_blocks){22878, 0, 2}, 3584,
|
||||
4096, 0, 7680, true);
|
||||
check("zstd prefix", (struct erofs_map_blocks){151552, 0, 3}, 0,
|
||||
4096, 0, 151552, false);
|
||||
check("lzma tail", (struct erofs_map_blocks){151552, 0, 1}, 147456,
|
||||
4096, 0, 151552, false);
|
||||
check("zstd partial ref", (struct erofs_map_blocks){151552,
|
||||
EROFS_MAP_PARTIAL_REF, 3}, 0, 4096, 0, 4096, true);
|
||||
check("shifted fallback", (struct erofs_map_blocks){65536, 0, 4}, 0,
|
||||
4096, 0, 65536, false);
|
||||
check("bad range", (struct erofs_map_blocks){4096, 0, 0}, 4090,
|
||||
16, EINTEGRITY, 0, false);
|
||||
return failures != 0;
|
||||
}
|
||||
'''
|
||||
harness_path.write_text(harness, encoding="ascii")
|
||||
report = {
|
||||
"baseline": baseline,
|
||||
"batch": "B28",
|
||||
"cache_hit_before_decode": True,
|
||||
"candidate": "P15-086",
|
||||
"descriptor_decisions": descriptor_decisions,
|
||||
"frozen_gate_full_fallback": ["zstd"],
|
||||
"current_full_fallback": ["shifted", "interlaced", "unknown", "zstd"],
|
||||
"current_partial": ["deflate", "lz4", "lzma"],
|
||||
"gate": "GO",
|
||||
"implementation": implementation,
|
||||
"partial_cache_publication": False,
|
||||
"partial_reference_preserved": True,
|
||||
"persistent_state_added": False,
|
||||
"positive_errno": True,
|
||||
"provider_and_cleanup_unchanged": True,
|
||||
"runtime_base": runtime_base,
|
||||
"status": "PASS",
|
||||
"test": "TC176-stream-runtime",
|
||||
"write_set": sorted(changed),
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
PY
|
||||
then
|
||||
pre15_runner_fail 'B28 gate/source/write-set audit failed'
|
||||
fi
|
||||
|
||||
if ! cc -std=c11 -Wall -Wextra -Werror "$PRE15_CASE_TMP/decision-harness.c" \
|
||||
-o "$PRE15_CASE_TMP/decision-harness" \
|
||||
>"$artifacts/decision-harness-build.stdout" \
|
||||
2>"$artifacts/decision-harness-build.stderr" || \
|
||||
! "$PRE15_CASE_TMP/decision-harness" \
|
||||
>"$artifacts/decision-harness.stdout" \
|
||||
2>"$artifacts/decision-harness.stderr"; then
|
||||
pre15_dut_fail 'B28 extracted decode-length harness failed'
|
||||
fi
|
||||
cp "$gate_result" "$artifacts/P15-086-result.json"
|
||||
cp "$gate_capabilities" "$artifacts/P15-086-capabilities.json"
|
||||
sha256sum "$artifacts/P15-086-result.json" \
|
||||
"$artifacts/P15-086-capabilities.json" \
|
||||
"$artifacts/B28-fixture-index.json" \
|
||||
"$artifacts/B28-source-audit.json" >"$artifacts/SHA256SUMS"
|
||||
fi
|
||||
pre15_target_reached
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'TC176 B28 host partial-subextent subset PASS' \
|
||||
'B28 LZ4/LZMA/Deflate partial policy and Zstd full fallback, cache exclusion, arithmetic, real fixtures, and cleanup PASS' \
|
||||
'QEMU runtime NOT_RUN in host mode' \
|
||||
'Full feature suite NOT_RUN'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in awk clang scp; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B28 QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B28-fixtures.tar.gz
|
||||
module=$PRE15_CASE_TMP/B28-erofs-zstdio1.ko
|
||||
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work" 1 >"$artifacts/B28-kld-build.stdout" \
|
||||
2>"$artifacts/B28-kld-build.stderr"; then
|
||||
pre15_dut_fail 'B28 cross-target zstdio1 KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/B28-kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/B28-kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/B28-kld-nm-u.txt"
|
||||
tar -C "$first" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
pre15_guest_ssh_bounded()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-p "$PRE15_QEMU_SSH_PORT" \
|
||||
"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B28-fixtures.tar.gz || \
|
||||
! pre15_scp "$probe" /root/B28-partial-probe.c || \
|
||||
! pre15_scp "$module" /root/B28-erofs-zstdio1.ko; then
|
||||
pre15_infra_blocked 'could not transfer B28 module, probe, or fixtures'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/B28-fixtures && mkdir /root/B28-fixtures && tar -xzf /root/B28-fixtures.tar.gz -C /root/B28-fixtures && cc -O2 -Wall -Wextra -Werror -o /root/B28-partial-probe /root/B28-partial-probe.c'; then
|
||||
pre15_infra_blocked 'could not prepare B28 guest fixtures/probe'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B28-erofs-zstdio1.ko \
|
||||
>"$artifacts/B28-kldload.stdout" 2>"$artifacts/B28-kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' \
|
||||
"$artifacts/B28-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
if grep -Eq 'link_elf|symbol|not defined' "$artifacts/B28-kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel lacks the planned ZSTDIO provider ABI'
|
||||
fi
|
||||
pre15_dut_fail 'B28 exact-source zstdio1 KLD failed to load'
|
||||
fi
|
||||
pre15_own_guest_kld B28-erofs-zstdio1.ko 'B28 exact-source KLD'
|
||||
|
||||
attach_mount()
|
||||
{
|
||||
image=$1
|
||||
label=$2
|
||||
B28_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "/root/B28-fixtures/images/$image") || \
|
||||
pre15_dut_fail "$label md attach failed"
|
||||
case "$B28_MD" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected B28 md unit: $B28_MD" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$B28_MD" "$label md"
|
||||
B28_MOUNT=/mnt/pre15-b28-$label
|
||||
pre15_guest_ssh_bounded mkdir -p "$B28_MOUNT"
|
||||
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B28_MD" "$B28_MOUNT" || \
|
||||
pre15_dut_fail "$label mount failed"
|
||||
pre15_own_guest_mount "$B28_MOUNT" "$label mount"
|
||||
}
|
||||
|
||||
valid_assertions=0
|
||||
corrupt_range_assertions=0
|
||||
full_corruption_assertions=0
|
||||
while IFS='|' read -r codec source logical_offset logical_length decision; do
|
||||
reference=/root/B28-fixtures/sources/$source/payload.bin
|
||||
attach_mount "$codec-valid.erofs" "$codec-valid"
|
||||
for range in '0 4096' '3584 4096' '8192 4096'; do
|
||||
set -- $range
|
||||
offset=$((logical_offset + $1))
|
||||
pre15_guest_ssh_bounded /root/B28-partial-probe range \
|
||||
"$B28_MOUNT/payload.bin" "$reference" "$offset" "$2" || \
|
||||
pre15_dut_fail "$codec valid subextent mismatch at $offset"
|
||||
valid_assertions=$((valid_assertions + 1))
|
||||
done
|
||||
tail_offset=$((logical_offset + logical_length - 4096))
|
||||
pre15_guest_ssh_bounded /root/B28-partial-probe range \
|
||||
"$B28_MOUNT/payload.bin" "$reference" "$tail_offset" 4096 || \
|
||||
pre15_dut_fail "$codec valid tail fallback mismatch"
|
||||
valid_assertions=$((valid_assertions + 1))
|
||||
|
||||
attach_mount "$codec-corrupt.erofs" "$codec-corrupt"
|
||||
if test "$decision" = GO; then
|
||||
pre15_guest_ssh_bounded /root/B28-partial-probe range \
|
||||
"$B28_MOUNT/payload.bin" "$reference" "$logical_offset" 4096 || \
|
||||
pre15_dut_fail "$codec range-before-corruption partial decode failed"
|
||||
else
|
||||
pre15_guest_ssh_bounded /root/B28-partial-probe range-errno \
|
||||
"$B28_MOUNT/payload.bin" "$logical_offset" 4096 97 || \
|
||||
pre15_dut_fail "$codec did not use exact full fallback"
|
||||
fi
|
||||
corrupt_range_assertions=$((corrupt_range_assertions + 1))
|
||||
pre15_guest_ssh_bounded /root/B28-partial-probe full-errno \
|
||||
"$B28_MOUNT/payload.bin" 97 || \
|
||||
pre15_dut_fail "$codec full corruption did not return EINTEGRITY"
|
||||
full_corruption_assertions=$((full_corruption_assertions + 1))
|
||||
done <<'EOF'
|
||||
deflate|stream|114326|22878|GO
|
||||
lz4|lz4|0|1038906|GO
|
||||
lzma|stream|0|151552|GO
|
||||
zstd|stream|0|151552|FULL_FALLBACK
|
||||
EOF
|
||||
if test "$valid_assertions" -ne 16 || \
|
||||
test "$corrupt_range_assertions" -ne 4 || \
|
||||
test "$full_corruption_assertions" -ne 4; then
|
||||
pre15_runner_fail "B28 assertion count mismatch: valid=$valid_assertions corrupt-range=$corrupt_range_assertions full-corruption=$full_corruption_assertions"
|
||||
fi
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/B28-dmesg.txt"
|
||||
printf '%s\n' \
|
||||
'TC176 B28 QEMU partial-subextent subset PASS' \
|
||||
'valid-ranges=16 partial-before-corruption=4 full-corruption=4 errno=97' \
|
||||
'Full feature suite NOT_RUN'
|
||||
File diff suppressed because it is too large
Load Diff
Executable
+448
@@ -0,0 +1,448 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=1b4b8abc249d9bbbb75e19b5259c5f46f91909ef
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B30 host tool: $tool"
|
||||
done
|
||||
|
||||
for source in internal.h compress.h decompressor.c decompressor_lz4.c \
|
||||
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c zmap.c; do
|
||||
pre15_record_fixture "b30-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
pre15_record_fixture b30-linux-compress "$PRE15_ROOT/src-linux/compress.h"
|
||||
pre15_record_fixture b30-linux-decompressor "$PRE15_ROOT/src-linux/decompressor.c"
|
||||
pre15_record_fixture b30-case \
|
||||
"$PRE15_DUT/tests/pre15/cases/B30-codec-structure.sh"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
|
||||
"$artifacts/B30-codec-structure.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
report_path = Path(sys.argv[4])
|
||||
src = dut / "src"
|
||||
|
||||
|
||||
def committed(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B30 baseline {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def replace_once(source: str, old: str, new: str, label: str) -> str:
|
||||
count = source.count(old)
|
||||
if count != 1:
|
||||
raise SystemExit(f"B30 baseline anchor count for {label}: {count}")
|
||||
return source.replace(old, new, 1)
|
||||
|
||||
|
||||
expected_changed = {
|
||||
"repo-pre-15/src/compress.h",
|
||||
"repo-pre-15/src/decompressor.c",
|
||||
"repo-pre-15/src/decompressor_deflate.c",
|
||||
"repo-pre-15/src/decompressor_lzma.c",
|
||||
"repo-pre-15/src/decompressor_zstd.c",
|
||||
"repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/zdata.c",
|
||||
"repo-pre-15/tests/pre15/cases/B30-codec-structure.sh",
|
||||
}
|
||||
changed = subprocess.run(
|
||||
["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
changed.extend(
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(root),
|
||||
"ls-files",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
"--",
|
||||
"repo-pre-15",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
if set(changed) != expected_changed or len(changed) != len(expected_changed):
|
||||
raise SystemExit(f"B30 repo-pre-15 write set mismatch: {changed!r}")
|
||||
|
||||
current = {
|
||||
path: (src / path).read_text(encoding="utf-8")
|
||||
for path in (
|
||||
"internal.h",
|
||||
"compress.h",
|
||||
"decompressor.c",
|
||||
"decompressor_lz4.c",
|
||||
"decompressor_lzma.c",
|
||||
"decompressor_deflate.c",
|
||||
"decompressor_zstd.c",
|
||||
"zdata.c",
|
||||
"zmap.c",
|
||||
)
|
||||
}
|
||||
before = {
|
||||
path: committed(f"src/{path}")
|
||||
for path in current
|
||||
}
|
||||
|
||||
expected = before["internal.h"]
|
||||
expected = replace_once(
|
||||
expected,
|
||||
"struct erofs_sb_lz4_info {\n"
|
||||
"\tuint16_t max_distance_pages;\n"
|
||||
"\tuint16_t max_pclusterblks;\n"
|
||||
"};\n\n",
|
||||
"",
|
||||
"LZ4 state type",
|
||||
)
|
||||
expected = replace_once(
|
||||
expected,
|
||||
"\tstruct erofs_sb_lz4_info lz4;\n",
|
||||
"",
|
||||
"LZ4 state member",
|
||||
)
|
||||
if current["internal.h"] != expected:
|
||||
raise SystemExit("internal.h differs from exact B30 dead-state removal")
|
||||
|
||||
expected = before["compress.h"]
|
||||
expected = replace_once(
|
||||
expected,
|
||||
"struct z_erofs_decompressor {\n"
|
||||
"\tconst char *name;\n"
|
||||
"\tbool supports_subextent;\n"
|
||||
"\t/* Callbacks return zero or a positive FreeBSD errno. */\n"
|
||||
"\tint (*config)(struct erofs_sb_info *, const struct erofs_super_block *,\n"
|
||||
"\t const void *, size_t);\n"
|
||||
"\tint (*decompress)(const struct z_erofs_decompress_req *);\n"
|
||||
"};",
|
||||
"struct z_erofs_decompressor {\n"
|
||||
"\t/* Callbacks return zero or a positive FreeBSD errno. */\n"
|
||||
"\tint (*config)(struct erofs_sb_info *, const struct erofs_super_block *,\n"
|
||||
"\t const void *, size_t);\n"
|
||||
"\tint (*decompress)(const struct z_erofs_decompress_req *);\n"
|
||||
"\tbool supports_subextent;\n"
|
||||
"\tconst char *name;\n"
|
||||
"};",
|
||||
"descriptor fields",
|
||||
)
|
||||
if current["compress.h"] != expected:
|
||||
raise SystemExit("compress.h differs from exact B30 descriptor alignment")
|
||||
|
||||
expected = before["decompressor.c"]
|
||||
for old, new, label in (
|
||||
("\t\tdistance = le16toh(lz4->max_distance);\n", "", "unused cfg distance"),
|
||||
("\t\tmax_pclusterblks = 1;\n\t\tsbi->available_compr_algs", "\t\tsbi->available_compr_algs", "legacy dead local store"),
|
||||
(
|
||||
"\tsbi->lz4.max_pclusterblks = max_pclusterblks;\n"
|
||||
"\tsbi->lz4.max_distance_pages = distance != 0 ?\n"
|
||||
"\t howmany(distance, PAGE_SIZE) + 1 :\n"
|
||||
"\t howmany(UINT16_MAX, PAGE_SIZE) + 1;\n",
|
||||
"",
|
||||
"runtime LZ4 stores",
|
||||
),
|
||||
(
|
||||
"static const struct z_erofs_decompressor z_erofs_shifted_decomp = {\n"
|
||||
"\t.name = \"shifted\",\n"
|
||||
"\t.decompress = z_erofs_transform_plain,\n"
|
||||
"};",
|
||||
"static const struct z_erofs_decompressor z_erofs_shifted_decomp = {\n"
|
||||
"\t.decompress = z_erofs_transform_plain,\n"
|
||||
"\t.name = \"shifted\",\n"
|
||||
"};",
|
||||
"shifted descriptor",
|
||||
),
|
||||
(
|
||||
"static const struct z_erofs_decompressor z_erofs_interlaced_decomp = {\n"
|
||||
"\t.name = \"interlaced\",\n"
|
||||
"\t.decompress = z_erofs_transform_plain,\n"
|
||||
"};",
|
||||
"static const struct z_erofs_decompressor z_erofs_interlaced_decomp = {\n"
|
||||
"\t.decompress = z_erofs_transform_plain,\n"
|
||||
"\t.name = \"interlaced\",\n"
|
||||
"};",
|
||||
"interlaced descriptor",
|
||||
),
|
||||
(
|
||||
"static const struct z_erofs_decompressor z_erofs_lz4_decomp = {\n"
|
||||
"\t.name = \"lz4\",\n"
|
||||
"\t.supports_subextent = 1,\n"
|
||||
"\t.config = z_erofs_load_lz4_config,\n"
|
||||
"\t.decompress = z_erofs_lz4_decompress,\n"
|
||||
"};",
|
||||
"static const struct z_erofs_decompressor z_erofs_lz4_decomp = {\n"
|
||||
"\t.config = z_erofs_load_lz4_config,\n"
|
||||
"\t.decompress = z_erofs_lz4_decompress,\n"
|
||||
"\t.supports_subextent = 1,\n"
|
||||
"\t.name = \"lz4\",\n"
|
||||
"};",
|
||||
"LZ4 descriptor",
|
||||
),
|
||||
(
|
||||
"static const struct z_erofs_decompressor * const z_erofs_decomp[] = {\n"
|
||||
"\t[Z_EROFS_COMPRESSION_LZ4] = &z_erofs_lz4_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_LZMA] = &z_erofs_lzma_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_DEFLATE] = &z_erofs_deflate_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_ZSTD] = &z_erofs_zstd_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_SHIFTED] = &z_erofs_shifted_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_INTERLACED] = &z_erofs_interlaced_decomp,\n"
|
||||
"};",
|
||||
"static const struct z_erofs_decompressor * const z_erofs_decomp[] = {\n"
|
||||
"\t[Z_EROFS_COMPRESSION_SHIFTED] = &z_erofs_shifted_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_INTERLACED] = &z_erofs_interlaced_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_LZ4] = &z_erofs_lz4_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_LZMA] = &z_erofs_lzma_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_DEFLATE] = &z_erofs_deflate_decomp,\n"
|
||||
"\t[Z_EROFS_COMPRESSION_ZSTD] = &z_erofs_zstd_decomp,\n"
|
||||
"};",
|
||||
"decompressor table",
|
||||
),
|
||||
):
|
||||
expected = replace_once(expected, old, new, label)
|
||||
if current["decompressor.c"] != expected:
|
||||
raise SystemExit("decompressor.c differs from exact B30 structural edits")
|
||||
|
||||
for path, name, capability in (
|
||||
("decompressor_lzma.c", "lzma", "1"),
|
||||
("decompressor_deflate.c", "deflate", "1"),
|
||||
("decompressor_zstd.c", "zstd", "0"),
|
||||
):
|
||||
expected = before[path]
|
||||
expected = replace_once(
|
||||
expected,
|
||||
f"const struct z_erofs_decompressor z_erofs_{name}_decomp = {{\n"
|
||||
f"\t.name = \"{name}\",\n"
|
||||
f"\t.supports_subextent = {capability},\n"
|
||||
f"\t.config = z_erofs_load_{name}_config,\n"
|
||||
f"\t.decompress = z_erofs_{name}_decompress,\n"
|
||||
"};",
|
||||
f"const struct z_erofs_decompressor z_erofs_{name}_decomp = {{\n"
|
||||
f"\t.config = z_erofs_load_{name}_config,\n"
|
||||
f"\t.decompress = z_erofs_{name}_decompress,\n"
|
||||
f"\t.supports_subextent = {capability},\n"
|
||||
f"\t.name = \"{name}\",\n"
|
||||
"};",
|
||||
f"{name} descriptor",
|
||||
)
|
||||
if current[path] != expected:
|
||||
raise SystemExit(f"{path} differs from exact B30 descriptor reorder")
|
||||
|
||||
if current["decompressor_lz4.c"] != before["decompressor_lz4.c"]:
|
||||
raise SystemExit("B30 changed the independent FreeBSD LZ4 backend")
|
||||
expected = before["zdata.c"]
|
||||
if expected.count("z_erofs_read_extent") != 2:
|
||||
raise SystemExit("B30 zdata baseline rename denominator changed")
|
||||
expected = expected.replace("z_erofs_read_extent", "z_erofs_decode_extent")
|
||||
if current["zdata.c"] != expected:
|
||||
raise SystemExit("zdata.c differs from the exact two-token decode rename")
|
||||
if current["zmap.c"] != before["zmap.c"]:
|
||||
raise SystemExit("B30 changed the zmap record reader")
|
||||
|
||||
descriptor_markers = [
|
||||
"int (*config)(",
|
||||
"int (*decompress)(",
|
||||
"bool supports_subextent;",
|
||||
"const char *name;",
|
||||
]
|
||||
positions = [current["compress.h"].index(marker) for marker in descriptor_markers]
|
||||
if positions != sorted(positions):
|
||||
raise SystemExit("FreeBSD descriptor field order drifted")
|
||||
|
||||
|
||||
def initializer_fields(source: str, object_name: str) -> list[tuple[str, str]]:
|
||||
match = re.search(
|
||||
rf"(?:static )?const struct z_erofs_decompressor {object_name} = \{{\n"
|
||||
rf"(?P<body>.*?)\n\}};",
|
||||
source,
|
||||
re.DOTALL,
|
||||
)
|
||||
if match is None:
|
||||
raise SystemExit(f"missing descriptor initializer: {object_name}")
|
||||
return re.findall(r"^\s*\.([a-z_]+)\s*=\s*([^,]+),$", match.group("body"), re.MULTILINE)
|
||||
|
||||
|
||||
descriptors = {
|
||||
"shifted": initializer_fields(current["decompressor.c"], "z_erofs_shifted_decomp"),
|
||||
"interlaced": initializer_fields(current["decompressor.c"], "z_erofs_interlaced_decomp"),
|
||||
"lz4": initializer_fields(current["decompressor.c"], "z_erofs_lz4_decomp"),
|
||||
"lzma": initializer_fields(current["decompressor_lzma.c"], "z_erofs_lzma_decomp"),
|
||||
"deflate": initializer_fields(current["decompressor_deflate.c"], "z_erofs_deflate_decomp"),
|
||||
"zstd": initializer_fields(current["decompressor_zstd.c"], "z_erofs_zstd_decomp"),
|
||||
}
|
||||
expected_descriptors = {
|
||||
"shifted": [("decompress", "z_erofs_transform_plain"), ("name", '"shifted"')],
|
||||
"interlaced": [("decompress", "z_erofs_transform_plain"), ("name", '"interlaced"')],
|
||||
"lz4": [
|
||||
("config", "z_erofs_load_lz4_config"),
|
||||
("decompress", "z_erofs_lz4_decompress"),
|
||||
("supports_subextent", "1"),
|
||||
("name", '"lz4"'),
|
||||
],
|
||||
"lzma": [
|
||||
("config", "z_erofs_load_lzma_config"),
|
||||
("decompress", "z_erofs_lzma_decompress"),
|
||||
("supports_subextent", "1"),
|
||||
("name", '"lzma"'),
|
||||
],
|
||||
"deflate": [
|
||||
("config", "z_erofs_load_deflate_config"),
|
||||
("decompress", "z_erofs_deflate_decompress"),
|
||||
("supports_subextent", "1"),
|
||||
("name", '"deflate"'),
|
||||
],
|
||||
"zstd": [
|
||||
("config", "z_erofs_load_zstd_config"),
|
||||
("decompress", "z_erofs_zstd_decompress"),
|
||||
("supports_subextent", "0"),
|
||||
("name", '"zstd"'),
|
||||
],
|
||||
}
|
||||
if descriptors != expected_descriptors:
|
||||
raise SystemExit(f"backend descriptor initializer mismatch: {descriptors!r}")
|
||||
|
||||
table_match = re.search(
|
||||
r"static const struct z_erofs_decompressor \* const z_erofs_decomp\[\] = \{\n"
|
||||
r"(?P<body>.*?)\n\};",
|
||||
current["decompressor.c"],
|
||||
re.DOTALL,
|
||||
)
|
||||
if table_match is None:
|
||||
raise SystemExit("missing FreeBSD decompressor table")
|
||||
table_order = re.findall(r"\[(Z_EROFS_COMPRESSION_[A-Z0-9_]+)\]", table_match.group("body"))
|
||||
expected_order = [
|
||||
"Z_EROFS_COMPRESSION_SHIFTED",
|
||||
"Z_EROFS_COMPRESSION_INTERLACED",
|
||||
"Z_EROFS_COMPRESSION_LZ4",
|
||||
"Z_EROFS_COMPRESSION_LZMA",
|
||||
"Z_EROFS_COMPRESSION_DEFLATE",
|
||||
"Z_EROFS_COMPRESSION_ZSTD",
|
||||
]
|
||||
if table_order != expected_order:
|
||||
raise SystemExit(f"FreeBSD decompressor table order mismatch: {table_order!r}")
|
||||
linux_decompressor = (root / "src-linux/decompressor.c").read_text(encoding="utf-8")
|
||||
linux_table = linux_decompressor[linux_decompressor.index("z_erofs_decomp[] = {") :]
|
||||
linux_order = re.findall(r"\[(Z_EROFS_COMPRESSION_[A-Z0-9_]+)\]", linux_table)[:6]
|
||||
if linux_order != expected_order:
|
||||
raise SystemExit(f"Linux decompressor table anchor drifted: {linux_order!r}")
|
||||
|
||||
callbacks = []
|
||||
for path in sorted(src.glob("decompressor*.c")):
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
match = re.match(r"\s*\.(config|decompress)\s*=\s*([^,]+),", line)
|
||||
if match:
|
||||
callbacks.append((path.name, match.group(1), match.group(2)))
|
||||
expected_callbacks = [
|
||||
("decompressor.c", "decompress", "z_erofs_transform_plain"),
|
||||
("decompressor.c", "decompress", "z_erofs_transform_plain"),
|
||||
("decompressor.c", "config", "z_erofs_load_lz4_config"),
|
||||
("decompressor.c", "decompress", "z_erofs_lz4_decompress"),
|
||||
("decompressor_deflate.c", "config", "z_erofs_load_deflate_config"),
|
||||
("decompressor_deflate.c", "decompress", "z_erofs_deflate_decompress"),
|
||||
("decompressor_lzma.c", "config", "z_erofs_load_lzma_config"),
|
||||
("decompressor_lzma.c", "decompress", "z_erofs_lzma_decompress"),
|
||||
("decompressor_zstd.c", "config", "z_erofs_load_zstd_config"),
|
||||
("decompressor_zstd.c", "decompress", "z_erofs_zstd_decompress"),
|
||||
]
|
||||
if Counter(callbacks) != Counter(expected_callbacks) or len(callbacks) != 10:
|
||||
raise SystemExit(f"G01 decompressor callback multiset changed: {callbacks!r}")
|
||||
|
||||
all_source = "\n".join(current.values())
|
||||
for removed in ("erofs_sb_lz4_info", "max_distance_pages", "sbi->lz4"):
|
||||
if removed in all_source:
|
||||
raise SystemExit(f"removed LZ4 runtime state remains: {removed}")
|
||||
config = current["decompressor.c"][: current["decompressor.c"].index("static int\nz_erofs_transform_plain")]
|
||||
for marker in (
|
||||
"uint32_t max_pclusterblks;",
|
||||
"max_pclusterblks = le16toh(lz4->max_pclusterblks);",
|
||||
"if (max_pclusterblks == 0)",
|
||||
"max_pclusterblks = 1;",
|
||||
"max_pclusterblks >\n\t\t (Z_EROFS_PCLUSTER_MAX_SIZE >> sbi->blkszbits)",
|
||||
"return (EOPNOTSUPP);",
|
||||
"distance = le16toh(dsb->u1.lz4_max_distance);",
|
||||
"if (distance == 0 && !erofs_sb_has_lz4_0padding(sbi))",
|
||||
):
|
||||
if marker not in config:
|
||||
raise SystemExit(f"LZ4 config validation marker missing: {marker!r}")
|
||||
|
||||
if current["zdata.c"].count("z_erofs_decode_extent") != 2:
|
||||
raise SystemExit("zdata decode helper rename denominator changed")
|
||||
if "z_erofs_read_extent" in current["zdata.c"]:
|
||||
raise SystemExit("old zdata decode helper name remains")
|
||||
if current["zmap.c"].count("z_erofs_read_extent") != 4:
|
||||
raise SystemExit("zmap record reader definition/calls changed")
|
||||
if "z_erofs_decode_extent" in current["zmap.c"]:
|
||||
raise SystemExit("zmap record reader was incorrectly renamed")
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", all_source):
|
||||
raise SystemExit("B30 introduced Linux negative errno")
|
||||
|
||||
for path, load_name, decode_name, descriptor_name in (
|
||||
("decompressor_lzma.c", "z_erofs_load_lzma_config", "z_erofs_lzma_decompress", "z_erofs_lzma_decomp"),
|
||||
("decompressor_deflate.c", "z_erofs_load_deflate_config", "z_erofs_deflate_decompress", "z_erofs_deflate_decomp"),
|
||||
("decompressor_zstd.c", "z_erofs_load_zstd_config", "z_erofs_zstd_decompress", "z_erofs_zstd_decomp"),
|
||||
):
|
||||
positions = [current[path].index(name) for name in (load_name, decode_name, descriptor_name)]
|
||||
if positions != sorted(positions):
|
||||
raise SystemExit(f"backend definition order drifted: {path}")
|
||||
|
||||
report = {
|
||||
"status": "PASS",
|
||||
"baseline": baseline,
|
||||
"changed_paths": sorted(changed),
|
||||
"descriptor_fields": ["config", "decompress", "supports_subextent", "name"],
|
||||
"descriptor_table_order": table_order,
|
||||
"callback_field_count": len(callbacks),
|
||||
"callback_target_multiset": sorted(callbacks),
|
||||
"removed_runtime_state": ["erofs_sb_lz4_info", "max_distance_pages", "max_pclusterblks store"],
|
||||
"retained_config_validation": ["max_pclusterblks local", "format cap", "legacy distance branch"],
|
||||
"rename": {"zdata_decode": 2, "zmap_record_reader": 4},
|
||||
"preserved": ["B25 typed errno", "B27 trailing policy", "B28 partial fallback ownership"],
|
||||
}
|
||||
with report_path.open("x", encoding="ascii") as stream:
|
||||
json.dump(report, stream, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
print("B30-descriptors PASS fields=4 backends=6 callbacks=10")
|
||||
print("B30-write-only PASS dead-state=2 config-local=1 rename=2/4")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B30 descriptor, dead-state, or rename oracle failed'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/B30-codec-structure.json" > "$artifacts/SHA256SUMS"
|
||||
printf 'B30 PASS codec structure aligned without behavior changes\n'
|
||||
Executable
+350
@@ -0,0 +1,350 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=643b83c9cea6a99398fa3dd053f2e8f65e108ca0
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
action=${1:-callgraph}
|
||||
shift || :
|
||||
|
||||
case "$action" in
|
||||
callgraph|nm-allowlist) ;;
|
||||
*) pre15_fail_usage "unknown B31 action: $action" ;;
|
||||
esac
|
||||
|
||||
for tool in git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B31 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b31-data "$PRE15_DUT/src/data.c"
|
||||
pre15_record_fixture b31-dir "$PRE15_DUT/src/dir.c"
|
||||
pre15_record_fixture b31-inode "$PRE15_DUT/src/inode.c"
|
||||
pre15_record_fixture b31-internal "$PRE15_DUT/src/internal.h"
|
||||
pre15_record_fixture b31-case \
|
||||
"$PRE15_DUT/tests/pre15/cases/B31-visibility.sh"
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if ! git -C "$PRE15_ROOT" diff --quiet "$baseline" -- \
|
||||
repo-pre-15/src/data.c repo-pre-15/src/dir.c \
|
||||
repo-pre-15/src/inode.c repo-pre-15/src/internal.h; then
|
||||
pre15_target_reached
|
||||
pre15_dut_fail 'B31 production files differ from the B30 source baseline'
|
||||
fi
|
||||
|
||||
if test "$action" = callgraph; then
|
||||
test "$#" -eq 0 || pre15_fail_usage 'B31 callgraph takes no arguments'
|
||||
for source in zdata.c super.c namei.c erofs_fs.h; do
|
||||
pre15_record_fixture "b31-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
pre15_record_fixture b31-linux-data "$PRE15_ROOT/src-linux/data.c"
|
||||
pre15_record_fixture b31-linux-internal "$PRE15_ROOT/src-linux/internal.h"
|
||||
pre15_record_fixture b31-linux-fileio "$PRE15_ROOT/src-linux/fileio.c"
|
||||
pre15_record_fixture b31-linux-fscache "$PRE15_ROOT/src-linux/fscache.c"
|
||||
pre15_record_fixture b31-linux-inode "$PRE15_ROOT/src-linux/inode.c"
|
||||
pre15_record_fixture b31-linux-xattr "$PRE15_ROOT/src-linux/xattr.c"
|
||||
pre15_record_fixture b31-linux-zmap "$PRE15_ROOT/src-linux/zmap.c"
|
||||
pre15_record_fixture b31-linux-zdata "$PRE15_ROOT/src-linux/zdata.c"
|
||||
|
||||
pre15_target_reached
|
||||
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" \
|
||||
"$artifacts/B31-callgraph.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
report_path = Path(sys.argv[3])
|
||||
src = dut / "src"
|
||||
|
||||
symbols = {
|
||||
"erofs_map_dev": "data.c",
|
||||
"erofs_map_blocks": "data.c",
|
||||
"erofs_dirent_namelen": "dir.c",
|
||||
"erofs_iloc": "inode.c",
|
||||
}
|
||||
expected_occurrences = {
|
||||
"erofs_map_dev": {"data.c": 2},
|
||||
"erofs_map_blocks": {"data.c": 3, "super.c": 1, "zdata.c": 1},
|
||||
"erofs_dirent_namelen": {"dir.c": 3, "namei.c": 1},
|
||||
"erofs_iloc": {"inode.c": 3},
|
||||
}
|
||||
expected_cross_tu = {
|
||||
"erofs_map_dev": [],
|
||||
"erofs_map_blocks": ["super.c", "zdata.c"],
|
||||
"erofs_dirent_namelen": ["namei.c"],
|
||||
"erofs_iloc": [],
|
||||
}
|
||||
introduced_by = {
|
||||
("erofs_map_blocks", "super.c"): "55c609db",
|
||||
("erofs_map_blocks", "zdata.c"): "55c609db",
|
||||
("erofs_dirent_namelen", "namei.c"): "c7508502",
|
||||
}
|
||||
|
||||
|
||||
def strip_noncode(source: str) -> str:
|
||||
output = list(source)
|
||||
state = "code"
|
||||
index = 0
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
output[index] = output[index + 1] = " "
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
output[index] = output[index + 1] = " "
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
output[index] = " "
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
output[index] = " "
|
||||
state = "character"
|
||||
elif state == "block":
|
||||
if char == "*" and following == "/":
|
||||
output[index] = output[index + 1] = " "
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
if char != "\n":
|
||||
output[index] = " "
|
||||
elif state == "line":
|
||||
if char == "\n":
|
||||
state = "code"
|
||||
else:
|
||||
output[index] = " "
|
||||
else:
|
||||
if char == "\\" and following:
|
||||
output[index] = output[index + 1] = " "
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
if char != "\n":
|
||||
output[index] = " "
|
||||
index += 1
|
||||
if state == "block":
|
||||
raise SystemExit("unterminated block comment")
|
||||
return "".join(output)
|
||||
|
||||
|
||||
sources = {
|
||||
path.name: strip_noncode(path.read_text(encoding="utf-8"))
|
||||
for path in sorted(src.glob("*.c"))
|
||||
}
|
||||
locations: dict[str, list[dict[str, object]]] = {}
|
||||
cross_tu: dict[str, list[str]] = {}
|
||||
history: dict[str, str] = {}
|
||||
|
||||
for symbol, owner in symbols.items():
|
||||
pattern = re.compile(r"\b" + re.escape(symbol) + r"\s*\(")
|
||||
sites = []
|
||||
counts = Counter()
|
||||
for filename, source in sources.items():
|
||||
for match in pattern.finditer(source):
|
||||
line = source.count("\n", 0, match.start()) + 1
|
||||
sites.append({"file": filename, "line": line})
|
||||
counts[filename] += 1
|
||||
actual_counts = dict(sorted(counts.items()))
|
||||
if actual_counts != expected_occurrences[symbol]:
|
||||
raise SystemExit(
|
||||
f"{symbol} call-token inventory changed: {actual_counts!r}"
|
||||
)
|
||||
consumers = sorted(filename for filename in counts if filename != owner)
|
||||
if consumers != expected_cross_tu[symbol]:
|
||||
raise SystemExit(f"{symbol} cross-TU consumers changed: {consumers!r}")
|
||||
locations[symbol] = sites
|
||||
cross_tu[symbol] = consumers
|
||||
|
||||
for filename in consumers:
|
||||
for site in sites:
|
||||
if site["file"] != filename:
|
||||
continue
|
||||
blamed = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(root),
|
||||
"blame",
|
||||
"--line-porcelain",
|
||||
f"-L{site['line']},{site['line']}",
|
||||
"--",
|
||||
f"repo-pre-15/src/{filename}",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()[0].split()[0]
|
||||
expected = introduced_by[(symbol, filename)]
|
||||
if not blamed.startswith(expected):
|
||||
raise SystemExit(
|
||||
f"{symbol} {filename} introducer changed: {blamed}"
|
||||
)
|
||||
history[f"{symbol}:{filename}"] = blamed
|
||||
|
||||
internal = (src / "internal.h").read_text(encoding="utf-8")
|
||||
for symbol in symbols:
|
||||
prototype_count = len(
|
||||
re.findall(r"\b" + re.escape(symbol) + r"\s*\(", internal)
|
||||
)
|
||||
if prototype_count != 1:
|
||||
raise SystemExit(f"{symbol} prototype count changed: {prototype_count}")
|
||||
|
||||
all_source = "\n".join(
|
||||
path.read_text(encoding="utf-8") for path in sorted(src.glob("*.[ch]"))
|
||||
)
|
||||
mtime_helper_invocations = internal.count(
|
||||
"EROFS_FEATURE_FUNCS(mtime, compat, COMPAT_MTIME)"
|
||||
)
|
||||
mtime_explicit_references = len(re.findall(r"\berofs_sb_has_mtime\b", all_source))
|
||||
if mtime_helper_invocations != 1 or mtime_explicit_references != 0:
|
||||
raise SystemExit(
|
||||
"mtime helper inventory changed: "
|
||||
f"generator={mtime_helper_invocations} explicit={mtime_explicit_references}"
|
||||
)
|
||||
|
||||
erofs_fs = (src / "erofs_fs.h").read_text(encoding="utf-8")
|
||||
inode = (src / "inode.c").read_text(encoding="utf-8")
|
||||
super_source = (src / "super.c").read_text(encoding="utf-8")
|
||||
timestamp_anchors = {
|
||||
"ondisk_constant": "#define EROFS_FEATURE_COMPAT_MTIME" in erofs_fs,
|
||||
"compact_checked_add": "__builtin_add_overflow(sbi->epoch," in inode,
|
||||
"extended_timestamp": "(int64_t)le64toh(die->i_mtime)" in inode,
|
||||
"epoch_decode": "sbi->epoch = (int64_t)le64toh(dsb->epoch);" in super_source,
|
||||
"fixed_nsec_decode": "sbi->fixed_nsec = le32toh(dsb->fixed_nsec);" in super_source,
|
||||
}
|
||||
if not all(timestamp_anchors.values()):
|
||||
raise SystemExit(f"timestamp path changed: {timestamp_anchors!r}")
|
||||
|
||||
linux_src = root / "src-linux"
|
||||
linux_text = {
|
||||
path.name: strip_noncode(path.read_text(encoding="utf-8"))
|
||||
for path in (
|
||||
linux_src / "data.c",
|
||||
linux_src / "fileio.c",
|
||||
linux_src / "fscache.c",
|
||||
linux_src / "inode.c",
|
||||
linux_src / "xattr.c",
|
||||
linux_src / "zmap.c",
|
||||
linux_src / "zdata.c",
|
||||
linux_src / "internal.h",
|
||||
)
|
||||
}
|
||||
linux_internal = linux_text["internal.h"]
|
||||
if not re.search(r"\bint\s+erofs_map_dev\s*\(", linux_internal):
|
||||
raise SystemExit("Linux erofs_map_dev external prototype is absent")
|
||||
if not re.search(r"\bint\s+erofs_map_blocks\s*\(", linux_internal):
|
||||
raise SystemExit("Linux erofs_map_blocks external prototype is absent")
|
||||
if not re.search(
|
||||
r"static\s+inline\s+erofs_off_t\s+erofs_iloc\s*\(", linux_internal
|
||||
):
|
||||
raise SystemExit("Linux erofs_iloc is no longer static inline")
|
||||
if re.search(r"\berofs_dirent_namelen\s*\(", "\n".join(linux_text.values())):
|
||||
raise SystemExit("unexpected Linux erofs_dirent_namelen symbol")
|
||||
|
||||
linux_consumers = {}
|
||||
for symbol in ("erofs_map_dev", "erofs_map_blocks", "erofs_iloc"):
|
||||
pattern = re.compile(r"\b" + re.escape(symbol) + r"\s*\(")
|
||||
linux_consumers[symbol] = sorted(
|
||||
filename for filename, source in linux_text.items() if pattern.search(source)
|
||||
)
|
||||
|
||||
report = {
|
||||
"decision": "STOP-NO-SOURCE",
|
||||
"freebsd_call_tokens": locations,
|
||||
"freebsd_cross_tu_consumers": cross_tu,
|
||||
"introduced_by": history,
|
||||
"linux_linkage": {
|
||||
"erofs_map_dev": "external",
|
||||
"erofs_map_blocks": "external",
|
||||
"erofs_dirent_namelen": "absent",
|
||||
"erofs_iloc": "static inline",
|
||||
},
|
||||
"linux_consumer_files_in_probe": linux_consumers,
|
||||
"mtime_helper": {
|
||||
"generator_invocations": mtime_helper_invocations,
|
||||
"explicit_references": mtime_explicit_references,
|
||||
},
|
||||
"timestamp_anchors": timestamp_anchors,
|
||||
}
|
||||
report_path.write_text(
|
||||
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
PY
|
||||
then
|
||||
pre15_dut_fail 'B31 cross-TU visibility audit failed'
|
||||
fi
|
||||
printf '%s\n' \
|
||||
'B31 callgraph audit reached mandatory STOP' \
|
||||
'erofs_map_blocks: cross-TU consumers super.c,zdata.c introduced by B07c' \
|
||||
'erofs_dirent_namelen: cross-TU consumer namei.c introduced by B10' \
|
||||
'erofs_map_dev and erofs_iloc remain same-TU only; atomic B31 source edit is blocked' \
|
||||
'mtime helper remains generated with zero explicit references; ondisk constant and timestamp paths remain'
|
||||
pre15_gate_stop \
|
||||
'B31 blocked: B07c/B10 introduced cross-TU consumers for listed symbols'
|
||||
fi
|
||||
|
||||
test "$#" -eq 2 || \
|
||||
pre15_fail_usage 'B31 nm-allowlist requires zstdio0 and zstdio1 modules'
|
||||
for tool in awk diff nm; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B31 nm tool: $tool"
|
||||
done
|
||||
|
||||
module0=$1
|
||||
module1=$2
|
||||
pre15_record_fixture b31-zstdio0-module "$module0"
|
||||
pre15_record_fixture b31-zstdio1-module "$module1"
|
||||
pre15_target_reached
|
||||
|
||||
for config in 0 1; do
|
||||
eval module=\$module$config
|
||||
nm -g --defined-only "$module" | awk '{ print $NF }' | LC_ALL=C sort \
|
||||
>"$artifacts/B31-zstdio$config-global-names.txt"
|
||||
nm -u "$module" | awk '{ print $NF }' | LC_ALL=C sort \
|
||||
>"$artifacts/B31-zstdio$config-undefined-names.txt"
|
||||
for symbol in erofs_map_dev erofs_map_blocks erofs_dirent_namelen erofs_iloc; do
|
||||
if ! awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
|
||||
"$artifacts/B31-zstdio$config-global-names.txt"; then
|
||||
pre15_dut_fail "B31 zstdio$config lost required global $symbol"
|
||||
fi
|
||||
done
|
||||
if awk '$0 == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
|
||||
"$artifacts/B31-zstdio$config-global-names.txt" || \
|
||||
awk '$0 == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
|
||||
"$artifacts/B31-zstdio$config-undefined-names.txt"; then
|
||||
pre15_dut_fail "B31 zstdio$config unexpectedly emits erofs_sb_has_mtime"
|
||||
fi
|
||||
done
|
||||
|
||||
if ! diff -u "$artifacts/B31-zstdio0-global-names.txt" \
|
||||
"$artifacts/B31-zstdio1-global-names.txt" \
|
||||
>"$artifacts/B31-config-global.diff"; then
|
||||
pre15_dut_fail 'B31 defined globals differ by ZSTDIO configuration'
|
||||
fi
|
||||
|
||||
printf '%s\n' \
|
||||
'B31 nm allowlist PASS' \
|
||||
'global delta from B30 source baseline: zero' \
|
||||
'four listed symbols remain global in both configurations after mandatory STOP' \
|
||||
'erofs_sb_has_mtime is not emitted or undefined in either configuration'
|
||||
Executable
+334
@@ -0,0 +1,334 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
action=${1:-callgraph}
|
||||
shift || :
|
||||
|
||||
case "$action" in
|
||||
callgraph|nm-allowlist) ;;
|
||||
*) pre15_fail_usage "unknown B31r action: $action" ;;
|
||||
esac
|
||||
|
||||
for tool in git nm python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B31r host tool: $tool"
|
||||
done
|
||||
|
||||
for source in data.c dir.c inode.c internal.h namei.c super.c zdata.c zmap.c; do
|
||||
pre15_record_fixture "b31r-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
pre15_record_fixture b31r-case \
|
||||
"$PRE15_DUT/tests/pre15/cases/B31r-visibility.sh"
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if test "$action" = callgraph; then
|
||||
test "$#" -eq 0 || pre15_fail_usage 'B31r callgraph takes no arguments'
|
||||
pre15_target_reached
|
||||
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" \
|
||||
"$artifacts/B31r-callgraph.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
report_path = Path(sys.argv[3])
|
||||
src = dut / "src"
|
||||
|
||||
symbols = {
|
||||
"erofs_map_dev": "data.c",
|
||||
"erofs_map_blocks": "data.c",
|
||||
"erofs_dirent_namelen": "dir.c",
|
||||
"erofs_iloc": "inode.c",
|
||||
}
|
||||
expected_occurrences = {
|
||||
"erofs_map_dev": {"data.c": 2},
|
||||
"erofs_map_blocks": {"data.c": 3, "super.c": 1, "zdata.c": 1},
|
||||
"erofs_dirent_namelen": {"dir.c": 3, "namei.c": 1},
|
||||
"erofs_iloc": {"inode.c": 3},
|
||||
}
|
||||
expected_cross_tu = {
|
||||
"erofs_map_dev": [],
|
||||
"erofs_map_blocks": ["super.c", "zdata.c"],
|
||||
"erofs_dirent_namelen": ["namei.c"],
|
||||
"erofs_iloc": [],
|
||||
}
|
||||
introduced_by = {
|
||||
("erofs_map_blocks", "super.c"): "55c609db",
|
||||
("erofs_map_blocks", "zdata.c"): "55c609db",
|
||||
("erofs_dirent_namelen", "namei.c"): "c7508502",
|
||||
}
|
||||
|
||||
|
||||
def strip_noncode(source: str) -> str:
|
||||
output = list(source)
|
||||
state = "code"
|
||||
index = 0
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
output[index] = output[index + 1] = " "
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
output[index] = output[index + 1] = " "
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
output[index] = " "
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
output[index] = " "
|
||||
state = "character"
|
||||
elif state == "block":
|
||||
if char == "*" and following == "/":
|
||||
output[index] = output[index + 1] = " "
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
if char != "\n":
|
||||
output[index] = " "
|
||||
elif state == "line":
|
||||
if char == "\n":
|
||||
state = "code"
|
||||
else:
|
||||
output[index] = " "
|
||||
else:
|
||||
if char == "\\" and following:
|
||||
output[index] = output[index + 1] = " "
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
if char != "\n":
|
||||
output[index] = " "
|
||||
index += 1
|
||||
if state == "block":
|
||||
raise SystemExit("unterminated block comment")
|
||||
return "".join(output)
|
||||
|
||||
|
||||
sources = {
|
||||
path.name: strip_noncode(path.read_text(encoding="utf-8"))
|
||||
for path in sorted(src.glob("*.c"))
|
||||
}
|
||||
locations: dict[str, list[dict[str, object]]] = {}
|
||||
cross_tu: dict[str, list[str]] = {}
|
||||
history: dict[str, str] = {}
|
||||
|
||||
for symbol, owner in symbols.items():
|
||||
pattern = re.compile(r"\b" + re.escape(symbol) + r"\s*\(")
|
||||
sites = []
|
||||
counts = Counter()
|
||||
for filename, source in sources.items():
|
||||
for match in pattern.finditer(source):
|
||||
line = source.count("\n", 0, match.start()) + 1
|
||||
sites.append({"file": filename, "line": line})
|
||||
counts[filename] += 1
|
||||
actual_counts = dict(sorted(counts.items()))
|
||||
if actual_counts != expected_occurrences[symbol]:
|
||||
raise SystemExit(
|
||||
f"{symbol} call-token inventory changed: {actual_counts!r}"
|
||||
)
|
||||
consumers = sorted(filename for filename in counts if filename != owner)
|
||||
if consumers != expected_cross_tu[symbol]:
|
||||
raise SystemExit(f"{symbol} cross-TU consumers changed: {consumers!r}")
|
||||
locations[symbol] = sites
|
||||
cross_tu[symbol] = consumers
|
||||
|
||||
for filename in consumers:
|
||||
for site in sites:
|
||||
if site["file"] != filename:
|
||||
continue
|
||||
blamed = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(root),
|
||||
"blame",
|
||||
"--line-porcelain",
|
||||
f"-L{site['line']},{site['line']}",
|
||||
"--",
|
||||
f"repo-pre-15/src/{filename}",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()[0].split()[0]
|
||||
expected = introduced_by[(symbol, filename)]
|
||||
if not blamed.startswith(expected):
|
||||
raise SystemExit(
|
||||
f"{symbol} {filename} introducer changed: {blamed}"
|
||||
)
|
||||
history[f"{symbol}:{filename}"] = blamed
|
||||
|
||||
internal = (src / "internal.h").read_text(encoding="utf-8")
|
||||
for symbol in symbols:
|
||||
prototype_count = len(
|
||||
re.findall(r"\b" + re.escape(symbol) + r"\s*\(", internal)
|
||||
)
|
||||
expected = 1 if symbol in {"erofs_map_blocks", "erofs_dirent_namelen"} else 0
|
||||
if prototype_count != expected:
|
||||
raise SystemExit(
|
||||
f"{symbol} prototype count changed: {prototype_count}, expected {expected}"
|
||||
)
|
||||
|
||||
data = (src / "data.c").read_text(encoding="utf-8")
|
||||
inode = (src / "inode.c").read_text(encoding="utf-8")
|
||||
if not re.search(r"\bstatic\s+int\s+erofs_map_dev\s*\(", data):
|
||||
raise SystemExit("erofs_map_dev is not static in data.c")
|
||||
if not re.search(r"\bstatic\s+erofs_off_t\s+erofs_iloc\s*\(", inode):
|
||||
raise SystemExit("erofs_iloc is not static in inode.c")
|
||||
|
||||
all_source = "\n".join(
|
||||
path.read_text(encoding="utf-8") for path in sorted(src.glob("*.[ch]"))
|
||||
)
|
||||
mtime_helper_invocations = all_source.count(
|
||||
"EROFS_FEATURE_FUNCS(mtime, compat, COMPAT_MTIME)"
|
||||
)
|
||||
mtime_explicit_references = len(re.findall(r"\berofs_sb_has_mtime\b", all_source))
|
||||
if mtime_helper_invocations != 0 or mtime_explicit_references != 0:
|
||||
raise SystemExit(
|
||||
"mtime helper inventory changed: "
|
||||
f"generator={mtime_helper_invocations} explicit={mtime_explicit_references}"
|
||||
)
|
||||
|
||||
erofs_fs = (src / "erofs_fs.h").read_text(encoding="utf-8")
|
||||
super_source = (src / "super.c").read_text(encoding="utf-8")
|
||||
timestamp_anchors = {
|
||||
"ondisk_constant": "#define EROFS_FEATURE_COMPAT_MTIME" in erofs_fs,
|
||||
"compact_checked_add": "__builtin_add_overflow(sbi->epoch," in inode,
|
||||
"extended_timestamp": "(int64_t)le64toh(die->i_mtime)" in inode,
|
||||
"epoch_decode": "sbi->epoch = (int64_t)le64toh(dsb->epoch);" in super_source,
|
||||
"fixed_nsec_decode": "sbi->fixed_nsec = le32toh(dsb->fixed_nsec);" in super_source,
|
||||
}
|
||||
if not all(timestamp_anchors.values()):
|
||||
raise SystemExit(f"timestamp path changed: {timestamp_anchors!r}")
|
||||
|
||||
source_hashes = {}
|
||||
for path in sorted(src.glob("*.[ch]")):
|
||||
source_hashes[path.relative_to(dut).as_posix()] = hashlib.sha256(
|
||||
path.read_bytes()
|
||||
).hexdigest()
|
||||
|
||||
report = {
|
||||
"decision": "PASS",
|
||||
"freebsd_call_tokens": locations,
|
||||
"freebsd_cross_tu_consumers": cross_tu,
|
||||
"introduced_by": history,
|
||||
"linkage": {
|
||||
"erofs_map_dev": "static",
|
||||
"erofs_map_blocks": "external",
|
||||
"erofs_dirent_namelen": "external",
|
||||
"erofs_iloc": "static",
|
||||
},
|
||||
"internal_prototypes": {
|
||||
symbol: len(re.findall(r"\b" + re.escape(symbol) + r"\s*\(", internal))
|
||||
for symbol in symbols
|
||||
},
|
||||
"mtime_helper": {
|
||||
"generator_invocations": mtime_helper_invocations,
|
||||
"explicit_references": mtime_explicit_references,
|
||||
},
|
||||
"timestamp_anchors": timestamp_anchors,
|
||||
"source_hashes": source_hashes,
|
||||
}
|
||||
report_path.write_text(
|
||||
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
(report_path.parent / "B31r-source-sha256.txt").write_text(
|
||||
"".join(f"{digest} {path}\n" for path, digest in source_hashes.items()),
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
then
|
||||
pre15_dut_fail 'B31r independent callgraph audit failed'
|
||||
fi
|
||||
printf '%s\n' \
|
||||
'B31r independent cross-TU callgraph PASS' \
|
||||
'erofs_map_dev and erofs_iloc are same-TU and static' \
|
||||
'erofs_map_blocks and erofs_dirent_namelen retain external linkage' \
|
||||
'erofs_sb_has_mtime helper is absent; timestamp anchors remain' \
|
||||
'source hashes and exact consumers recorded in B31r-callgraph.json'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test "$#" -eq 2 || \
|
||||
pre15_fail_usage 'B31r nm-allowlist requires zstdio0 and zstdio1 modules'
|
||||
for module in "$@"; do
|
||||
pre15_record_fixture "b31r-module-$module" "$module"
|
||||
done
|
||||
pre15_target_reached
|
||||
|
||||
for config in 0 1; do
|
||||
case "$config" in
|
||||
0) module=$1 ;;
|
||||
1) module=$2 ;;
|
||||
esac
|
||||
nm -an "$module" >"$artifacts/B31r-zstdio$config-nm-an.txt"
|
||||
awk '$NF ~ /^(erofs_map_dev|erofs_map_blocks|erofs_dirent_namelen|erofs_iloc|erofs_sb_has_mtime)$/' \
|
||||
"$artifacts/B31r-zstdio$config-nm-an.txt" \
|
||||
>"$artifacts/B31r-zstdio$config-target-bindings.txt"
|
||||
nm -g --defined-only "$module" | awk '{ print $NF }' | LC_ALL=C sort \
|
||||
>"$artifacts/B31r-zstdio$config-global-names.txt"
|
||||
nm -u "$module" | awk '{ print $NF }' | LC_ALL=C sort \
|
||||
>"$artifacts/B31r-zstdio$config-undefined-names.txt"
|
||||
for symbol in erofs_map_blocks erofs_dirent_namelen; do
|
||||
if ! awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
|
||||
"$artifacts/B31r-zstdio$config-global-names.txt"; then
|
||||
pre15_dut_fail "B31r zstdio$config lost required global $symbol"
|
||||
fi
|
||||
done
|
||||
for symbol in erofs_map_dev erofs_iloc erofs_sb_has_mtime; do
|
||||
if awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
|
||||
"$artifacts/B31r-zstdio$config-global-names.txt" || \
|
||||
awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
|
||||
"$artifacts/B31r-zstdio$config-undefined-names.txt"; then
|
||||
pre15_dut_fail "B31r zstdio$config unexpectedly exposes $symbol"
|
||||
fi
|
||||
done
|
||||
for symbol in erofs_map_dev erofs_iloc; do
|
||||
if ! awk -v symbol="$symbol" \
|
||||
'$NF == symbol && $(NF - 1) !~ /^[a-z]$/ { bad = 1 } END { exit bad }' \
|
||||
"$artifacts/B31r-zstdio$config-nm-an.txt"; then
|
||||
pre15_dut_fail "B31r zstdio$config has non-local binding for $symbol"
|
||||
fi
|
||||
done
|
||||
if awk '$NF == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
|
||||
"$artifacts/B31r-zstdio$config-nm-an.txt"; then
|
||||
pre15_dut_fail "B31r zstdio$config emits erofs_sb_has_mtime"
|
||||
fi
|
||||
done
|
||||
|
||||
if ! diff -u "$artifacts/B31r-zstdio0-global-names.txt" \
|
||||
"$artifacts/B31r-zstdio1-global-names.txt" \
|
||||
>"$artifacts/B31r-config-global.diff"; then
|
||||
pre15_dut_fail 'B31r defined globals differ by ZSTDIO configuration'
|
||||
fi
|
||||
|
||||
printf '%s\n' \
|
||||
'B31r nm allowlist PASS' \
|
||||
'cross-TU symbols remain global in both configurations' \
|
||||
'same-TU symbols are absent or locally bound and never global/undefined' \
|
||||
'erofs_sb_has_mtime is absent from all symbol bindings' \
|
||||
'zstdio0/zstdio1 defined-global sets match'
|
||||
Executable
+601
@@ -0,0 +1,601 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
spec=$fixture_dir/B32-cache-state.json
|
||||
oracle=$fixture_dir/B32-cache-oracle.c
|
||||
generator=$fixture_dir/B32-cache-generate.py
|
||||
probe=$fixture_dir/B32-cache-probe.c
|
||||
qemu_runner=$fixture_dir/B32-qemu-run.sh
|
||||
kld_builder=$fixture_dir/B28-build-kld.sh
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
scenario=${PRE15_B32_SCENARIO:-TC168-cache-inflight}
|
||||
|
||||
case "$scenario" in
|
||||
TC168-cache-inflight|TC184-vnode-races) ;;
|
||||
*) pre15_fail_usage "unknown B32 scenario: $scenario" ;;
|
||||
esac
|
||||
|
||||
for tool in cc git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B32 host tool: $tool"
|
||||
done
|
||||
for fixture in "$spec" "$oracle" "$generator" "$probe" "$qemu_runner" \
|
||||
"$kld_builder"; do
|
||||
pre15_record_fixture "b32-$(basename "$fixture")" "$fixture"
|
||||
done
|
||||
for source in internal.h decompressor.c zdata.c zmap.c; do
|
||||
pre15_record_fixture "b32-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
|
||||
pre15_target_reached
|
||||
else
|
||||
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" \
|
||||
"$artifacts/B32-source-audit.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
spec = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
|
||||
report_path = Path(sys.argv[4])
|
||||
baseline = spec["baseline"]
|
||||
implementation = "c6a502184da6e973b1f0fa2ff8c0290c041e7092"
|
||||
current_owners = {
|
||||
"internal.h": "aa20623132b6821c6ad4651b263fc0ac0f7a14c8",
|
||||
"decompressor.c": implementation,
|
||||
"zdata.c": "bc56f830918b76029871b60cca2e53992de70a2e",
|
||||
"zmap.c": "d09924362eb29819bd393e1e86602eb38c7608c6",
|
||||
}
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(message)
|
||||
|
||||
|
||||
def committed(commit: str, path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{commit}:repo-pre-15/{path}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
fail(f"cannot read B32 source {commit}:{path}: {completed.stderr.strip()}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def function(source: str, name: str) -> str:
|
||||
match = re.search(rf"\n(?:static\s+)?[^\n]+\n{name}\([^{{]+\n\{{", source)
|
||||
if match is None:
|
||||
fail(f"cannot locate function {name}")
|
||||
start = match.start() + 1
|
||||
brace = source.find("{", match.start())
|
||||
depth = 0
|
||||
for index in range(brace, len(source)):
|
||||
if source[index] == "{":
|
||||
depth += 1
|
||||
elif source[index] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
fail(f"unterminated function {name}")
|
||||
|
||||
|
||||
expected_changed = {
|
||||
"repo-pre-15/src/decompressor.c",
|
||||
"repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/zdata.c",
|
||||
"repo-pre-15/src/zmap.c",
|
||||
"repo-pre-15/tests/pre15/cases/B32-cache-state.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B32-cache-generate.py",
|
||||
"repo-pre-15/tests/pre15/fixtures/B32-cache-oracle.c",
|
||||
"repo-pre-15/tests/pre15/fixtures/B32-cache-probe.c",
|
||||
"repo-pre-15/tests/pre15/fixtures/B32-qemu-run.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B32-cache-state.json",
|
||||
}
|
||||
changed = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(root),
|
||||
"diff-tree",
|
||||
"--no-commit-id",
|
||||
"--name-only",
|
||||
"-r",
|
||||
implementation,
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
).stdout.splitlines()
|
||||
if set(changed) != expected_changed or len(changed) != len(expected_changed):
|
||||
fail(f"B32 implementation write set mismatch: {changed!r}")
|
||||
|
||||
for name, commit in (("baseline", baseline), ("implementation", implementation)):
|
||||
if subprocess.check_output(
|
||||
["git", "-C", str(root), "rev-parse", commit], text=True
|
||||
).strip() != commit:
|
||||
fail(f"B32 {name} identity changed")
|
||||
|
||||
sources = {
|
||||
name: (dut / "src" / name).read_text(encoding="utf-8")
|
||||
for name in ("internal.h", "decompressor.c", "zdata.c", "zmap.c")
|
||||
}
|
||||
implemented_sources = {
|
||||
name: committed(implementation, f"src/{name}") for name in sources
|
||||
}
|
||||
for name, owner in current_owners.items():
|
||||
if sources[name] != committed(owner, f"src/{name}"):
|
||||
fail(f"current B32 source differs from accepted owner {owner}: {name}")
|
||||
required_key_tokens = {
|
||||
"nid": "cache->nid == vi->nid",
|
||||
"decoded_size": "cache->decoded_size == decoded_size",
|
||||
"m_pa": "cache->map.m_pa == map->m_pa",
|
||||
"m_la": "cache->map.m_la == map->m_la",
|
||||
"m_plen": "cache->map.m_plen == map->m_plen",
|
||||
"m_llen": "cache->map.m_llen == map->m_llen",
|
||||
"m_deviceid": "cache->map.m_deviceid == map->m_deviceid",
|
||||
"m_algorithmformat": "cache->map.m_algorithmformat == map->m_algorithmformat",
|
||||
"m_flags": "cache->map.m_flags == map->m_flags",
|
||||
}
|
||||
if list(required_key_tokens) != spec["key_fields"]:
|
||||
fail("B32 key fixture field order changed")
|
||||
|
||||
|
||||
def audit_b32_sources(label: str, source_set: dict[str, str]) -> None:
|
||||
internal = source_set["internal.h"]
|
||||
zdata = source_set["zdata.c"]
|
||||
decompressor = source_set["decompressor.c"]
|
||||
zmap = source_set["zmap.c"]
|
||||
map_match = re.search(
|
||||
r"struct erofs_map_blocks \{(?P<body>.*?)\n\};", internal, re.S
|
||||
)
|
||||
cache_match = re.search(
|
||||
r"struct erofs_zextent_cache \{(?P<body>.*?)\n\};", internal, re.S
|
||||
)
|
||||
if map_match is None or cache_match is None:
|
||||
fail(f"{label} canonical map/cache structures are absent")
|
||||
if map_match.start() > cache_match.start():
|
||||
fail(f"{label} canonical map is not defined before embedded cache use")
|
||||
map_body = map_match.group("body")
|
||||
cache_body = cache_match.group("body")
|
||||
if "uint8_t m_algorithmformat;" not in map_body:
|
||||
fail(f"{label} m_algorithmformat is not an unsigned narrow type")
|
||||
if "struct erofs_map_blocks map;" not in cache_body:
|
||||
fail(f"{label} cache does not embed the canonical map")
|
||||
for duplicate in required_key_tokens:
|
||||
if duplicate in ("nid", "decoded_size"):
|
||||
continue
|
||||
if re.search(
|
||||
rf"\b{duplicate}\b",
|
||||
cache_body.replace("struct erofs_map_blocks map;", ""),
|
||||
):
|
||||
fail(f"{label} cache duplicates canonical map field {duplicate}")
|
||||
for field in ("decoded_size", "waiters", "error", "state", "cv", "closing"):
|
||||
if not re.search(rf"\b{field}\b", cache_body):
|
||||
fail(f"{label} cache state is missing {field}")
|
||||
|
||||
match_body = function(zdata, "z_erofs_extent_cache_match")
|
||||
for field, token in required_key_tokens.items():
|
||||
if match_body.count(token) != 1:
|
||||
fail(f"{label} cache key does not compare {field} exactly once")
|
||||
for token in (
|
||||
"cv_wait(&cache->cv, &sbi->z_extent_cache_lock)",
|
||||
"cv_broadcast(&cache->cv)",
|
||||
"cache->state = EROFS_ZCACHE_INFLIGHT",
|
||||
"cache->state = EROFS_ZCACHE_READY",
|
||||
"cache->state = EROFS_ZCACHE_FAILED",
|
||||
"cache->waiters != 0",
|
||||
"cache->error = error",
|
||||
"cache->closing = true",
|
||||
):
|
||||
if token not in zdata:
|
||||
fail(f"{label} cache state-machine token is absent: {token}")
|
||||
if "cache->state == EROFS_ZCACHE_INFLIGHT || cache->waiters != 0" not in zdata:
|
||||
fail(f"{label} eviction does not protect registered waiter generations")
|
||||
if "z_erofs_extent_cache_eligible(sbi, vi, &map, decoded_len)" not in zdata:
|
||||
fail(f"{label} cache admission is not based on decoded size")
|
||||
if "Z_EROFS_CACHE_BYPASS" not in zdata or "free(decoded, M_EROFS);" not in zdata:
|
||||
fail(f"{label} no-cache fallback is absent")
|
||||
if (
|
||||
"uint8_t algorithm;" not in decompressor
|
||||
or "(unsigned char)map->m_algorithmformat" in decompressor
|
||||
):
|
||||
fail(f"{label} decompressor algorithm boundary is not unsigned narrow")
|
||||
for token in (
|
||||
"(uint8_t)(map->m_plen >>",
|
||||
"(uint8_t)(fmt - 1)",
|
||||
"(uint8_t)(h->h_algorithmtype & 15)",
|
||||
"(uint8_t)(h->h_algorithmtype >> 4)",
|
||||
):
|
||||
if token not in zmap:
|
||||
fail(f"{label} explicit on-disk conversion is absent: {token}")
|
||||
for forbidden in ("bitlock", "wait_on_bit", "workqueue", "work_struct", "folio"):
|
||||
if forbidden in "\n".join(source_set.values()):
|
||||
fail(f"{label} introduced Linux-only primitive: {forbidden}")
|
||||
|
||||
|
||||
audit_b32_sources("implementation", implemented_sources)
|
||||
audit_b32_sources("current", sources)
|
||||
|
||||
baseline_zdata = committed(baseline, "src/zdata.c")
|
||||
for preserved in ("z_erofs_decode_length", "z_erofs_decode_extent"):
|
||||
if function(implemented_sources["zdata.c"], preserved) != function(
|
||||
baseline_zdata, preserved
|
||||
):
|
||||
fail(f"B28 {preserved} changed outside B32 cache state")
|
||||
for untouched in (
|
||||
"compress.h",
|
||||
"decompressor_lz4.c",
|
||||
"decompressor_lzma.c",
|
||||
"decompressor_deflate.c",
|
||||
"decompressor_zstd.c",
|
||||
):
|
||||
if committed(implementation, f"src/{untouched}") != committed(
|
||||
baseline, f"src/{untouched}"
|
||||
):
|
||||
fail(f"B32 changed B25/B27/B28 source outside its write set: {untouched}")
|
||||
current = (dut / "src" / untouched).read_text(encoding="utf-8")
|
||||
owner = current_owners["zmap.c"] if untouched == "decompressor_zstd.c" else baseline
|
||||
if current != committed(owner, f"src/{untouched}"):
|
||||
fail(f"current preserved codec source differs from accepted owner: {untouched}")
|
||||
|
||||
report = {
|
||||
"algorithm_type": "uint8_t",
|
||||
"baseline": baseline,
|
||||
"batch": "B32",
|
||||
"cache_map_embedded": True,
|
||||
"current_owners": current_owners,
|
||||
"decoded_size_admission": True,
|
||||
"implementation": implementation,
|
||||
"key_fields": spec["key_fields"],
|
||||
"lock": "z_extent_cache_lock",
|
||||
"no_cache_fallback": True,
|
||||
"preserved": ["B25-positive-errno", "B27-trailing", "B28-partial-fallback"],
|
||||
"states": spec["states"],
|
||||
"write_set": sorted(changed),
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
PY
|
||||
then
|
||||
pre15_runner_fail 'B32 source, lock, key, or preservation audit failed'
|
||||
fi
|
||||
|
||||
if ! cc -std=c11 -Wall -Wextra -Werror -pthread "$oracle" \
|
||||
-o "$PRE15_CASE_TMP/B32-cache-oracle" \
|
||||
>"$artifacts/oracle-build.stdout" 2>"$artifacts/oracle-build.stderr"; then
|
||||
pre15_runner_fail 'B32 host concurrency oracle did not compile'
|
||||
fi
|
||||
if ! timeout -k 5 60 "$PRE15_CASE_TMP/B32-cache-oracle" \
|
||||
>"$artifacts/oracle.stdout" 2>"$artifacts/oracle.stderr"; then
|
||||
pre15_dut_fail 'B32 host concurrency oracle failed'
|
||||
fi
|
||||
sha256sum "$artifacts/B32-source-audit.json" "$artifacts/oracle.stdout" \
|
||||
>"$artifacts/SHA256SUMS"
|
||||
fi
|
||||
pre15_target_reached
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'TC168-cache-inflight host PASS' \
|
||||
'Canonical key, one owner, waiter bytes/positive errno, retry, eviction, fallback, and shutdown PASS' \
|
||||
'TC126/TC127/TC129 host oracle NOT_RUN (not necessary)' \
|
||||
'QEMU and full feature suite NOT_RUN in host mode'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for tool in clang cmp diff file mkfs.erofs nm scp tar timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B32 QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
first=$PRE15_CASE_TMP/fixtures-first
|
||||
second=$PRE15_CASE_TMP/fixtures-second
|
||||
if ! (umask 022 && timeout -k 5 180 python3 -B "$generator" --spec "$spec" \
|
||||
--output "$first") >"$artifacts/generate-first.json" \
|
||||
2>"$artifacts/generate-first.stderr" || \
|
||||
! (umask 022 && timeout -k 5 180 python3 -B "$generator" --spec "$spec" \
|
||||
--output "$second") >"$artifacts/generate-second.json" \
|
||||
2>"$artifacts/generate-second.stderr"; then
|
||||
pre15_runner_fail 'B32 fixture generation failed'
|
||||
fi
|
||||
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
|
||||
pre15_runner_fail 'B32 fixtures are not byte reproducible'
|
||||
fi
|
||||
cp "$first/fixture-index.json" "$artifacts/B32-fixture-index.json"
|
||||
|
||||
module=$PRE15_CASE_TMP/B32-erofs-zstdio0.ko
|
||||
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
||||
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
|
||||
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
|
||||
pre15_dut_fail 'B32 cross-target zstdio0 KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B32-fixtures.tar.gz
|
||||
tar -C "$first" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
pre15_guest_ssh_bounded()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-p "$PRE15_QEMU_SSH_PORT" \
|
||||
"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B32-fixtures.tar.gz || \
|
||||
! pre15_scp "$probe" /root/B32-cache-probe.c || \
|
||||
! pre15_scp "$module" /root/B32-erofs-zstdio0.ko; then
|
||||
pre15_infra_blocked 'could not transfer B32 module, probe, or fixtures'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/B32-fixtures && mkdir /root/B32-fixtures && tar -xzf /root/B32-fixtures.tar.gz -C /root/B32-fixtures && cc -O2 -Wall -Wextra -Werror -pthread -o /root/B32-cache-probe /root/B32-cache-probe.c'; then
|
||||
pre15_infra_blocked 'could not prepare B32 guest fixtures/probe'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-before-load.txt"
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before-load.txt"
|
||||
cache_tunable=vfs.erofs.decoded_cache.minimum_decode_work
|
||||
cache_tunable_before=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
|
||||
printf 'name=%s before=%s test-value=0\n' "$cache_tunable" \
|
||||
"${cache_tunable_before:-unset}" >"$artifacts/cache-tunable.txt"
|
||||
if ! pre15_guest_ssh_bounded kenv "$cache_tunable=0"; then
|
||||
pre15_infra_blocked 'could not force B32 decoded-cache admission'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B32-erofs-zstdio0.ko \
|
||||
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
|
||||
if test -n "$cache_tunable_before"; then
|
||||
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" || true
|
||||
else
|
||||
pre15_guest_ssh_bounded kenv -u "$cache_tunable" || true
|
||||
fi
|
||||
if grep -q 'module already loaded or in kernel' \
|
||||
"$artifacts/kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_dut_fail 'B32 exact-source zstdio0 KLD failed to load'
|
||||
fi
|
||||
if test -n "$cache_tunable_before"; then
|
||||
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" || \
|
||||
pre15_infra_blocked 'could not restore B32 cache tunable'
|
||||
else
|
||||
pre15_guest_ssh_bounded kenv -u "$cache_tunable" || \
|
||||
pre15_infra_blocked 'could not remove B32 cache tunable override'
|
||||
fi
|
||||
cache_tunable_after=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
|
||||
printf 'after=%s\n' "${cache_tunable_after:-unset}" \
|
||||
>>"$artifacts/cache-tunable.txt"
|
||||
if test "$cache_tunable_after" != "$cache_tunable_before"; then
|
||||
pre15_runner_fail 'B32 cache tunable was not restored exactly'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldstat -q -m erofs; then
|
||||
pre15_dut_fail 'B32 kldload returned success without erofs.1 ownership'
|
||||
fi
|
||||
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-after-load.txt"
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before.txt"
|
||||
diff -u "$artifacts/guest-dmesg-before-load.txt" \
|
||||
"$artifacts/guest-dmesg-before.txt" >"$artifacts/guest-dmesg-load.diff" || true
|
||||
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg-load.diff" \
|
||||
>"$artifacts/guest-dmesg-load-added.txt"
|
||||
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
|
||||
"$artifacts/guest-dmesg-load-added.txt"; then
|
||||
pre15_dut_fail 'B32 exact-source KLD load produced kernel or linker errors'
|
||||
fi
|
||||
pre15_own_guest_kld B32-erofs-zstdio0.ko 'B32 exact-source KLD'
|
||||
|
||||
b32_unown()
|
||||
{
|
||||
kind=$1
|
||||
value=$2
|
||||
tmp=$PRE15_CASE_TMP/ownership.$$
|
||||
awk -F ' ' -v kind="$kind" -v value="$value" \
|
||||
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
|
||||
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
|
||||
}
|
||||
|
||||
b32_attach()
|
||||
{
|
||||
image=$1
|
||||
label=$2
|
||||
B32_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "/root/B32-fixtures/images/$image") || \
|
||||
pre15_dut_fail "$label md attach failed"
|
||||
case "$B32_MD" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected B32 md unit: $B32_MD" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$B32_MD" "$label md"
|
||||
B32_MOUNT=/mnt/pre15-b32-$label
|
||||
pre15_guest_ssh_bounded mkdir -p "$B32_MOUNT"
|
||||
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B32_MD" "$B32_MOUNT" || \
|
||||
pre15_dut_fail "$label mount failed"
|
||||
pre15_own_guest_mount "$B32_MOUNT" "$label mount"
|
||||
}
|
||||
|
||||
b32_detach()
|
||||
{
|
||||
if pre15_guest_ssh_bounded mount | grep -F " on $B32_MOUNT " >/dev/null; then
|
||||
pre15_guest_ssh_bounded umount "$B32_MOUNT" || \
|
||||
pre15_dut_fail "$B32_MOUNT unmount failed"
|
||||
fi
|
||||
b32_unown guest-mount "$B32_MOUNT"
|
||||
pre15_guest_ssh_bounded mdconfig -d -u "$B32_MD" || \
|
||||
pre15_dut_fail "$B32_MD detach failed"
|
||||
b32_unown guest-md "$B32_MD"
|
||||
}
|
||||
|
||||
b32_start_lifecycle()
|
||||
{
|
||||
label=$1
|
||||
B32_CONTROL=/tmp/B32-$label-control
|
||||
B32_LIFECYCLE_PID=$(pre15_guest_ssh_bounded \
|
||||
"rm -rf '$B32_CONTROL'; mkdir '$B32_CONTROL'; nohup /root/B32-cache-probe lifecycle '$B32_MOUNT/payload.bin' '$B32_CONTROL' >/tmp/B32-$label.stdout 2>/tmp/B32-$label.stderr </dev/null & echo \$!") || \
|
||||
pre15_infra_blocked "$label lifecycle process did not start"
|
||||
case "$B32_LIFECYCLE_PID" in
|
||||
''|*[!0-9]*) pre15_runner_fail "invalid B32 lifecycle PID: $B32_LIFECYCLE_PID" ;;
|
||||
esac
|
||||
pre15_guest_ssh_bounded \
|
||||
"i=0; while test ! -f '$B32_CONTROL/ready'; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
|
||||
pre15_infra_blocked "$label lifecycle ready marker timed out"
|
||||
}
|
||||
|
||||
b32_stop_lifecycle()
|
||||
{
|
||||
pre15_guest_ssh_bounded touch "$B32_CONTROL/exit"
|
||||
pre15_guest_ssh_bounded \
|
||||
"i=0; while kill -0 '$B32_LIFECYCLE_PID' 2>/dev/null; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
|
||||
pre15_infra_blocked 'B32 lifecycle process did not exit'
|
||||
}
|
||||
|
||||
reference=/root/B32-fixtures/source/payload.bin
|
||||
b32_attach lzma-valid.erofs valid
|
||||
if ! pre15_guest_ssh_bounded /root/B32-cache-probe concurrent \
|
||||
"$B32_MOUNT/payload.bin" "$reference" 0 64 8 0 65536 \
|
||||
>"$artifacts/valid-concurrent.stdout" \
|
||||
2>"$artifacts/valid-concurrent.stderr"; then
|
||||
pre15_dut_fail 'TC168 valid same-key concurrent reads diverged'
|
||||
fi
|
||||
if ! grep -qx 'concurrent PASS workers=64 loops=8 assertions=512 errno=0 offset=0 length=65536' \
|
||||
"$artifacts/valid-concurrent.stdout"; then
|
||||
pre15_runner_fail 'TC168 valid assertion count was not exactly 512'
|
||||
fi
|
||||
if test "$scenario" = TC184-vnode-races; then
|
||||
if ! pre15_guest_ssh_bounded /root/B32-cache-probe vnode-race \
|
||||
"$B32_MOUNT/payload.bin" "$reference" 64 50 \
|
||||
>"$artifacts/vnode-race.stdout" \
|
||||
2>"$artifacts/vnode-race.stderr"; then
|
||||
pre15_dut_fail 'TC184 lookup/open/read/reclaim loop diverged'
|
||||
fi
|
||||
if ! grep -qx 'vnode-race PASS workers=64 loops=50 assertions=3200' \
|
||||
"$artifacts/vnode-race.stdout"; then
|
||||
pre15_runner_fail 'TC184 vnode assertion count was not exactly 3200'
|
||||
fi
|
||||
fi
|
||||
b32_detach
|
||||
|
||||
if test "$scenario" = TC168-cache-inflight; then
|
||||
b32_attach lzma-truncated.erofs truncated
|
||||
if ! pre15_guest_ssh_bounded /root/B32-cache-probe concurrent \
|
||||
"$B32_MOUNT/payload.bin" "$reference" 97 1 1 0 65536 \
|
||||
>"$artifacts/failure-single.stdout" \
|
||||
2>"$artifacts/failure-single.stderr"; then
|
||||
pre15_dut_fail 'TC168 single corrupt read did not return EINTEGRITY'
|
||||
fi
|
||||
if ! grep -qx 'concurrent PASS workers=1 loops=1 assertions=1 errno=97 offset=0 length=65536' \
|
||||
"$artifacts/failure-single.stdout"; then
|
||||
pre15_runner_fail 'TC168 single corrupt assertion count was not 1'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded /root/B32-cache-probe concurrent \
|
||||
"$B32_MOUNT/payload.bin" "$reference" 97 64 2 0 65536 \
|
||||
>"$artifacts/failure-concurrent.stdout" \
|
||||
2>"$artifacts/failure-concurrent.stderr"; then
|
||||
pre15_dut_fail 'TC168 failure waiters did not receive EINTEGRITY'
|
||||
fi
|
||||
if ! grep -qx 'concurrent PASS workers=64 loops=2 assertions=128 errno=97 offset=0 length=65536' \
|
||||
"$artifacts/failure-concurrent.stdout"; then
|
||||
pre15_runner_fail 'TC168 failure assertion count was not exactly 128'
|
||||
fi
|
||||
b32_detach
|
||||
else
|
||||
b32_attach lzma-valid.erofs normal-busy
|
||||
b32_start_lifecycle normal-busy
|
||||
if pre15_guest_ssh_bounded umount "$B32_MOUNT" \
|
||||
>"$artifacts/normal-unmount.stdout" 2>"$artifacts/normal-unmount.stderr"; then
|
||||
pre15_dut_fail 'TC184 normal unmount with held vnode unexpectedly succeeded'
|
||||
fi
|
||||
if ! grep -qi 'busy' "$artifacts/normal-unmount.stderr"; then
|
||||
pre15_dut_fail 'TC184 normal unmount did not report EBUSY'
|
||||
fi
|
||||
b32_stop_lifecycle
|
||||
b32_detach
|
||||
|
||||
b32_attach lzma-valid.erofs forced-held
|
||||
b32_start_lifecycle forced-held
|
||||
pre15_guest_ssh_bounded umount -f "$B32_MOUNT" || \
|
||||
pre15_dut_fail 'TC184 held-vnode forced unmount command failed'
|
||||
b32_unown guest-mount "$B32_MOUNT"
|
||||
pre15_guest_ssh_bounded touch "$B32_CONTROL/read"
|
||||
pre15_guest_ssh_bounded \
|
||||
"i=0; while test ! -f '$B32_CONTROL/result'; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
|
||||
pre15_infra_blocked 'TC184 held-vnode result timed out'
|
||||
pre15_guest_ssh_bounded cat "$B32_CONTROL/result" \
|
||||
>"$artifacts/forced-held-result.txt"
|
||||
if ! grep -qx -- '-1 6' "$artifacts/forced-held-result.txt"; then
|
||||
pre15_dut_fail 'TC184 revoked held vnode did not return exact ENXIO'
|
||||
fi
|
||||
b32_detach
|
||||
|
||||
b32_attach lzma-valid.erofs forced-closed
|
||||
b32_start_lifecycle forced-closed
|
||||
pre15_guest_ssh_bounded touch "$B32_CONTROL/close"
|
||||
pre15_guest_ssh_bounded \
|
||||
"i=0; while test ! -f '$B32_CONTROL/closed'; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
|
||||
pre15_infra_blocked 'TC184 closed-descriptor marker timed out'
|
||||
pre15_guest_ssh_bounded umount -f "$B32_MOUNT" || \
|
||||
pre15_dut_fail 'TC184 closed-descriptor forced unmount command failed'
|
||||
b32_unown guest-mount "$B32_MOUNT"
|
||||
pre15_guest_ssh_bounded touch "$B32_CONTROL/read"
|
||||
pre15_guest_ssh_bounded \
|
||||
"i=0; while test ! -f '$B32_CONTROL/result'; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
|
||||
pre15_infra_blocked 'TC184 closed-descriptor result timed out'
|
||||
pre15_guest_ssh_bounded cat "$B32_CONTROL/result" \
|
||||
>"$artifacts/forced-closed-result.txt"
|
||||
if ! grep -qx -- '-1 9' "$artifacts/forced-closed-result.txt"; then
|
||||
pre15_dut_fail 'TC184 closed descriptor did not return exact EBADF'
|
||||
fi
|
||||
b32_detach
|
||||
fi
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-after.txt"
|
||||
diff -u "$artifacts/guest-dmesg-before.txt" \
|
||||
"$artifacts/guest-dmesg-after.txt" >"$artifacts/guest-dmesg.diff" || true
|
||||
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg.diff" \
|
||||
>"$artifacts/guest-dmesg-added.txt"
|
||||
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
|
||||
"$artifacts/guest-dmesg-added.txt"; then
|
||||
pre15_dut_fail "$scenario produced kernel, linker, WITNESS, or UAF evidence"
|
||||
fi
|
||||
printf '%s PASS\n' "$scenario"
|
||||
printf '%s\n' \
|
||||
'QEMU used an owned overlay and dynamic forwarded port' \
|
||||
'KLD erofs.1 ownership and cache tunable restoration PASS' \
|
||||
'TC130 and full feature suite NOT_RUN'
|
||||
Executable
+641
@@ -0,0 +1,641 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=93d0c64e321f50cf68026c3f1c0c95293bf17302
|
||||
case_script=$PRE15_DUT/tests/pre15/cases/B33-cache-policy.sh
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
gate_dir=$PRE15_DUT/tests/pre15/gates
|
||||
oracle=$fixture_dir/B33-cache-oracle.c
|
||||
qemu_spec=$fixture_dir/B33-cache-state.json
|
||||
qemu_generator=$fixture_dir/B33-cache-generate.py
|
||||
qemu_runner=$fixture_dir/B33-qemu-run.sh
|
||||
b32_probe=$fixture_dir/B32-cache-probe.c
|
||||
kld_builder=$fixture_dir/B28-build-kld.sh
|
||||
gate_input=$gate_dir/P15-038-input.json
|
||||
gate_result=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/result.json
|
||||
gate_benefit=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/benefit.json
|
||||
gate_state=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/state-model.json
|
||||
gate_hashes=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/SHA256SUMS
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in cc git python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B33 host tool: $tool"
|
||||
done
|
||||
for fixture in "$case_script" "$oracle" "$qemu_spec" "$qemu_generator" "$qemu_runner" \
|
||||
"$b32_probe" "$kld_builder" "$gate_input" "$gate_result" \
|
||||
"$gate_benefit" "$gate_state" "$gate_hashes"; do
|
||||
pre15_record_fixture "b33-$(basename "$fixture")" "$fixture"
|
||||
done
|
||||
for source in internal.h zdata.c; do
|
||||
pre15_record_fixture "b33-$source" "$PRE15_DUT/src/$source"
|
||||
done
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
|
||||
pre15_target_reached
|
||||
else
|
||||
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$gate_input" \
|
||||
"$gate_result" "$gate_benefit" "$gate_state" \
|
||||
"$artifacts/B33-source-audit.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
implementation = "bc56f830918b76029871b60cca2e53992de70a2e"
|
||||
current_owners = {
|
||||
"internal.h": "aa20623132b6821c6ad4651b263fc0ac0f7a14c8",
|
||||
"zdata.c": implementation,
|
||||
}
|
||||
gate_input = json.loads(Path(sys.argv[4]).read_text(encoding="ascii"))
|
||||
gate_result = json.loads(Path(sys.argv[5]).read_text(encoding="ascii"))
|
||||
gate_benefit = json.loads(Path(sys.argv[6]).read_text(encoding="ascii"))
|
||||
gate_state = json.loads(Path(sys.argv[7]).read_text(encoding="ascii"))
|
||||
report_path = Path(sys.argv[8])
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(message)
|
||||
|
||||
|
||||
def committed(commit: str, relative: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{commit}:{relative}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
fail(f"cannot read B33 source {commit}:{relative}: {completed.stderr.strip()}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def function(source: str, name: str) -> str:
|
||||
match = re.search(rf"\n(?:static\s+)?[^\n]+\n{name}\([^{{]+\n\{{", source)
|
||||
if match is None:
|
||||
fail(f"cannot locate function {name}")
|
||||
start = match.start() + 1
|
||||
brace = source.find("{", match.start())
|
||||
depth = 0
|
||||
for index in range(brace, len(source)):
|
||||
if source[index] == "{":
|
||||
depth += 1
|
||||
elif source[index] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
fail(f"unterminated function {name}")
|
||||
|
||||
|
||||
expected_changed = {
|
||||
"repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/zdata.c",
|
||||
"repo-pre-15/tests/pre15/cases/B33-cache-policy.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B33-cache-generate.py",
|
||||
"repo-pre-15/tests/pre15/fixtures/B33-cache-oracle.c",
|
||||
"repo-pre-15/tests/pre15/fixtures/B33-cache-state.json",
|
||||
"repo-pre-15/tests/pre15/fixtures/B33-qemu-run.sh",
|
||||
}
|
||||
changed = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(root),
|
||||
"diff-tree",
|
||||
"--no-commit-id",
|
||||
"--name-only",
|
||||
"-r",
|
||||
implementation,
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=30,
|
||||
).stdout.splitlines()
|
||||
if set(changed) != expected_changed or len(changed) != len(expected_changed):
|
||||
fail(f"B33 implementation write set mismatch: {changed!r}")
|
||||
|
||||
for name, commit in (("baseline", baseline), ("implementation", implementation)):
|
||||
if subprocess.check_output(
|
||||
["git", "-C", str(root), "rev-parse", commit], text=True
|
||||
).strip() != commit:
|
||||
fail(f"B33 {name} identity changed")
|
||||
|
||||
internal = (dut / "src/internal.h").read_text(encoding="utf-8")
|
||||
zdata = (dut / "src/zdata.c").read_text(encoding="utf-8")
|
||||
implemented_internal = committed(implementation, "repo-pre-15/src/internal.h")
|
||||
implemented_zdata = committed(implementation, "repo-pre-15/src/zdata.c")
|
||||
for name, source in (("internal.h", internal), ("zdata.c", zdata)):
|
||||
owner = current_owners[name]
|
||||
if source != committed(owner, f"repo-pre-15/src/{name}"):
|
||||
fail(f"current B33 source differs from accepted owner {owner}: {name}")
|
||||
|
||||
baseline_zdata = committed(baseline, "repo-pre-15/src/zdata.c")
|
||||
for preserved in ("z_erofs_decode_length", "z_erofs_decode_extent"):
|
||||
if function(implemented_zdata, preserved) != function(baseline_zdata, preserved):
|
||||
fail(f"B25/B27/B28 read/decode path changed in B33: {preserved}")
|
||||
|
||||
do_read = function(implemented_zdata, "z_erofs_do_read")
|
||||
baseline_do_read = function(baseline_zdata, "z_erofs_do_read")
|
||||
bypass_delta = (
|
||||
"\t\t\t} else\n"
|
||||
"\t\t\t\tz_erofs_extent_cache_record_bypass(sbi, &map);"
|
||||
)
|
||||
if do_read.count(bypass_delta) != 1 or do_read.replace(bypass_delta, "\t\t\t}") != baseline_do_read:
|
||||
fail("B33 z_erofs_do_read differs beyond policy-rejection accounting")
|
||||
|
||||
required_b32 = (
|
||||
"cache->nid == vi->nid",
|
||||
"cache->decoded_size == decoded_size",
|
||||
"cache->map.m_pa == map->m_pa",
|
||||
"cache->map.m_la == map->m_la",
|
||||
"cache->map.m_plen == map->m_plen",
|
||||
"cache->map.m_llen == map->m_llen",
|
||||
"cache->map.m_deviceid == map->m_deviceid",
|
||||
"cache->map.m_algorithmformat == map->m_algorithmformat",
|
||||
"cache->map.m_flags == map->m_flags",
|
||||
"cache->state == EROFS_ZCACHE_INFLIGHT || cache->waiters != 0",
|
||||
"cv_wait(&cache->cv, &sbi->z_extent_cache_lock)",
|
||||
"cache->error = error",
|
||||
"cache->closing = true",
|
||||
"Z_EROFS_CACHE_BYPASS",
|
||||
"free(decoded, M_EROFS);",
|
||||
)
|
||||
missing = [token for token in required_b32 if token not in zdata]
|
||||
if missing:
|
||||
fail(f"B32 key/inflight/failure/fallback token missing: {missing!r}")
|
||||
|
||||
policy = function(zdata, "z_erofs_extent_cache_eligible")
|
||||
for named_codec in (
|
||||
"Z_EROFS_COMPRESSION_LZ4",
|
||||
"Z_EROFS_COMPRESSION_LZMA",
|
||||
"Z_EROFS_COMPRESSION_DEFLATE",
|
||||
"Z_EROFS_COMPRESSION_ZSTD",
|
||||
):
|
||||
if named_codec in policy:
|
||||
fail(f"B33 admission still names a codec: {named_codec}")
|
||||
for token in (
|
||||
"map->m_algorithmformat < Z_EROFS_COMPRESSION_MAX",
|
||||
"len <= cache->budget_bytes",
|
||||
"decode_work >= cache->minimum_decode_work",
|
||||
"z_erofs_cache_enabled != 0",
|
||||
):
|
||||
if token not in policy:
|
||||
fail(f"codec-neutral work/size policy token is absent: {token}")
|
||||
if "map->m_algorithmformat == Z_EROFS_COMPRESSION_LZMA" in zdata:
|
||||
fail("LZMA-only admission branch remains")
|
||||
record_bypass = function(zdata, "z_erofs_extent_cache_record_bypass")
|
||||
if "++cache->metrics[map->m_algorithmformat].bypasses" not in record_bypass or (
|
||||
"z_erofs_extent_cache_record_bypass(sbi, &map)" not in do_read
|
||||
):
|
||||
fail("B33 policy rejection is absent from four-codec accounting")
|
||||
|
||||
budget = gate_input["budget"]
|
||||
expected_macros = {
|
||||
"EROFS_ZCACHE_MOUNT_HARD_BUDGET": budget["per_mount_bytes"] // 1024,
|
||||
"EROFS_ZCACHE_GLOBAL_HARD_BUDGET": budget["global_bytes"] // 1024,
|
||||
"EROFS_ZCACHE_MIN_DECODE_WORK": budget["minimum_decode_work_bytes"] // 1024,
|
||||
}
|
||||
for name, kib in expected_macros.items():
|
||||
if f"#define {name} ({kib}UL * 1024)" not in zdata:
|
||||
fail(f"B33 hard policy differs from G05: {name}")
|
||||
for token in (
|
||||
'TUNABLE_INT("vfs.erofs.decoded_cache.enabled"',
|
||||
'TUNABLE_ULONG("vfs.erofs.decoded_cache.mount_budget"',
|
||||
'TUNABLE_ULONG("vfs.erofs.decoded_cache.global_budget"',
|
||||
'TUNABLE_ULONG("vfs.erofs.decoded_cache.minimum_decode_work"',
|
||||
"MIN(z_erofs_cache_mount_budget,",
|
||||
"MIN(z_erofs_cache_global_budget,",
|
||||
"EVENTHANDLER_REGISTER(vm_lowmem",
|
||||
"EVENTHANDLER_DEREGISTER(vm_lowmem",
|
||||
"LIST_FOREACH(sbi, &z_erofs_cache_mounts, z_extent_cache_link)",
|
||||
"mtx_trylock(&sbi->z_extent_cache_lock)",
|
||||
):
|
||||
if token not in zdata:
|
||||
fail(f"B33 budget/config/reclaim token is absent: {token}")
|
||||
|
||||
claim = function(zdata, "z_erofs_extent_cache_claim")
|
||||
if claim.find("z_erofs_extent_cache_reserve(decoded_size)") > claim.find(
|
||||
"cache->state = EROFS_ZCACHE_INFLIGHT"
|
||||
):
|
||||
fail("B33 does not reserve before inflight decode")
|
||||
drop_position = claim.find("z_erofs_extent_cache_drop_locked(cache, true, false)")
|
||||
empty_position = claim.find("cache->state = EROFS_ZCACHE_EMPTY", drop_position)
|
||||
reserve_position = claim.find("z_erofs_extent_cache_reserve(decoded_size)")
|
||||
if drop_position < 0 or not (drop_position < empty_position < reserve_position):
|
||||
fail("B33 replacement reservation failure can leave READY without data")
|
||||
complete = function(zdata, "z_erofs_extent_cache_complete")
|
||||
if complete.find("z_erofs_extent_cache_release(cache->charged_bytes)") > complete.find(
|
||||
"cache->state = EROFS_ZCACHE_FAILED"
|
||||
):
|
||||
fail("B33 failed decode publishes before releasing reservation")
|
||||
|
||||
for field in (
|
||||
"hits",
|
||||
"misses",
|
||||
"bypasses",
|
||||
"evictions",
|
||||
"reclaims",
|
||||
"resident_bytes",
|
||||
"charged_bytes",
|
||||
"budget_bytes",
|
||||
"minimum_decode_work",
|
||||
"metrics[Z_EROFS_COMPRESSION_MAX]",
|
||||
):
|
||||
if field not in internal:
|
||||
fail(f"B33 four-codec accounting field is absent: {field}")
|
||||
for forbidden in ("shrinker", "workqueue", "work_struct", "wait_on_bit", "folio"):
|
||||
if forbidden in internal + zdata:
|
||||
fail(f"Linux-only cache primitive introduced: {forbidden}")
|
||||
|
||||
if gate_result.get("status") != "GO" or gate_result.get("b33") != "AUTHORIZED":
|
||||
fail("P15-038 G05 result is not GO")
|
||||
if gate_state.get("status") != "PASS" or gate_state["budget"]["global_remaining"] != 0:
|
||||
fail("P15-038 state model is not closed")
|
||||
if gate_benefit.get("codecs_meeting_threshold", 0) < 2 or any(
|
||||
len(summary["current_ns"]) != 5 or len(summary["candidate_ns"]) != 5
|
||||
for summary in gate_benefit.get("summaries", [])
|
||||
):
|
||||
fail("P15-038 fixed five-sample benefit is incomplete")
|
||||
|
||||
report = {
|
||||
"baseline": baseline,
|
||||
"batch": "B33",
|
||||
"b32_contract_preserved": True,
|
||||
"codec_neutral": True,
|
||||
"current_owners": current_owners,
|
||||
"gate_codecs_meeting_threshold": gate_benefit["codecs_meeting_threshold"],
|
||||
"global_hard_budget": budget["global_bytes"],
|
||||
"implementation": implementation,
|
||||
"lock_order": ["global-list", "mount-cache", "global-budget"],
|
||||
"mount_hard_budget": budget["per_mount_bytes"],
|
||||
"no_cache_fallback": True,
|
||||
"reclaim": "FreeBSD-vm_lowmem",
|
||||
"write_set": sorted(changed),
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
PY
|
||||
then
|
||||
pre15_runner_fail 'B33 source, gate, write-set, or preservation audit failed'
|
||||
fi
|
||||
|
||||
if ! (cd "$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038" && \
|
||||
timeout -k 5 30 sha256sum -c SHA256SUMS) \
|
||||
>"$artifacts/gate-manifest.stdout" 2>"$artifacts/gate-manifest.stderr"; then
|
||||
pre15_runner_fail 'P15-038 gate evidence manifest failed'
|
||||
fi
|
||||
if ! cc -std=c11 -O2 -Wall -Wextra -Werror "$oracle" \
|
||||
-o "$PRE15_CASE_TMP/B33-cache-oracle" \
|
||||
>"$artifacts/oracle-build.stdout" 2>"$artifacts/oracle-build.stderr"; then
|
||||
pre15_runner_fail 'B33 cache budget oracle did not compile'
|
||||
fi
|
||||
if ! timeout -k 5 60 "$PRE15_CASE_TMP/B33-cache-oracle" \
|
||||
>"$artifacts/oracle.json" 2>"$artifacts/oracle.stderr"; then
|
||||
pre15_dut_fail 'B33 cache budget/accounting oracle failed'
|
||||
fi
|
||||
if ! python3 -B - "$artifacts/oracle.json" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
result = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
if result.get("status") != "PASS":
|
||||
raise SystemExit("B33 oracle status is not PASS")
|
||||
if result.get("global_peak") != result.get("global_limit") or result.get("global_remaining") != 0:
|
||||
raise SystemExit("B33 oracle global budget did not close")
|
||||
metrics = result.get("metrics", [])
|
||||
if len(metrics) != 4 or [item.get("codec") for item in metrics] != list(range(4)):
|
||||
raise SystemExit("B33 four-codec metric set is incomplete")
|
||||
for item in metrics:
|
||||
if item.get("hits", 0) < 1 or item.get("misses", 0) < 1 or item.get("evictions", 0) < 1:
|
||||
raise SystemExit(f"B33 codec metric is incomplete: {item!r}")
|
||||
if item.get("resident_bytes") != 0:
|
||||
raise SystemExit(f"B33 codec resident bytes leaked: {item!r}")
|
||||
PY
|
||||
then
|
||||
pre15_dut_fail 'B33 four-codec accounting result is incomplete'
|
||||
fi
|
||||
|
||||
sha256sum "$artifacts/B33-source-audit.json" "$artifacts/oracle.json" \
|
||||
"$artifacts/gate-manifest.stdout" >"$artifacts/SHA256SUMS"
|
||||
fi
|
||||
pre15_target_reached
|
||||
|
||||
if test "${PRE15_MODE:-host}" != qemu; then
|
||||
printf '%s\n' \
|
||||
'B33-cache-budget host PASS' \
|
||||
'Four-codec admission/accounting, mount/global hard budgets, exhaustion fallback, reclaim, failure retry, disable, and eviction hash PASS' \
|
||||
'P15-038 host codec-cost gate replay manifest PASS; no guest vnode performance claimed' \
|
||||
'TC084/TC105/TC129 NOT_RUN (not necessary)' \
|
||||
'QEMU TC168, TC130, and full feature suite NOT_RUN in host mode'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test "${PRE15_B32_SCENARIO:-}" = TC168-cache-inflight || \
|
||||
pre15_runner_fail 'B33 QEMU mode requires TC168-cache-inflight'
|
||||
for tool in clang cmp diff file mkfs.erofs nm scp tar timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B33 QEMU tool: $tool"
|
||||
done
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
first=$PRE15_CASE_TMP/fixtures-first
|
||||
second=$PRE15_CASE_TMP/fixtures-second
|
||||
if ! (umask 022 && timeout -k 5 180 python3 -B "$qemu_generator" \
|
||||
--spec "$qemu_spec" --output "$first") \
|
||||
>"$artifacts/generate-first.json" 2>"$artifacts/generate-first.stderr" || \
|
||||
! (umask 022 && timeout -k 5 180 python3 -B "$qemu_generator" \
|
||||
--spec "$qemu_spec" --output "$second") \
|
||||
>"$artifacts/generate-second.json" 2>"$artifacts/generate-second.stderr"; then
|
||||
pre15_runner_fail 'B33 fixture generation failed'
|
||||
fi
|
||||
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
|
||||
pre15_runner_fail 'B33 fixtures are not byte reproducible'
|
||||
fi
|
||||
cp "$first/fixture-index.json" "$artifacts/B33-fixture-index.json"
|
||||
|
||||
module=$PRE15_CASE_TMP/B33-erofs-zstdio0.ko
|
||||
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
||||
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
|
||||
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
|
||||
pre15_dut_fail 'B33 cross-target zstdio0 KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
file "$module" >"$artifacts/kld-file.txt"
|
||||
sha256sum "$module" >"$artifacts/kld-sha256.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
|
||||
|
||||
fixture_archive=$PRE15_CASE_TMP/B33-fixtures.tar.gz
|
||||
tar -C "$first" -czf "$fixture_archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
pre15_guest_ssh_bounded()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-p "$PRE15_QEMU_SSH_PORT" \
|
||||
"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
|
||||
}
|
||||
|
||||
if ! pre15_scp "$fixture_archive" /root/B33-fixtures.tar.gz || \
|
||||
! pre15_scp "$b32_probe" /root/B33-cache-probe.c || \
|
||||
! pre15_scp "$module" /root/B33-erofs-zstdio0.ko; then
|
||||
pre15_infra_blocked 'could not transfer B33 module, probe, or fixtures'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/B33-fixtures && mkdir /root/B33-fixtures && tar -xzf /root/B33-fixtures.tar.gz -C /root/B33-fixtures && cc -O2 -Wall -Wextra -Werror -pthread -o /root/B33-cache-probe /root/B33-cache-probe.c'; then
|
||||
pre15_infra_blocked 'could not prepare B33 guest fixtures/probe'
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
module_sha256=$(sha256sum "$module" | awk '{print $1}')
|
||||
guest_module_sha256=$(pre15_guest_ssh_bounded sha256 -q /root/B33-erofs-zstdio0.ko) || \
|
||||
pre15_infra_blocked 'could not hash transferred B33 KLD'
|
||||
printf 'host=%s guest=%s\n' "$module_sha256" "$guest_module_sha256" \
|
||||
>"$artifacts/guest-module-sha256.txt"
|
||||
test "$guest_module_sha256" = "$module_sha256" || \
|
||||
pre15_runner_fail 'transferred B33 KLD hash differs from host module'
|
||||
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-before-load.txt"
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before-load.txt"
|
||||
cache_tunable=vfs.erofs.decoded_cache.minimum_decode_work
|
||||
cache_tunable_before=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
|
||||
printf 'name=%s before=%s test-value=0\n' "$cache_tunable" \
|
||||
"${cache_tunable_before:-unset}" >"$artifacts/cache-tunable.txt"
|
||||
if ! pre15_guest_ssh_bounded kenv "$cache_tunable=0" \
|
||||
>"$artifacts/cache-tunable-set.stdout" \
|
||||
2>"$artifacts/cache-tunable-set.stderr"; then
|
||||
pre15_infra_blocked 'could not force B33 decoded-cache admission'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload /root/B33-erofs-zstdio0.ko \
|
||||
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
|
||||
if test -n "$cache_tunable_before"; then
|
||||
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" || true
|
||||
else
|
||||
pre15_guest_ssh_bounded kenv -u "$cache_tunable" || true
|
||||
fi
|
||||
if grep -q 'module already loaded or in kernel' \
|
||||
"$artifacts/kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
pre15_dut_fail 'B33 exact-source zstdio0 KLD failed to load'
|
||||
fi
|
||||
if test -n "$cache_tunable_before"; then
|
||||
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" \
|
||||
>"$artifacts/cache-tunable-restore.stdout" \
|
||||
2>"$artifacts/cache-tunable-restore.stderr" || \
|
||||
pre15_infra_blocked 'could not restore B33 cache tunable'
|
||||
else
|
||||
pre15_guest_ssh_bounded kenv -u "$cache_tunable" \
|
||||
>"$artifacts/cache-tunable-restore.stdout" \
|
||||
2>"$artifacts/cache-tunable-restore.stderr" || \
|
||||
pre15_infra_blocked 'could not remove B33 cache tunable override'
|
||||
fi
|
||||
cache_tunable_after=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
|
||||
printf 'after=%s\n' "${cache_tunable_after:-unset}" \
|
||||
>>"$artifacts/cache-tunable.txt"
|
||||
if test "$cache_tunable_after" != "$cache_tunable_before"; then
|
||||
pre15_runner_fail 'B33 cache tunable was not restored exactly'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldstat -q -m erofs; then
|
||||
pre15_dut_fail 'B33 kldload returned success without erofs.1 ownership'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldstat -q -n B33-erofs-zstdio0.ko; then
|
||||
pre15_dut_fail 'B33 kldload returned success without exact KLD file ownership'
|
||||
fi
|
||||
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-after-load.txt"
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before.txt"
|
||||
diff -u "$artifacts/guest-dmesg-before-load.txt" \
|
||||
"$artifacts/guest-dmesg-before.txt" >"$artifacts/guest-dmesg-load.diff" || true
|
||||
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg-load.diff" \
|
||||
>"$artifacts/guest-dmesg-load-added.txt"
|
||||
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
|
||||
"$artifacts/guest-dmesg-load-added.txt"; then
|
||||
pre15_dut_fail 'B33 exact-source KLD load produced kernel or linker errors'
|
||||
fi
|
||||
pre15_own_guest_kld B33-erofs-zstdio0.ko 'B33 exact-source KLD'
|
||||
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-before-trace.txt"
|
||||
if ! pre15_guest_ssh_bounded command -v dtrace \
|
||||
>"$artifacts/guest-dtrace-command.txt" 2>"$artifacts/guest-dtrace-command.stderr"; then
|
||||
pre15_infra_blocked 'guest dtrace tool is unavailable for B33 call proof'
|
||||
fi
|
||||
set +e
|
||||
pre15_guest_ssh_bounded kldload dtraceall \
|
||||
>"$artifacts/guest-dtrace-kldload.stdout" \
|
||||
2>"$artifacts/guest-dtrace-kldload.stderr"
|
||||
dtrace_load_rc=$?
|
||||
set -e
|
||||
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-after-trace.txt"
|
||||
awk 'NR == FNR { if (FNR > 1) seen[$5] = 1; next }
|
||||
FNR > 1 && !seen[$5] { print $1 "\t" $5 }' \
|
||||
"$artifacts/guest-kldstat-before-trace.txt" \
|
||||
"$artifacts/guest-kldstat-after-trace.txt" \
|
||||
>"$artifacts/guest-trace-klds-all.tsv"
|
||||
awk '$2 == "dtraceall.ko"' "$artifacts/guest-trace-klds-all.tsv" \
|
||||
>"$artifacts/guest-trace-klds.tsv"
|
||||
while IFS="$(printf '\t')" read -r trace_kld_id trace_kld_name; do
|
||||
test -n "$trace_kld_id" || continue
|
||||
pre15_own_guest_kld "$trace_kld_name" 'B33 FBT call-proof dependency'
|
||||
done <"$artifacts/guest-trace-klds.tsv"
|
||||
test "$dtrace_load_rc" -eq 0 || \
|
||||
pre15_infra_blocked 'guest dtraceall KLD failed to load for B33 call proof'
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
"dtrace -l -n 'fbt::z_erofs_extent_cache_init:entry' -n 'fbt::z_erofs_do_read:entry'" \
|
||||
>"$artifacts/guest-dtrace-probe-list.txt" \
|
||||
2>"$artifacts/guest-dtrace-probe-list.stderr"; then
|
||||
pre15_infra_blocked 'B33 cache init/read FBT probes are unavailable'
|
||||
fi
|
||||
grep -q 'z_erofs_extent_cache_init.*entry' \
|
||||
"$artifacts/guest-dtrace-probe-list.txt" || \
|
||||
pre15_runner_fail 'B33 cache init FBT probe was not listed exactly'
|
||||
grep -q 'z_erofs_do_read.*entry' \
|
||||
"$artifacts/guest-dtrace-probe-list.txt" || \
|
||||
pre15_runner_fail 'B33 compressed-read FBT probe was not listed exactly'
|
||||
|
||||
b33_unown()
|
||||
{
|
||||
kind=$1
|
||||
value=$2
|
||||
tmp=$PRE15_CASE_TMP/ownership.$$
|
||||
awk -F ' ' -v kind="$kind" -v value="$value" \
|
||||
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
|
||||
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
|
||||
}
|
||||
|
||||
b33_attach()
|
||||
{
|
||||
image=$1
|
||||
label=$2
|
||||
B33_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "/root/B33-fixtures/images/$image") || \
|
||||
pre15_dut_fail "$label md attach failed"
|
||||
case "$B33_MD" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected B33 md unit: $B33_MD" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$B33_MD" "$label md"
|
||||
B33_MOUNT=/mnt/pre15-b33-$label
|
||||
pre15_guest_ssh_bounded mkdir -p "$B33_MOUNT"
|
||||
if test "${B33_TRACE_MOUNT:-0}" = 1; then
|
||||
mount_trace=/tmp/pre15-b33-cache-init.trace
|
||||
pre15_guest_ssh_bounded \
|
||||
"dtrace -q -o '$mount_trace' -n 'fbt::z_erofs_extent_cache_init:entry { @calls = count(); } END { printa(@calls); }' -c 'mount -t erofs -o ro /dev/$B33_MD $B33_MOUNT'" \
|
||||
>"$artifacts/valid-mount.stdout" \
|
||||
2>"$artifacts/valid-mount.stderr" || \
|
||||
pre15_dut_fail "$label traced mount failed"
|
||||
pre15_guest_ssh_bounded cat "$mount_trace" \
|
||||
>"$artifacts/guest-cache-init-fbt.txt"
|
||||
if ! awk '$1 ~ /^[0-9]+$/ && $1 > 0 { found = 1 }
|
||||
END { exit !found }' "$artifacts/guest-cache-init-fbt.txt"; then
|
||||
pre15_dut_fail 'B33 guest mount did not call decoded-cache init'
|
||||
fi
|
||||
else
|
||||
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B33_MD" "$B33_MOUNT" || \
|
||||
pre15_dut_fail "$label mount failed"
|
||||
fi
|
||||
pre15_own_guest_mount "$B33_MOUNT" "$label mount"
|
||||
}
|
||||
|
||||
b33_detach()
|
||||
{
|
||||
if pre15_guest_ssh_bounded mount | grep -F " on $B33_MOUNT " >/dev/null; then
|
||||
pre15_guest_ssh_bounded umount "$B33_MOUNT" || \
|
||||
pre15_dut_fail "$B33_MOUNT unmount failed"
|
||||
fi
|
||||
b33_unown guest-mount "$B33_MOUNT"
|
||||
pre15_guest_ssh_bounded mdconfig -d -u "$B33_MD" || \
|
||||
pre15_dut_fail "$B33_MD detach failed"
|
||||
b33_unown guest-md "$B33_MD"
|
||||
}
|
||||
|
||||
reference=/root/B33-fixtures/source/payload.bin
|
||||
B33_TRACE_MOUNT=1
|
||||
b33_attach lz4-valid.erofs valid
|
||||
unset B33_TRACE_MOUNT
|
||||
trace_file=/tmp/pre15-b33-do-read.trace
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
"dtrace -q -o '$trace_file' -n 'fbt::z_erofs_do_read:entry { @calls = count(); } END { printa(@calls); }' -c '/root/B33-cache-probe concurrent $B33_MOUNT/payload.bin $reference 0 64 4 0 65536'" \
|
||||
>"$artifacts/valid-concurrent.stdout" \
|
||||
2>"$artifacts/valid-concurrent.stderr"; then
|
||||
pre15_dut_fail 'TC168 valid full-extent concurrent reads diverged'
|
||||
fi
|
||||
if ! grep -qx 'concurrent PASS workers=64 loops=4 assertions=256 errno=0 offset=0 length=65536' \
|
||||
"$artifacts/valid-concurrent.stdout"; then
|
||||
pre15_runner_fail 'TC168 valid assertion count was not exactly 256'
|
||||
fi
|
||||
pre15_guest_ssh_bounded cat "$trace_file" \
|
||||
>"$artifacts/guest-do-read-fbt.txt"
|
||||
if ! awk '$1 ~ /^[0-9]+$/ && $1 > 0 { found = 1 }
|
||||
END { exit !found }' "$artifacts/guest-do-read-fbt.txt"; then
|
||||
pre15_dut_fail 'B33 guest reads did not call the target read implementation'
|
||||
fi
|
||||
b33_detach
|
||||
|
||||
b33_attach lz4-truncated.erofs truncated
|
||||
if ! pre15_guest_ssh_bounded /root/B33-cache-probe concurrent \
|
||||
"$B33_MOUNT/payload.bin" "$reference" 97 1 1 0 65536 \
|
||||
>"$artifacts/failure-single.stdout" \
|
||||
2>"$artifacts/failure-single.stderr"; then
|
||||
pre15_dut_fail 'TC168 single corrupt read did not return EINTEGRITY'
|
||||
fi
|
||||
if ! grep -qx 'concurrent PASS workers=1 loops=1 assertions=1 errno=97 offset=0 length=65536' \
|
||||
"$artifacts/failure-single.stdout"; then
|
||||
pre15_runner_fail 'TC168 single corrupt assertion count was not 1'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded /root/B33-cache-probe concurrent \
|
||||
"$B33_MOUNT/payload.bin" "$reference" 97 64 2 0 65536 \
|
||||
>"$artifacts/failure-concurrent.stdout" \
|
||||
2>"$artifacts/failure-concurrent.stderr"; then
|
||||
pre15_dut_fail 'TC168 failure waiters did not receive EINTEGRITY'
|
||||
fi
|
||||
if ! grep -qx 'concurrent PASS workers=64 loops=2 assertions=128 errno=97 offset=0 length=65536' \
|
||||
"$artifacts/failure-concurrent.stdout"; then
|
||||
pre15_runner_fail 'TC168 failure assertion count was not exactly 128'
|
||||
fi
|
||||
b33_detach
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-after.txt"
|
||||
diff -u "$artifacts/guest-dmesg-before.txt" \
|
||||
"$artifacts/guest-dmesg-after.txt" >"$artifacts/guest-dmesg.diff" || true
|
||||
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg.diff" \
|
||||
>"$artifacts/guest-dmesg-added.txt"
|
||||
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
|
||||
"$artifacts/guest-dmesg-added.txt"; then
|
||||
pre15_dut_fail 'TC168 produced panic, WITNESS, or UAF evidence'
|
||||
fi
|
||||
printf '%s\n' \
|
||||
'TC168-cache-inflight B33 QEMU PASS' \
|
||||
'Exact KLD ownership, FBT-proven cache init/read calls, full-extent success/failure waiters, unmount drain, and cleanup exercised' \
|
||||
'TC130 and full feature suite NOT_RUN'
|
||||
Executable
+412
@@ -0,0 +1,412 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
baseline=5cb420221ab0a4da7838c68db55280999474038b
|
||||
makefile=$PRE15_DUT/src/Makefile
|
||||
linux_makefile=$PRE15_ROOT/src-linux/Makefile
|
||||
case_file=$PRE15_DUT/tests/pre15/cases/B34-build-groups.sh
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
|
||||
for tool in git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing B34 host tool: $tool"
|
||||
done
|
||||
|
||||
pre15_record_fixture b34-makefile "$makefile"
|
||||
pre15_record_fixture b34-linux-makefile "$linux_makefile"
|
||||
pre15_record_fixture b34-case "$case_file"
|
||||
|
||||
mkdir -p "$artifacts"
|
||||
pre15_target_reached
|
||||
|
||||
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
|
||||
"$artifacts/B34-object-list.json" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from fnmatch import fnmatchcase
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
baseline = sys.argv[3]
|
||||
report_path = Path(sys.argv[4])
|
||||
makefile_path = dut / "src/Makefile"
|
||||
|
||||
|
||||
def committed(path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read B34 baseline {path}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def changed_paths() -> list[str]:
|
||||
changed = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(root),
|
||||
"diff",
|
||||
"--name-only",
|
||||
baseline,
|
||||
"--",
|
||||
"repo-pre-15",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
changed.extend(
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(root),
|
||||
"ls-files",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
"--",
|
||||
"repo-pre-15",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
)
|
||||
return changed
|
||||
|
||||
|
||||
expected_changed = [
|
||||
"repo-pre-15/src/Makefile",
|
||||
"repo-pre-15/tests/pre15/cases/B34-build-groups.sh",
|
||||
]
|
||||
changed = changed_paths()
|
||||
if sorted(changed) != expected_changed or len(changed) != len(expected_changed):
|
||||
raise SystemExit(f"B34 repo-pre-15 write set mismatch: {changed!r}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Evaluation:
|
||||
variables: dict[str, str]
|
||||
includes: list[str]
|
||||
src_operations: list[tuple[str, list[str]]]
|
||||
|
||||
|
||||
class MakeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def logical_lines(source: str) -> list[tuple[int, str]]:
|
||||
result: list[tuple[int, str]] = []
|
||||
parts: list[str] = []
|
||||
start_line = 0
|
||||
for line_number, raw_line in enumerate(source.splitlines(), 1):
|
||||
content = raw_line.split("#", 1)[0].rstrip()
|
||||
if not parts and not content.strip():
|
||||
continue
|
||||
if not parts:
|
||||
start_line = line_number
|
||||
continued = content.endswith("\\")
|
||||
if continued:
|
||||
content = content[:-1].rstrip()
|
||||
parts.append(content.strip())
|
||||
if not continued:
|
||||
result.append((start_line, " ".join(part for part in parts if part)))
|
||||
parts = []
|
||||
if parts:
|
||||
raise SystemExit(f"unterminated Makefile continuation at line {start_line}")
|
||||
return result
|
||||
|
||||
|
||||
def expand(value: str, variables: dict[str, str]) -> str:
|
||||
pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_.]*)\}")
|
||||
previous = None
|
||||
while value != previous:
|
||||
previous = value
|
||||
value = pattern.sub(lambda match: variables.get(match.group(1), ""), value)
|
||||
return value
|
||||
|
||||
|
||||
def condition_value(expression: str, variables: dict[str, str], line: int) -> bool:
|
||||
terms = [term.strip() for term in expression.split("&&")]
|
||||
values: list[bool] = []
|
||||
for term in terms:
|
||||
empty_match = re.fullmatch(
|
||||
r"empty\(([A-Za-z_][A-Za-z0-9_.]*):M([^()]*)\)", term
|
||||
)
|
||||
if empty_match:
|
||||
variable, pattern = empty_match.groups()
|
||||
words = variables.get(variable, "").split()
|
||||
values.append(not any(fnmatchcase(word, pattern) for word in words))
|
||||
continue
|
||||
compare_match = re.fullmatch(
|
||||
r'\$\{([A-Za-z_][A-Za-z0-9_.]*)\}\s*(==|!=)\s*"?([^" ]+)"?',
|
||||
term,
|
||||
)
|
||||
if compare_match:
|
||||
variable, operator, expected = compare_match.groups()
|
||||
equal = variables.get(variable, "") == expected
|
||||
values.append(equal if operator == "==" else not equal)
|
||||
continue
|
||||
raise SystemExit(f"unsupported Makefile condition at line {line}: {term}")
|
||||
return all(values)
|
||||
|
||||
|
||||
def evaluate(source: str, initial: dict[str, str]) -> Evaluation:
|
||||
variables = dict(initial)
|
||||
includes: list[str] = []
|
||||
src_operations: list[tuple[str, list[str]]] = []
|
||||
active_stack = [True]
|
||||
for line_number, line in logical_lines(source):
|
||||
if line.startswith(".if "):
|
||||
parent_active = active_stack[-1]
|
||||
active_stack.append(
|
||||
parent_active
|
||||
and condition_value(line.removeprefix(".if "), variables, line_number)
|
||||
)
|
||||
continue
|
||||
if line == ".endif":
|
||||
if len(active_stack) == 1:
|
||||
raise SystemExit(f"unmatched .endif at line {line_number}")
|
||||
active_stack.pop()
|
||||
continue
|
||||
if not active_stack[-1]:
|
||||
continue
|
||||
if line.startswith(".error "):
|
||||
raise MakeError(line.removeprefix(".error "))
|
||||
if line.startswith(".include "):
|
||||
includes.append(line.removeprefix(".include ").strip())
|
||||
continue
|
||||
if line.startswith("."):
|
||||
raise SystemExit(f"unsupported Makefile directive at line {line_number}: {line}")
|
||||
assignment = re.fullmatch(
|
||||
r"([A-Za-z_][A-Za-z0-9_.]*)\s*(\?=|\+=|=)\s*(.*)", line
|
||||
)
|
||||
if assignment is None:
|
||||
raise SystemExit(f"unsupported Makefile statement at line {line_number}: {line}")
|
||||
variable, operator, raw_value = assignment.groups()
|
||||
value = expand(raw_value, variables)
|
||||
if operator == "?=" and variable in variables:
|
||||
continue
|
||||
if operator == "+=":
|
||||
old_value = variables.get(variable, "")
|
||||
variables[variable] = " ".join(part for part in (old_value, value) if part)
|
||||
else:
|
||||
variables[variable] = value
|
||||
if variable == "SRCS":
|
||||
src_operations.append((operator, value.split()))
|
||||
if len(active_stack) != 1:
|
||||
raise SystemExit("unterminated Makefile condition")
|
||||
return Evaluation(variables, includes, src_operations)
|
||||
|
||||
|
||||
def evaluate_error(source: str, initial: dict[str, str]) -> str:
|
||||
try:
|
||||
evaluate(source, initial)
|
||||
except MakeError as error:
|
||||
return str(error)
|
||||
raise SystemExit(f"Makefile unexpectedly accepted variables: {initial!r}")
|
||||
|
||||
|
||||
def multiset_list(items: Counter[str]) -> list[str]:
|
||||
return sorted(items.elements())
|
||||
|
||||
|
||||
def object_multiset(sources: list[str]) -> Counter[str]:
|
||||
objects: Counter[str] = Counter()
|
||||
for source in sources:
|
||||
if source.endswith((".c", ".cc", ".cpp", ".S", ".s")):
|
||||
objects[str(Path(source).with_suffix(".o"))] += 1
|
||||
return objects
|
||||
|
||||
|
||||
before = committed("src/Makefile")
|
||||
current = makefile_path.read_text(encoding="utf-8")
|
||||
linux = (root / "src-linux/Makefile").read_text(encoding="utf-8")
|
||||
|
||||
expected_groups = [
|
||||
(
|
||||
"=",
|
||||
[
|
||||
"super.c",
|
||||
"inode.c",
|
||||
"data.c",
|
||||
"namei.c",
|
||||
"dir.c",
|
||||
"erofs_vnops.c",
|
||||
"vnode_if.h",
|
||||
],
|
||||
),
|
||||
("+=", ["xattr.c"]),
|
||||
("+=", ["decompressor.c", "zmap.c", "zdata.c"]),
|
||||
(
|
||||
"+=",
|
||||
[
|
||||
"decompressor_lz4.c",
|
||||
"decompressor_lzma.c",
|
||||
"decompressor_deflate.c",
|
||||
"decompressor_zstd.c",
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
scenario_reports: dict[str, object] = {}
|
||||
for config in ("0", "1"):
|
||||
initial = {
|
||||
"MACHINE_ARCH": "amd64",
|
||||
"SYSDIR": "/freebsd/sys",
|
||||
"WITH_ZSTDIO": config,
|
||||
}
|
||||
before_eval = evaluate(before, initial)
|
||||
current_eval = evaluate(current, initial)
|
||||
before_sources = before_eval.variables.get("SRCS", "").split()
|
||||
current_sources = current_eval.variables.get("SRCS", "").split()
|
||||
before_source_multiset = Counter(before_sources)
|
||||
current_source_multiset = Counter(current_sources)
|
||||
before_objects = object_multiset(before_sources)
|
||||
current_objects = object_multiset(current_sources)
|
||||
before_metadata = Counter(source for source in before_sources if not source.endswith(".c"))
|
||||
current_metadata = Counter(source for source in current_sources if not source.endswith(".c"))
|
||||
|
||||
if current_eval.src_operations != expected_groups:
|
||||
raise SystemExit(
|
||||
f"B34 semantic SRCS groups differ for zstdio{config}: "
|
||||
f"{current_eval.src_operations!r}"
|
||||
)
|
||||
if current_source_multiset != before_source_multiset:
|
||||
raise SystemExit(f"B34 SRCS multiset changed for zstdio{config}")
|
||||
if current_objects != before_objects:
|
||||
raise SystemExit(f"B34 object multiset changed for zstdio{config}")
|
||||
if current_metadata != before_metadata or current_metadata != Counter({"vnode_if.h": 1}):
|
||||
raise SystemExit(f"B34 vnode_if handling changed for zstdio{config}")
|
||||
if current_eval.includes != before_eval.includes or current_eval.includes != ["<bsd.kmod.mk>"]:
|
||||
raise SystemExit(f"B34 bsd.kmod.mk ownership changed for zstdio{config}")
|
||||
if (
|
||||
current_eval.variables.get("KMOD") != before_eval.variables.get("KMOD")
|
||||
or current_eval.variables.get("KMOD") != "erofs"
|
||||
):
|
||||
raise SystemExit(f"B34 KMOD ownership changed for zstdio{config}")
|
||||
cflags_name = "CFLAGS.decompressor_zstd.c"
|
||||
if current_eval.variables.get(cflags_name) != before_eval.variables.get(cflags_name):
|
||||
raise SystemExit(f"B34 Zstd flags changed for zstdio{config}")
|
||||
expected_flags = "-I/freebsd/sys/contrib/zstd/lib/freebsd"
|
||||
if config == "1":
|
||||
expected_flags += " -DZSTDIO"
|
||||
if current_eval.variables.get(cflags_name) != expected_flags:
|
||||
raise SystemExit(f"B34 ZSTDIO expansion changed for zstdio{config}")
|
||||
|
||||
scenario_reports[f"zstdio{config}"] = {
|
||||
"sources": multiset_list(current_source_multiset),
|
||||
"objects": multiset_list(current_objects),
|
||||
"metadata": multiset_list(current_metadata),
|
||||
"zstd_cflags": current_eval.variables[cflags_name].split(),
|
||||
"includes": current_eval.includes,
|
||||
}
|
||||
|
||||
default_initial = {"MACHINE_ARCH": "amd64", "SYSDIR": "/freebsd/sys"}
|
||||
before_default = evaluate(before, default_initial)
|
||||
current_default = evaluate(current, default_initial)
|
||||
if before_default.variables.get("WITH_ZSTDIO") != "0":
|
||||
raise SystemExit("B34 baseline default is not WITH_ZSTDIO=0")
|
||||
if current_default.variables.get("WITH_ZSTDIO") != "0":
|
||||
raise SystemExit("B34 changed the WITH_ZSTDIO default")
|
||||
before_default_variables = dict(before_default.variables)
|
||||
current_default_variables = dict(current_default.variables)
|
||||
del before_default_variables["SRCS"]
|
||||
del current_default_variables["SRCS"]
|
||||
if current_default_variables != before_default_variables:
|
||||
raise SystemExit("B34 changed a default-config Makefile variable expansion")
|
||||
if current_default.includes != before_default.includes:
|
||||
raise SystemExit("B34 changed a default-config Makefile include")
|
||||
|
||||
invalid_message = "WITH_ZSTDIO must be 0 or 1"
|
||||
for source, label in ((before, "baseline"), (current, "current")):
|
||||
message = evaluate_error(
|
||||
source,
|
||||
{"MACHINE_ARCH": "amd64", "SYSDIR": "/freebsd/sys", "WITH_ZSTDIO": "2"},
|
||||
)
|
||||
if message != invalid_message:
|
||||
raise SystemExit(f"B34 {label} invalid-value contract drifted: {message!r}")
|
||||
|
||||
arch_message = "erofs supports only MACHINE_ARCH=amd64"
|
||||
for config in ("0", "1"):
|
||||
initial = {
|
||||
"MACHINE_ARCH": "arm64",
|
||||
"SYSDIR": "/freebsd/sys",
|
||||
"WITH_ZSTDIO": config,
|
||||
}
|
||||
before_message = evaluate_error(before, initial)
|
||||
current_message = evaluate_error(current, initial)
|
||||
if before_message != arch_message or current_message != arch_message:
|
||||
raise SystemExit(f"B34 amd64 gate changed for zstdio{config}")
|
||||
|
||||
for forbidden in ("CONFIG_", "obj-", "erofs-objs", "stub"):
|
||||
if forbidden in current:
|
||||
raise SystemExit(f"B34 introduced forbidden Linux/config stub surface: {forbidden}")
|
||||
|
||||
linux_categories = {
|
||||
"core": "erofs-objs :=" in linux,
|
||||
"xattr": "CONFIG_EROFS_FS_XATTR" in linux,
|
||||
"compression": "CONFIG_EROFS_FS_ZIP" in linux,
|
||||
"algorithms": all(
|
||||
token in linux
|
||||
for token in (
|
||||
"CONFIG_EROFS_FS_ZIP_LZMA",
|
||||
"CONFIG_EROFS_FS_ZIP_DEFLATE",
|
||||
"CONFIG_EROFS_FS_ZIP_ZSTD",
|
||||
)
|
||||
),
|
||||
}
|
||||
if not all(linux_categories.values()):
|
||||
raise SystemExit(f"B34 Linux semantic grouping reference drifted: {linux_categories!r}")
|
||||
|
||||
report = {
|
||||
"baseline": baseline,
|
||||
"write_set": changed,
|
||||
"groups": [
|
||||
{"operator": operator, "sources": sources}
|
||||
for operator, sources in expected_groups
|
||||
],
|
||||
"configurations": scenario_reports,
|
||||
"default_with_zstdio": current_default.variables["WITH_ZSTDIO"],
|
||||
"invalid_with_zstdio_error": invalid_message,
|
||||
"non_amd64_error": arch_message,
|
||||
"linux_reference_categories": linux_categories,
|
||||
}
|
||||
report_path.write_text(
|
||||
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
for config in ("zstdio0", "zstdio1"):
|
||||
details = scenario_reports[config]
|
||||
print(f"B34 {config} SRCS multiset: {' '.join(details['sources'])}")
|
||||
print(f"B34 {config} object multiset: {' '.join(details['objects'])}")
|
||||
print(f"B34 {config} Zstd flags: {' '.join(details['zstd_cflags'])}")
|
||||
print("B34 default/invalid/amd64 gates: PASS")
|
||||
print("B34 bsd.kmod.mk and vnode_if ownership: PASS")
|
||||
print("B34 object-list equivalence: PASS")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'B34 object-list or conditional expansion audit failed'
|
||||
fi
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
PRE15_SMOKE_CODEC=lz4
|
||||
export PRE15_SMOKE_CODEC
|
||||
exec /bin/sh "$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
PRE15_SMOKE_CODEC=lzma
|
||||
export PRE15_SMOKE_CODEC
|
||||
exec /bin/sh "$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
PRE15_SMOKE_CODEC=plain
|
||||
export PRE15_SMOKE_CODEC
|
||||
exec /bin/sh "$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
PRE15_SMOKE_CODEC=zstd
|
||||
export PRE15_SMOKE_CODEC
|
||||
exec /bin/sh "$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
: "${PRE15_SMOKE_CODEC:?PRE15_SMOKE_CODEC is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
||||
generator=$fixture_dir/SMOKE-generate.py
|
||||
kldsym_probe=$fixture_dir/SMOKE-kldsym.c
|
||||
kld_builder=$fixture_dir/B28-build-kld.sh
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
source_first=$PRE15_CASE_TMP/source-first
|
||||
source_second=$PRE15_CASE_TMP/source-second
|
||||
image=$PRE15_CASE_TMP/SMOKE-$PRE15_SMOKE_CODEC.erofs
|
||||
module=$PRE15_CASE_TMP/SMOKE-$PRE15_SMOKE_CODEC-erofs.ko
|
||||
|
||||
case "$PRE15_SMOKE_CODEC" in
|
||||
plain)
|
||||
config=0
|
||||
uuid=00000000-0000-4000-8000-000000000100
|
||||
mkfs_codec=
|
||||
case_timeout=180
|
||||
;;
|
||||
lz4)
|
||||
config=0
|
||||
uuid=00000000-0000-4000-8000-000000000104
|
||||
mkfs_codec='-zlz4 -C4096'
|
||||
case_timeout=240
|
||||
;;
|
||||
lzma)
|
||||
config=0
|
||||
uuid=00000000-0000-4000-8000-0000000001a0
|
||||
mkfs_codec='-zlzma,level=6,dictsize=65536 -C4096'
|
||||
case_timeout=300
|
||||
;;
|
||||
zstd)
|
||||
config=1
|
||||
uuid=00000000-0000-4000-8000-00000000025d
|
||||
mkfs_codec='-zzstd,level=3,dictsize=65536 -C4096'
|
||||
case_timeout=300
|
||||
;;
|
||||
*) pre15_runner_fail "unknown smoke codec: $PRE15_SMOKE_CODEC" ;;
|
||||
esac
|
||||
|
||||
for tool in clang cmp diff file mkfs.erofs nm python3 scp sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || \
|
||||
pre15_infra_blocked "missing smoke tool: $tool"
|
||||
done
|
||||
for fixture in "$generator" "$kldsym_probe" "$kld_builder" \
|
||||
"$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"; do
|
||||
pre15_record_fixture "smoke-$(basename "$fixture")" "$fixture"
|
||||
done
|
||||
mkdir -p "$artifacts"
|
||||
if ! python3 -B "$generator" --output "$source_first" \
|
||||
>"$artifacts/source-first.json" 2>"$artifacts/source-first.stderr" || \
|
||||
! python3 -B "$generator" --output "$source_second" \
|
||||
>"$artifacts/source-second.json" 2>"$artifacts/source-second.stderr"; then
|
||||
pre15_runner_fail 'smoke fixture generation failed'
|
||||
fi
|
||||
if ! diff -qr "$source_first" "$source_second" \
|
||||
>"$artifacts/source-repeat.diff" || \
|
||||
! cmp "$artifacts/source-first.json" "$artifacts/source-second.json"; then
|
||||
pre15_runner_fail 'smoke source fixture is not byte reproducible'
|
||||
fi
|
||||
if ! timeout -k 5 120 sh -c \
|
||||
"mkfs.erofs -d0 -T0 --all-time --all-root --workers=1 --sort=path -x-1 -U$uuid $mkfs_codec '$image' '$source_first'" \
|
||||
>"$artifacts/mkfs.stdout" 2>"$artifacts/mkfs.stderr"; then
|
||||
pre15_runner_fail "$PRE15_SMOKE_CODEC smoke image generation failed"
|
||||
fi
|
||||
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
||||
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" "$config" \
|
||||
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke cross-target KLD build failed"
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
sha256sum "$image" >"$artifacts/image-sha256.txt"
|
||||
sha256sum "$module" >"$artifacts/kld-sha256.txt"
|
||||
file "$module" >"$artifacts/kld-file.txt"
|
||||
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
|
||||
pre15_target_reached
|
||||
|
||||
test "${PRE15_MODE:-}" = qemu || \
|
||||
pre15_runner_fail 'smoke cases require QEMU mode'
|
||||
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
||||
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
||||
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
||||
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
||||
}
|
||||
|
||||
remote_root=/root/pre15-smoke-$PRE15_SMOKE_CODEC
|
||||
pre15_guest_ssh_bounded rm -rf "$remote_root"
|
||||
pre15_guest_ssh_bounded mkdir -p "$remote_root"
|
||||
if ! pre15_scp "$image" "$remote_root/image.erofs" || \
|
||||
! pre15_scp "$module" "$remote_root/erofs.ko" || \
|
||||
! pre15_scp "$kldsym_probe" "$remote_root/SMOKE-kldsym.c"; then
|
||||
pre15_infra_blocked "$PRE15_SMOKE_CODEC smoke transfer failed"
|
||||
fi
|
||||
if test "$PRE15_SMOKE_CODEC" = zstd; then
|
||||
if ! pre15_guest_ssh_bounded \
|
||||
"cc -O2 -Wall -Wextra -Werror -std=c17 '$remote_root/SMOKE-kldsym.c' -o '$remote_root/SMOKE-kldsym'"; then
|
||||
pre15_infra_blocked 'could not compile guest ZSTDIO capability probe'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded "$remote_root/SMOKE-kldsym" \
|
||||
ZSTD_DCtx_setParameter ZSTD_createDCtx_advanced \
|
||||
ZSTD_decompressStream ZSTD_freeDCtx ZSTD_isError \
|
||||
>"$artifacts/zstdio-capability.txt" 2>"$artifacts/zstdio-capability.stderr"; then
|
||||
pre15_infra_blocked 'running guest kernel lacks required ZSTDIO symbols'
|
||||
fi
|
||||
fi
|
||||
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
if ! pre15_guest_ssh_bounded kldload "$remote_root/erofs.ko" \
|
||||
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
|
||||
if grep -q 'module already loaded or in kernel' \
|
||||
"$artifacts/kldload.stderr"; then
|
||||
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
||||
fi
|
||||
if test "$PRE15_SMOKE_CODEC" = zstd && \
|
||||
grep -Eqi 'link_elf|symbol|not defined' "$artifacts/kldload.stderr"; then
|
||||
pre15_infra_blocked 'zstdio1 EROFS KLD cannot load on this guest kernel'
|
||||
fi
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke KLD failed to load"
|
||||
fi
|
||||
pre15_own_guest_kld erofs "$PRE15_SMOKE_CODEC smoke KLD"
|
||||
smoke_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "$remote_root/image.erofs") || \
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke md attach failed"
|
||||
case "$smoke_md" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected smoke md unit: $smoke_md" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$smoke_md" "$PRE15_SMOKE_CODEC smoke md"
|
||||
smoke_mount=/mnt/pre15-smoke-$PRE15_SMOKE_CODEC
|
||||
pre15_guest_ssh_bounded mkdir -p "$smoke_mount"
|
||||
if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$smoke_md" "$smoke_mount"; then
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke readonly mount failed"
|
||||
fi
|
||||
pre15_own_guest_mount "$smoke_mount" "$PRE15_SMOKE_CODEC smoke mount"
|
||||
|
||||
python3 -B - "$artifacts/source-first.json" "$artifacts/expected-files.tsv" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
manifest = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
lines = [f"{item['path']}\t{item['size']}\t{item['sha256']}" for item in manifest["files"]]
|
||||
Path(sys.argv[2]).write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
PY
|
||||
while IFS="$(printf '\t')" read -r path expected_size expected_hash; do
|
||||
actual_size=$(pre15_guest_ssh_bounded stat -f %z "$smoke_mount/$path") || \
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke stat failed: $path"
|
||||
actual_hash=$(pre15_guest_ssh_bounded sha256 -q "$smoke_mount/$path") || \
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke hash failed: $path"
|
||||
printf '%s\t%s\t%s\n' "$path" "$actual_size" "$actual_hash" \
|
||||
>>"$artifacts/guest-files.tsv"
|
||||
test "$actual_size" = "$expected_size" && test "$actual_hash" = "$expected_hash" || \
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke file mismatch: $path"
|
||||
done <"$artifacts/expected-files.tsv"
|
||||
pre15_guest_ssh_bounded \
|
||||
"find '$smoke_mount' -mindepth 1 -maxdepth 1 -exec basename '{}' ';' | sort" \
|
||||
>"$artifacts/guest-root-entries.txt"
|
||||
printf '%s\n' empty nested payload.bin >"$artifacts/expected-root-entries.txt"
|
||||
cmp "$artifacts/expected-root-entries.txt" "$artifacts/guest-root-entries.txt" || \
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke root readdir set mismatch"
|
||||
|
||||
if test "$PRE15_SMOKE_CODEC" = lz4; then
|
||||
dd if="$source_first/payload.bin" bs=1 skip=4093 count=131071 2>/dev/null | \
|
||||
sha256sum | awk '{print $1}' >"$artifacts/expected-range-sha256.txt"
|
||||
pre15_guest_ssh_bounded \
|
||||
"dd if='$smoke_mount/payload.bin' bs=1 skip=4093 count=131071 2>/dev/null | sha256 -q" \
|
||||
>"$artifacts/guest-range-sha256.txt"
|
||||
cmp "$artifacts/expected-range-sha256.txt" "$artifacts/guest-range-sha256.txt" || \
|
||||
pre15_dut_fail 'LZ4 smoke cross-pcluster partial range mismatch'
|
||||
fi
|
||||
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg.txt"
|
||||
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free' \
|
||||
"$artifacts/guest-dmesg.txt"; then
|
||||
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke observed kernel failure evidence"
|
||||
fi
|
||||
printf '%s\n' \
|
||||
"SMOKE-$PRE15_SMOKE_CODEC PASS mode=zstdio$config deadline=$case_timeout" \
|
||||
'readonly mount, exact file hashes/sizes, readdir set, and owned cleanup exercised'
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_DUT:?PRE15_DUT is required}"
|
||||
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
spec=$PRE15_DUT/tests/pre15/fixtures/B01-xattr-legacy.json
|
||||
helper=$PRE15_DUT/tests/pre15/fixtures/g3.py
|
||||
output=$PRE15_CASE_TMP/xattr-legacy
|
||||
pre15_record_fixture xattr-legacy-contract "$spec"
|
||||
pre15_record_fixture xattr-legacy-generator "$helper"
|
||||
|
||||
if python3 -B "$helper" make-xattr-legacy \
|
||||
--spec "$spec" --output "$output" \
|
||||
--freebsd-root "$PRE15_DUT/src" --linux-root "$PRE15_ROOT/src-linux"; then
|
||||
:
|
||||
else
|
||||
pre15_runner_fail 'legacy xattr fixture generation or independent parsing failed'
|
||||
fi
|
||||
|
||||
mkdir -p "$PRE15_RUN_DIR/artifacts"
|
||||
cp "$output/fixture-manifest.json" \
|
||||
"$PRE15_RUN_DIR/artifacts/TC162-fixture-manifest.json"
|
||||
pre15_record_fixture xattr-legacy-manifest \
|
||||
"$PRE15_RUN_DIR/artifacts/TC162-fixture-manifest.json"
|
||||
python3 - "$PRE15_RUN_DIR/artifacts/TC162-fixture-manifest.json" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
manifest = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
positive = {
|
||||
"legacy-primary": "primary-legacy",
|
||||
"explicit-plain": "primary",
|
||||
"packed-carrier": "packed",
|
||||
"metabox-carrier": "metabox",
|
||||
}
|
||||
for name, carrier in positive.items():
|
||||
item = manifest["results"][name]
|
||||
if item["carrier"] != carrier or not item["checksum_valid"]:
|
||||
raise SystemExit(f"{name}: carrier/checksum oracle failed")
|
||||
if item["base_index"] != 1 or item["infix"] != "repo.pre15.legacy.":
|
||||
raise SystemExit(f"{name}: prefix value differs")
|
||||
for name in ("legacy-length-invalid", "legacy-offset-outside"):
|
||||
item = manifest["results"][name]
|
||||
if item["expected_errno"] != "EINTEGRITY" or not item["checksum_valid"]:
|
||||
raise SystemExit(f"{name}: exact negative oracle failed")
|
||||
print("TC162: legacy, explicit plain, packed, and metabox carriers read one value")
|
||||
print("TC162: single-field negatives return exactly EINTEGRITY")
|
||||
PY
|
||||
|
||||
pre15_target_reached
|
||||
if python3 - "$PRE15_DUT/src/xattr.c" "$PRE15_ROOT/src-linux/xattr.c" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
freebsd = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||
linux = Path(sys.argv[2]).read_text(encoding="utf-8")
|
||||
freebsd_markers = (
|
||||
"prefix_en = NULL;",
|
||||
"EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX",
|
||||
"else if (em->packed_inode != NULL)",
|
||||
"else if (em->packed_nid != 0)",
|
||||
"erofs_xattr_read_metadata(em, prefix_en",
|
||||
)
|
||||
linux_markers = (
|
||||
"bool plain = erofs_sb_has_plain_xattr_pfx(sbi);",
|
||||
"else if (sbi->packed_inode)",
|
||||
"else\n\t\t\tplain = true;",
|
||||
"if (plain)\n\t\t(void)erofs_init_metabuf(&buf, sb, false);",
|
||||
)
|
||||
if not all(marker in freebsd for marker in freebsd_markers):
|
||||
raise SystemExit("FreeBSD legacy primary fallback changed")
|
||||
if not all(marker in linux for marker in linux_markers):
|
||||
raise SystemExit("Linux legacy primary fallback changed")
|
||||
print("TC162: FreeBSD and Linux retain the no-carrier primary fallback")
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
pre15_dut_fail 'legacy xattr primary fallback branch no longer matches the frozen contract'
|
||||
fi
|
||||
Reference in New Issue
Block a user