Research-2102: SYCL Clang-Tidy Wrapper Executable Path Resolution for Safe Subprocess¶
Question¶
Why did make tidy-ratchet LANE=sycl fail with CommandValidationError: allowlisted executable must be bare or absolute: scripts/ci/clang-tidy-sycl.sh, and how do we durably guarantee that the SYCL clang-tidy wrapper can be invoked across arbitrary worktrees and working directories under safe_subprocess?
Sources¶
scripts/lib/safe_subprocess.py: ADR-1270 bounded subprocess execution boundary.docs/adr/1270-bounded-process-execution.md: contract requiring allowlisted executables to be bare or absolute.scripts/ci/tidy-ratchet.py: whole-tree clang-tidy debt ratchet runner.scripts/ci/clang-tidy-sycl.sh: icpx-aware wrapper injecting SYCL headers and flags.Makefile: definition ofTIDY_RATCHET_EXTRA_syclandtidy-ratchettargets.
Findings¶
-
Root cause in
safe_subprocess.pycontract enforcement: ADR-1270 hardened repository subprocess boundaries by validating argument vectors, deadlines, and allowlists. In_parse_allowed_executables()and_resolve_allowed_executable(), any executable token that contains directory components (len(candidate.parts) > 1) and is not absolute is rejected withallowlisted executable must be bare or absolute: <token>to prevent working-directory traversal and path ambiguity. -
Root cause in
Makefileandtidy-ratchet.py:MakefileconfiguredTIDY_RATCHET_EXTRA_sycl := --clang-tidy scripts/ci/clang-tidy-sycl.shas a relative path. Whentidy-ratchet.pyran: shutil.which(args.clang_tidy)returned"scripts/ci/clang-tidy-sycl.sh"(preserving the relative string).measure()calledclang_tidy_version(binary)at startup.clang_tidy_versioninvokedrun_command([binary, "--version"], allowed_executables=(binary,))without resolvingbinary.safe_subprocessimmediately rejected the relative path withCommandValidationError, causingtidy-ratchet.pyto abort with exit code 5 (measurement/output failed: allowlisted executable must be bare or absolute: scripts/ci/clang-tidy-sycl.sh).-
Although
run_one()previously contained an ad-hoc resolution check (if "/" in binary and Path(binary).exists(): binary = str(Path(binary).resolve())), it never ran becauseclang_tidy_version()crashed first, and resolving per-TU inside the thread pool was fragile whencwdwas not the repository root. -
Remediation:
- In
Makefile, anchorTIDY_RATCHET_EXTRA_syclto$(CURDIR)/scripts/ci/clang-tidy-sycl.sh, ensuring that when Make runs from any worktree (including viamake -C <path>), an absolute path anchored to that worktree's root is passed. - In
scripts/ci/tidy-ratchet.py, addresolve_clang_tidy(binary, repo_root):- Absolute paths are preserved.
- Bare executable names (no slashes) are preserved for standard PATH resolution by
safe_subprocess. - Multi-component relative paths are resolved to absolute paths against
Path.cwd()orrepo_root. - Wire
resolve_clang_tidy()acrossmain(),measure(),clang_tidy_version(), andrun_one(). - Update
clang_tidy_version()to catch(OSError, ValueError)so unexpected validation failures report clean version unavailability rather than uncaught crashes.
Verification¶
- Reproduction on parent:
make tidy-ratchet LANE=sycl TIDY_RATCHET_BUILD_DIR=/tmp/test-sycl-buildreproducederror: measurement/output failed: allowlisted executable must be bare or absolute: scripts/ci/clang-tidy-sycl.sh.-
Python unit test calling
ratchet.measure("sycl", build, root, "scripts/ci/fake-clang-tidy.sh", [], 1)failed withCommandValidationError. -
Post-fix verification:
- The focused regression test
test_relative_wrapper_path_in_subdirectory_survives_safe_subprocessand 3D tests inResolveClangTidypass. make tidy-ratchet LANE=sycl TIDY_RATCHET_BUILD_DIR=/tmp/test-sycl-build TIDY_RATCHET_ARGS="--only core/src/cpu.cpp"succeeds (exit code 0), verifyingsafe_subprocessaccepts the wrapper.- Direct CLI invocation
python3 scripts/ci/tidy-ratchet.py --lane sycl --build-dir /tmp/test-sycl-build --clang-tidy scripts/ci/clang-tidy-sycl.sh --only core/src/cpu.cppsucceeds (exit code 0).