From 06577a241552fbee1f03d7d21393e1563163fa7d Mon Sep 17 00:00:00 2001 From: Shuai Mu Date: Fri, 29 May 2026 23:34:10 -0400 Subject: [PATCH] vec_port: pin extract_if layout mismatch audit --- docs/rusty-std-book.md | 20 +++ tests/vec_port/run_extract_if_check.sh | 161 +++++++++++++++++++++++++ tests/vec_port/vec_extract_if_test.cpp | 46 +++++++ 3 files changed, 227 insertions(+) create mode 100755 tests/vec_port/run_extract_if_check.sh create mode 100644 tests/vec_port/vec_extract_if_test.cpp diff --git a/docs/rusty-std-book.md b/docs/rusty-std-book.md index 7c12a618..03705703 100644 --- a/docs/rusty-std-book.md +++ b/docs/rusty-std-book.md @@ -2388,6 +2388,26 @@ Closed since last revision: forward-decl approach impossible (the merge is the right resolution). Test: `docs/vec_port/vec_extract_if_test.cpp`. + Root cause audit (TODO-005): commit `0680e4c` correctly isolated + the crash to a value-type mismatch, not sizeof/alignment or vtable + shape. The generated `ExtractIf` body came from + `vec_port.vec.extract_if.cppm`, where `rusty::Vec` resolves to + the hand-written compatibility alias in `include/rusty/vec.hpp` + (`VecLegacy`, fields `data_`, `size_`, `capacity_`). The object + passed by `Vec::extract_if` is the transpiled module-local + `vec_port::Vec` (`RawVec buf` plus `len_field`), so the + old reinterpret-cast made `ExtractIf::new_` call `VecLegacy::set_len` + through the wrong layout. `drain` masked the same class of bug on + full-range tests because its destructor could short-circuit before + touching the parent vec; partial drain and `extract_if` both need the + real parent layout. The viable shape is therefore the `8a8154f` + merge: inject the aux-module content into `vec.cppm` under the same + C++20 module attachment and rewrite `rusty::Vec` to the local + transpiled `Vec`. This is now pinned by + `tests/vec_port/run_extract_if_check.sh`, which rebuilds the port + from rustc sources and runs `tests/vec_port/vec_extract_if_test.cpp` + under ASAN. + Two ancillary fixes landed with the merge: - `slice_ext::range` in `include/rusty/slice.hpp` now also detects `r.end_value()` on `rusty::range` (in addition to the `.end` diff --git a/tests/vec_port/run_extract_if_check.sh b/tests/vec_port/run_extract_if_check.sh new file mode 100755 index 00000000..3916b7c8 --- /dev/null +++ b/tests/vec_port/run_extract_if_check.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# End-to-end ASAN check for the transpiled vec_port::Vec::extract_if path. +# +# This intentionally builds a fresh vec_port module from rustc's alloc sources +# instead of relying on stale /tmp artifacts. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +WORK_DIR="${TMPDIR:-/tmp}/rusty-vec-port-extract-if" +KEEP_WORK_DIR=0 +CXX="${CXX:-clang++}" + +print_usage() { + cat < Work directory for generated vec_port artifacts + (default: ${WORK_DIR}) + --keep-work-dir Keep generated artifacts after a successful run + --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --work-dir) + if [[ $# -lt 2 ]]; then + echo "error: --work-dir requires a value" >&2 + exit 2 + fi + WORK_DIR="$2" + shift 2 + ;; + --keep-work-dir) + KEEP_WORK_DIR=1 + shift + ;; + --help|-h) + print_usage + exit 0 + ;; + *) + echo "error: unknown option '$1'" >&2 + print_usage >&2 + exit 2 + ;; + esac +done + +if ! command -v "${CXX}" >/dev/null 2>&1; then + echo "error: CXX compiler '${CXX}' not found; clang++ with C++20 modules is required" >&2 + exit 1 +fi + +SYSROOT="$(rustc --print sysroot)" +RUST_SRC="${SYSROOT}/lib/rustlib/src/rust/library/alloc/src" +if [[ ! -d "${RUST_SRC}/vec" || ! -d "${RUST_SRC}/raw_vec" ]]; then + echo "error: rust-src component not found under ${RUST_SRC}" >&2 + echo "hint: rustup component add rust-src" >&2 + exit 1 +fi + +VEC_CRATE="${WORK_DIR}/vec_crate" +CPP_OUT="${WORK_DIR}/cpp_out" +BUILD_DIR="${CPP_OUT}/build" +LOG_DIR="${WORK_DIR}/logs" + +run_logged() { + local name="$1" + shift + local log="${LOG_DIR}/${name}.log" + echo "[vec_port] ${name}" + if ! "$@" >"${log}" 2>&1; then + echo "error: ${name} failed; showing last 120 log lines" >&2 + tail -n 120 "${log}" >&2 || true + exit 1 + fi +} + +rm -rf "${WORK_DIR}" +mkdir -p "${VEC_CRATE}/src" "${LOG_DIR}" + +cp -r "${RUST_SRC}/vec" "${VEC_CRATE}/src/vec" +cp -r "${RUST_SRC}/raw_vec" "${VEC_CRATE}/src/raw_vec" + +run_logged prep \ + bash "${REPO_ROOT}/docs/vec_port/prep.sh" \ + "${VEC_CRATE}/src/vec" \ + "${VEC_CRATE}/src/raw_vec" + +cat > "${VEC_CRATE}/Cargo.toml" <<'EOF' +[package] +name = "vec_port" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/lib.rs" +EOF + +cat > "${VEC_CRATE}/src/lib.rs" <<'EOF' +#![allow(unused)] +pub mod vec; +pub mod raw_vec; +EOF + +run_logged transpile \ + cargo run --manifest-path "${REPO_ROOT}/Cargo.toml" -p rusty-cpp-transpiler -- \ + --crate "${VEC_CRATE}/Cargo.toml" \ + --output-dir "${CPP_OUT}" + +run_logged post_transpile_patch \ + python3 "${REPO_ROOT}/docs/vec_port/post_transpile_patch.py" "${CPP_OUT}" + +if ! grep -q "vec_port: extract_if content merged" "${CPP_OUT}/vec_port.vec.cppm"; then + echo "error: extract_if content was not merged into vec_port.vec.cppm" >&2 + exit 1 +fi +if ! awk ' + /struct ExtractIf \{/ { in_extract_if = 1 } + in_extract_if && /Vec& vec;/ { found_local_vec = 1 } + in_extract_if && /^};/ { done = 1; exit } + END { exit (done && found_local_vec) ? 0 : 1 } +' "${CPP_OUT}/vec_port.vec.cppm"; then + echo "error: ExtractIf does not hold the local transpiled Vec&" >&2 + exit 1 +fi + +ASAN_FLAGS="-I${REPO_ROOT}/include -std=c++23 -fsanitize=address -fno-omit-frame-pointer" +run_logged cmake_configure \ + cmake -B "${BUILD_DIR}" -S "${CPP_OUT}" -G Ninja \ + -DCMAKE_CXX_COMPILER="${CXX}" \ + -DCMAKE_CXX_FLAGS="${ASAN_FLAGS}" \ + -DCMAKE_CXX_STANDARD=23 + +run_logged cmake_build \ + cmake --build "${BUILD_DIR}" -- -j2 + +cp "${SCRIPT_DIR}/vec_extract_if_test.cpp" "${CPP_OUT}/vec_extract_if_test.cpp" +run_logged compile_extract_if_test \ + "${CXX}" -std=c++23 -fsanitize=address -fno-omit-frame-pointer \ + -I"${REPO_ROOT}/include" \ + -fprebuilt-module-path="${BUILD_DIR}/CMakeFiles/vec_port.dir" \ + -x c++ "${CPP_OUT}/vec_extract_if_test.cpp" \ + -x none "${BUILD_DIR}/libvec_port.a" \ + -o "${CPP_OUT}/vec_extract_if_test_asan" + +run_logged run_extract_if_test \ + env ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ + "${CPP_OUT}/vec_extract_if_test_asan" + +cat "${LOG_DIR}/run_extract_if_test.log" + +if [[ "${KEEP_WORK_DIR}" == "0" ]]; then + rm -rf "${WORK_DIR}" +fi + +echo "PASS: vec_port extract_if is ASAN-clean" diff --git a/tests/vec_port/vec_extract_if_test.cpp b/tests/vec_port/vec_extract_if_test.cpp new file mode 100644 index 00000000..86d7156a --- /dev/null +++ b/tests/vec_port/vec_extract_if_test.cpp @@ -0,0 +1,46 @@ +import vec_port; + +#include +#include +#include + +#define CHECK(cond, msg) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL: %s\n", msg); \ + std::exit(1); \ + } \ + } while (0) + +int main() { + auto v = Vec::new_in(rusty::alloc::Global{}); + for (int i = 1; i <= 6; ++i) { + v.push(i); + } + + { + auto ei = v.extract_if(rusty::range_from(static_cast(0)), + [](int& x) { return x % 2 == 0; }); + int count = 0; + int expected[3] = {2, 4, 6}; + while (true) { + auto next = ei.next(); + if (next.is_none()) { + break; + } + int val = next.unwrap(); + CHECK(count < 3, "extract_if yielded too many values"); + CHECK(val == expected[count], "extract_if yielded expected value"); + ++count; + } + CHECK(count == 3, "extract_if extracted exactly three values"); + } + + auto s = v.as_slice(); + CHECK(s.size() == 3, "extract_if left three values behind"); + CHECK(s[0] == 1 && s[1] == 3 && s[2] == 5, + "extract_if kept the non-matching values"); + + std::printf("ALL CHECKS PASSED\n"); + return 0; +}