From cf2f58b604b3adf7dc40ef9e0143a971b898e7cd Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 3 May 2026 04:39:39 +0800 Subject: [PATCH] fix(elfpatch): swap patchelf op order to --set-rpath then --set-interpreter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit patchelf 0.18.0 corrupts compact ELFs (e.g. ninja 1.12.1 at 273 KB) when --set-interpreter runs before --set-rpath. The interp op extends PT_LOAD and shifts the dynamic section; the subsequent rpath op operates on stale offsets, writes through DT_NEEDED, and the loader segfaults at execve+1 with no recovery path (--shrink-rpath / re-set-rpath also fail on the corrupted binary). Reproduction matrix (patchelf 0.18.0 + ninja 1.12.1, fresh from upstream): T1 --set-interpreter only ✓ T2 --set-rpath only ✓ T3 rpath then interp (two calls) ✓ T4 interp then rpath (two calls) ✗ SEGFAULT @ execve+1 T5 one cmd: --set-interpreter --set-rpath ✗ SEGFAULT T6 one cmd: --set-rpath --set-interpreter ✗ SEGFAULT T7 same wrong order on openssl (~700 KB) ✓ (large bin survives) T8 recovery via --shrink-rpath / re-set ✗ unrecoverable Insight: T5 vs T6 — patchelf processes interp before rpath INTERNALLY regardless of CLI argument order, so combined-command form is equivalent to T4. The only safe ordering is two separate invocations with rpath first. Fix: reverse op order in `_patch_elf_executables` (declarative bins mode) and `_patch_elf` fallback (full-scan mode). Why reverse-order is safe: - rpath op only mutates the dynamic section (DT_RUNPATH entry); program headers stay intact - interp op then extends .interp / appends PT_LOAD, but no patchelf op runs after it — so stale offsets don't matter Cross-platform scope: - Linux: this fix - macOS: unaffected (install_name_tool, no equivalent op-order issue) - Windows: unaffected (no patchelf path) Regression test: ApplyElfpatchAuto_LinuxRpathBeforeInterpreter asserts log order via stub patchelf shell script. Catches future accidental reverts. Reported by: mcpp sandbox bring-up — initially traced as "ninja install in 0.4.12 sandbox produces segfaulting binary". Earlier diagnosis hypothesized double-patch corruption, but characterization narrowed it to single-pass order sensitivity in patchelf 0.18.0 itself. Full analysis: docs/plans/2026-05-03-patchelf-order-bug-analysis.md --- .../2026-05-03-patchelf-order-bug-analysis.md | 122 ++++++++++++++++++ src/lua-stdlib/xim/libxpkg/elfpatch.lua | 41 ++++-- src/xpkg-lua-stdlib.cppm | 57 +++++--- tests/test_executor.cpp | 82 ++++++++++++ 4 files changed, 270 insertions(+), 32 deletions(-) create mode 100644 docs/plans/2026-05-03-patchelf-order-bug-analysis.md diff --git a/docs/plans/2026-05-03-patchelf-order-bug-analysis.md b/docs/plans/2026-05-03-patchelf-order-bug-analysis.md new file mode 100644 index 0000000..3348754 --- /dev/null +++ b/docs/plans/2026-05-03-patchelf-order-bug-analysis.md @@ -0,0 +1,122 @@ +# patchelf 0.18.0 op-order corruption on compact ELFs + +**Date**: 2026-05-03 +**Affects**: `mcpplibs-xpkg` ≤ v0.0.36 — any consumer using `elfpatch` on small ELFs (~280 KB or smaller, e.g. ninja 1.12.1 at 273 KB). +**Reported by**: mcpp sandbox bring-up — `g++` segfault diagnosis traced back to `xlings install ninja`. +**Fix**: ship in v0.0.37 — reverse op order in `_patch_elf_executables` and `_patch_elf` fallback so `--set-rpath` runs before `--set-interpreter`. + +## Symptom + +After `xlings install ninja` on a fresh sandbox using `xlings 0.4.12` (predicate-driven elfpatch enabled by default), the resulting ninja binary segfaults at execve+1: + +``` +$ ninja --version +Segmentation fault (core dumped) +``` + +`readelf -a` on the broken binary shows: + +- ✅ `PT_INTERP` correct (points at sandbox `xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2`) +- ❌ `RUNPATH` empty — but xlings's elfpatch DID call `--set-rpath` +- ❌ `DT_NEEDED` entries missing — dynamic section corrupted +- File size 273 KB → 282 KB (patchelf grew it ~8 KB) + +## Reproduction + +Use the patchelf shipped via `xim:patchelf@0.18.0` and a fresh ninja 1.12.1 binary from upstream GitHub release. No xlings code involved — this is a pure patchelf characterization. + +| # | patchelf invocation(s) | Result | +|---|---|---| +| T1 | `--set-interpreter ` only | ✅ binary OK | +| T2 | `--set-rpath ` only | ✅ binary OK | +| T3 | `--set-rpath ` then `--set-interpreter ` (two invocations) | ✅ binary OK | +| **T4** | `--set-interpreter ` then `--set-rpath ` (two invocations) | ❌ **segfault @ execve+1** | +| **T5** | `--set-interpreter --set-rpath ` (single command) | ❌ **segfault @ execve+1** | +| **T6** | `--set-rpath --set-interpreter ` (single command, reversed CLI args) | ❌ **segfault @ execve+1** | +| T8 | After T4 corruption, run `--shrink-rpath` or `--set-rpath ` again to "fix" | ❌ unrecoverable | + +Key observations: + +- **T5 vs T6**: putting `--set-rpath` first in CLI args does NOT help. `patchelf` processes interp before rpath internally regardless of the CLI arg order, so combined-command form is equivalent to T4. +- **T7** (not in matrix): the same wrong order on a larger binary like `openssl` (~700 KB) leaves the binary intact. The bug only triggers on compact ELFs whose program-header padding is too tight to absorb the `.interp` section growth. +- **T8**: once corrupted, no patchelf operation recovers the binary. DT_NEEDED is gone; the loader can't resolve symbols at all. + +## Why this only surfaces under `xlings 0.4.12+` + +The 0.4.10-era xlings did **not** auto-patchelf consumer binaries. Migration to predicate-driven elfpatch (xlings 0.4.11, then fixed in 0.4.12) means xlings now runs patchelf on **every** ELF in install_dir whenever a runtime dep declares `exports.runtime.loader` (e.g. ninja → glibc). Compact binaries that were previously left alone now trip the order bug. + +The contributor reporting this (mcpp sandbox) had been independently running their own `patchelf_walk` as a second pass for years — when the second pass ran on bins that 0.4.10 had left untouched, only their pass hit ninja and they used the same wrong order. So the bug was reachable before too, just less visible because 0.4.10 + their patch_walk hit fewer binaries. + +## Root cause (patchelf internal) + +`patchelf 0.18.0` processes `--set-interpreter` first within its operation pipeline: + +1. `--set-interpreter` extends the `.interp` section (longer absolute path → larger string). To fit, patchelf appends a new `PT_LOAD` segment at file tail and rewrites program headers. +2. `--set-rpath` then attempts to add a `DT_RUNPATH` entry to the dynamic section. It computes the dynamic section's file offset based on assumptions stale after step 1's program-header rewrite. +3. The rpath write lands at a wrong offset, overwriting `DT_NEEDED` entries — causing the loader to crash trying to walk a malformed dynamic table. + +For binaries with comfortable padding (most release binaries > 500 KB), step 1's PT_LOAD addition fits without disturbing later structures, and step 2's stale offset still happens to be correct. For compact binaries like ninja, the padding is exhausted and offsets shift. + +The order-sensitive nature is patchelf's robustness gap — see [NixOS/patchelf](https://github.com/NixOS/patchelf) issue tracker for similar reports across versions. We don't fix patchelf here; we work around it. + +## Fix + +In `src/lua-stdlib/xim/libxpkg/elfpatch.lua`, reverse op order in two places: + +### `_patch_elf_executables` (declarative bins/libs mode) + +```diff +-if loader and _has_pt_interp(filepath, patch_tool) then +- ok = _exec_ok(... " --set-interpreter " ...) +-end +-if ok and rpath and rpath ~= "" then +- ok = _exec_ok(... " --set-rpath " ...) ++if rpath and rpath ~= "" then ++ ok = _exec_ok(... " --set-rpath " ...) ++end ++if ok and loader and _has_pt_interp(filepath, patch_tool) then ++ ok = _exec_ok(... " --set-interpreter " ...) + end +``` + +### `_patch_elf` fallback (full-scan mode) + +Same reversal applied to the fallback branch. + +### Why reverse-order avoids the bug + +When `--set-rpath` runs first: + +1. rpath modification touches only the dynamic section (DT_RUNPATH entry). Program headers stay intact. +2. `--set-interpreter` then extends `.interp` and appends a PT_LOAD. Even if program-header layout shifts, no further patchelf op runs after this — so no stale-offset write occurs. + +Verified empirically (T3 in the matrix): rpath-first survives ninja 1.12.1 cleanly. + +## Regression test + +`tests/test_executor.cpp::ApplyElfpatchAuto_LinuxRpathBeforeInterpreter`: + +- Stub `patchelf` shell script logs every invocation +- Run `apply_elfpatch_auto` against a minimal ELF +- Read log, assert `--set-rpath` line index < `--set-interpreter` line index + +If a future change accidentally reverts the order, this test fails immediately with a clear message. + +## Cross-platform scope + +| Platform | Affected | Reason | +|---|---|---| +| Linux | YES (this fix) | patchelf-based path | +| macOS | NO | uses `install_name_tool` which has no equivalent op-order interaction | +| Windows | NO | no patchelf path; `M._apply` early-bails | + +## Ship plan + +1. Land this fix as libxpkg PR (this branch) +2. Tag `v0.0.37` after merge +3. Register in `mcpplibs-index` +4. Bump xlings's `add_requires("mcpplibs-xpkg X")` in a follow-up PR (no xlings release yet — defer to next 0.4.x release window) + +## Upstream patchelf + +This is patchelf's robustness gap. Filing an upstream bug is out-of-scope for this PR but worth doing — note that the trigger isn't just "compact binary" but also "two sequential ops where one mutates program headers". A defensive patchelf would re-resolve dynamic-section offsets after every PT_LOAD modification. diff --git a/src/lua-stdlib/xim/libxpkg/elfpatch.lua b/src/lua-stdlib/xim/libxpkg/elfpatch.lua index ddb783f..4e9996f 100644 --- a/src/lua-stdlib/xim/libxpkg/elfpatch.lua +++ b/src/lua-stdlib/xim/libxpkg/elfpatch.lua @@ -377,6 +377,18 @@ end -- Patch directories as executables (interpreter + rpath). Files without -- PT_INTERP (shared libs that happened to land in a bin dir, static -- binaries) get rpath-only treatment instead of failing the whole entry. +-- +-- Op order: --set-rpath FIRST, then --set-interpreter. patchelf 0.18.0 +-- has an order-sensitive corruption bug on compact ELFs (≤ ~280 KB, +-- e.g. ninja 1.12.1 at 273 KB): when --set-interpreter runs first, it +-- extends PT_LOAD and shifts the dynamic section; the subsequent +-- --set-rpath operates on stale offsets and writes through DT_NEEDED, +-- corrupting the binary irreversibly (loader segfaults at execve+1). +-- Note this is patchelf's INTERNAL processing order — passing both +-- flags in a single command also fails because patchelf processes +-- interp before rpath internally regardless of CLI order. The +-- workaround is two separate invocations in reverse order. +-- See docs/plans/2026-05-03-patchelf-order-bug-analysis.md. local function _patch_elf_executables(patch_tool, dirs, install_dir, loader, rpath, shrink, result) for _, dir in ipairs(dirs) do local full = path.is_absolute(dir) and dir or path.join(install_dir, dir) @@ -384,14 +396,14 @@ local function _patch_elf_executables(patch_tool, dirs, install_dir, loader, rpa for _, filepath in ipairs(targets) do result.scanned = result.scanned + 1 local ok = true - if loader and _has_pt_interp(filepath, patch_tool) then + if rpath and rpath ~= "" then ok = _exec_ok(_shell_quote(patch_tool.program) - .. " --set-interpreter " .. _shell_quote(loader) + .. " --set-rpath " .. _shell_quote(rpath) .. " " .. _shell_quote(filepath)) end - if ok and rpath and rpath ~= "" then + if ok and loader and _has_pt_interp(filepath, patch_tool) then ok = _exec_ok(_shell_quote(patch_tool.program) - .. " --set-rpath " .. _shell_quote(rpath) + .. " --set-interpreter " .. _shell_quote(loader) .. " " .. _shell_quote(filepath)) end if ok then @@ -473,6 +485,11 @@ local function _patch_elf(target, opts, result) -- legitimately have no INTERP segment, causing patchelf to exit 1 -- and log noise). Files with INTERP get loader + rpath; files -- without get rpath only. + -- + -- Op order: --set-rpath FIRST, then --set-interpreter. See + -- _patch_elf_executables comment for why (patchelf 0.18.0 small- + -- ELF corruption bug) and docs/plans/2026-05-03-patchelf-order- + -- bug-analysis.md for full analysis. _info("fallback scan mode, loader=" .. tostring(loader)) local targets = _collect_targets(target, opts) for _, filepath in ipairs(targets) do @@ -480,6 +497,13 @@ local function _patch_elf(target, opts, result) local any_ok = false local has_interp = _has_pt_interp(filepath, patch_tool) + if rpath and rpath ~= "" then + if _exec_ok(_shell_quote(patch_tool.program) + .. " --set-rpath " .. _shell_quote(rpath) + .. " " .. _shell_quote(filepath)) then + any_ok = true + end + end if loader and has_interp then if _exec_ok(_shell_quote(patch_tool.program) .. " --set-interpreter " .. _shell_quote(loader) @@ -489,16 +513,9 @@ local function _patch_elf(target, opts, result) elseif loader and not has_interp then -- Shared library / static binary: skip interp set silently; -- still consider it for rpath. Don't penalize the patched - -- count if rpath also succeeds below. + -- count if rpath alone succeeded above. any_ok = true end - if rpath and rpath ~= "" then - if _exec_ok(_shell_quote(patch_tool.program) - .. " --set-rpath " .. _shell_quote(rpath) - .. " " .. _shell_quote(filepath)) then - any_ok = true - end - end if any_ok then result.patched = result.patched + 1 diff --git a/src/xpkg-lua-stdlib.cppm b/src/xpkg-lua-stdlib.cppm index a73a967..f4e62dd 100644 --- a/src/xpkg-lua-stdlib.cppm +++ b/src/xpkg-lua-stdlib.cppm @@ -1356,6 +1356,18 @@ end -- Patch directories as executables (interpreter + rpath). Files without -- PT_INTERP (shared libs that happened to land in a bin dir, static -- binaries) get rpath-only treatment instead of failing the whole entry. +-- +-- Op order: --set-rpath FIRST, then --set-interpreter. patchelf 0.18.0 +-- has an order-sensitive corruption bug on compact ELFs (≤ ~280 KB, +-- e.g. ninja 1.12.1 at 273 KB): when --set-interpreter runs first, it +-- extends PT_LOAD and shifts the dynamic section; the subsequent +-- --set-rpath operates on stale offsets and writes through DT_NEEDED, +-- corrupting the binary irreversibly (loader segfaults at execve+1). +-- Note this is patchelf's INTERNAL processing order — passing both +-- flags in a single command also fails because patchelf processes +-- interp before rpath internally regardless of CLI order. The +-- workaround is two separate invocations in reverse order. +-- See docs/plans/2026-05-03-patchelf-order-bug-analysis.md. local function _patch_elf_executables(patch_tool, dirs, install_dir, loader, rpath, shrink, result) for _, dir in ipairs(dirs) do local full = path.is_absolute(dir) and dir or path.join(install_dir, dir) @@ -1363,14 +1375,14 @@ local function _patch_elf_executables(patch_tool, dirs, install_dir, loader, rpa for _, filepath in ipairs(targets) do result.scanned = result.scanned + 1 local ok = true - if loader and _has_pt_interp(filepath, patch_tool) then + if rpath and rpath ~= "" then ok = _exec_ok(_shell_quote(patch_tool.program) - .. " --set-interpreter " .. _shell_quote(loader) + .. " --set-rpath " .. _shell_quote(rpath) .. " " .. _shell_quote(filepath)) end - if ok and rpath and rpath ~= "" then + if ok and loader and _has_pt_interp(filepath, patch_tool) then ok = _exec_ok(_shell_quote(patch_tool.program) - .. " --set-rpath " .. _shell_quote(rpath) + .. " --set-interpreter " .. _shell_quote(loader) .. " " .. _shell_quote(filepath)) end if ok then @@ -1393,6 +1405,10 @@ local function _patch_elf_libraries(patch_tool, dirs, install_dir, rpath, shrink local ok = true if rpath and rpath ~= "" then ok = _exec_ok(_shell_quote(patch_tool.program) +)__LUA__"; + +inline constexpr std::string_view elfpatch_lua_1 = R"__LUA__( + .. " --set-rpath " .. _shell_quote(rpath) .. " " .. _shell_quote(filepath)) end @@ -1416,10 +1432,6 @@ local function _patch_elf(target, opts, result) local loader = _resolve_loader(opts.loader) local rpath = _normalize_rpath(opts.rpath) if opts.loader and not loader then -)__LUA__"; - -inline constexpr std::string_view elfpatch_lua_1 = R"__LUA__( - local msg = "cannot resolve loader: " .. tostring(opts.loader) if opts.strict then error(msg) @@ -1456,6 +1468,11 @@ inline constexpr std::string_view elfpatch_lua_1 = R"__LUA__( -- legitimately have no INTERP segment, causing patchelf to exit 1 -- and log noise). Files with INTERP get loader + rpath; files -- without get rpath only. + -- + -- Op order: --set-rpath FIRST, then --set-interpreter. See + -- _patch_elf_executables comment for why (patchelf 0.18.0 small- + -- ELF corruption bug) and docs/plans/2026-05-03-patchelf-order- + -- bug-analysis.md for full analysis. _info("fallback scan mode, loader=" .. tostring(loader)) local targets = _collect_targets(target, opts) for _, filepath in ipairs(targets) do @@ -1463,6 +1480,13 @@ inline constexpr std::string_view elfpatch_lua_1 = R"__LUA__( local any_ok = false local has_interp = _has_pt_interp(filepath, patch_tool) + if rpath and rpath ~= "" then + if _exec_ok(_shell_quote(patch_tool.program) + .. " --set-rpath " .. _shell_quote(rpath) + .. " " .. _shell_quote(filepath)) then + any_ok = true + end + end if loader and has_interp then if _exec_ok(_shell_quote(patch_tool.program) .. " --set-interpreter " .. _shell_quote(loader) @@ -1472,16 +1496,9 @@ inline constexpr std::string_view elfpatch_lua_1 = R"__LUA__( elseif loader and not has_interp then -- Shared library / static binary: skip interp set silently; -- still consider it for rpath. Don't penalize the patched - -- count if rpath also succeeds below. + -- count if rpath alone succeeded above. any_ok = true end - if rpath and rpath ~= "" then - if _exec_ok(_shell_quote(patch_tool.program) - .. " --set-rpath " .. _shell_quote(rpath) - .. " " .. _shell_quote(filepath)) then - any_ok = true - end - end if any_ok then result.patched = result.patched + 1 @@ -1752,6 +1769,10 @@ function M.auto(enable_or_opts) else _RUNTIME.elfpatch_legacy_auto = (enable_or_opts == true) end +)__LUA__"; + +inline constexpr std::string_view elfpatch_lua_2 = R"__LUA__( + return _RUNTIME.elfpatch_legacy_auto end @@ -1772,10 +1793,6 @@ function M._apply() -- macosx → Mach-O + install_name_tool: RPATH only; INTERP irrelevant -- (dyld is the kernel's responsibility, no per-binary loader). -- Predicate currently keys off `loader` so it's a no-op on -)__LUA__"; - -inline constexpr std::string_view elfpatch_lua_2 = R"__LUA__( - -- macosx unless a dep declares one — which is correct since -- macOS deps shouldn't declare `loader`. Use elfpatch.set({ -- rpath = {...} }) explicitly if rpath-only patching needed. diff --git a/tests/test_executor.cpp b/tests/test_executor.cpp index ceb7c13..8e5989a 100644 --- a/tests/test_executor.cpp +++ b/tests/test_executor.cpp @@ -435,6 +435,88 @@ TEST(ExecutorTest, ApplyElfpatchAuto_LinuxUsesPatchelfForElf) { fs::remove_all(temp_dir); } +// Regression: patchelf 0.18.0 corrupts compact ELFs (e.g. ninja 1.12.1 +// at 273 KB) when --set-interpreter runs before --set-rpath. The interp +// op extends PT_LOAD and shifts the dynamic section; the subsequent +// rpath op operates on stale offsets and writes through DT_NEEDED, +// segfaulting the binary at execve+1. Workaround: rpath must run first, +// interp second. See docs/plans/2026-05-03-patchelf-order-bug-analysis.md. +TEST(ExecutorTest, ApplyElfpatchAuto_LinuxRpathBeforeInterpreter) { +#ifdef _WIN32 + GTEST_SKIP() << "Linux tool emulation test is POSIX-specific"; +#endif + + const fs::path temp_dir = make_temp_dir("libxpkg-elfpatch-order-"); + const fs::path tools_dir = temp_dir / "tools"; + const fs::path install_dir = temp_dir / "install"; + const fs::path lib_dir = install_dir / "lib"; + const fs::path log_path = temp_dir / "tool.log"; + const fs::path pkg_path = temp_dir / "elfpatch-order.lua"; + const fs::path binary_path = install_dir / "demo-bin"; + + fs::create_directories(tools_dir); + fs::create_directories(lib_dir); + + // Fake patchelf logs every invocation; --print-interpreter returns + // a non-empty string so _has_pt_interp treats the file as having + // PT_INTERP (we want to exercise both ops on the same file). + write_executable_script(tools_dir / "patchelf", + "#!/bin/sh\n" + "if [ \"$1\" = \"--print-interpreter\" ]; then\n" + " echo /lib64/ld-linux-x86-64.so.2\n" + " exit 0\n" + "fi\n" + "printf 'patchelf %s\\n' \"$*\" >> \"$ELFPATCH_LOG\"\n"); + + { + std::ofstream binary(binary_path, std::ios::binary); + ASSERT_TRUE(binary.good()); + const unsigned char magic[] = {0x7f, 'E', 'L', 'F', 0, 0, 0, 0}; + binary.write(reinterpret_cast(magic), sizeof(magic)); + } + fs::permissions(binary_path, + fs::perms::owner_read | fs::perms::owner_write | fs::perms::owner_exec, + fs::perm_options::replace); + + write_text(pkg_path, + "package = { spec = \"1\", name = \"elfpatch-order\", xpm = { linux = { [\"latest\"] = { ref = \"1.0.0\" }, [\"1.0.0\"] = { url = \"https://example.com/demo.tar.gz\", sha256 = \"0\" } } } }\n" + "local elfpatch = import(\"xim.libxpkg.elfpatch\")\n" + "function install()\n" + // explicit interpreter so we bypass _resolve_loader's "subos" + // default (the system probe fails in this hermetic test env); + // we just need both --set-rpath and --set-interpreter to fire. + " elfpatch.auto({ enable = true, interpreter = \"/lib64/ld-linux-x86-64.so.2\" })\n" + " return true\n" + "end\n"); + + const std::string original_path = std::getenv("PATH") ? std::getenv("PATH") : ""; + ScopedEnvVar path_env("PATH", tools_dir.string() + ":" + original_path); + ScopedEnvVar log_env("ELFPATCH_LOG", log_path.string()); + + auto exec = create_executor(pkg_path); + ASSERT_TRUE(exec.has_value()) << (exec ? "" : exec.error()); + + auto hook_result = exec->run_hook(HookType::Install, make_context(install_dir, "linux", tools_dir)); + ASSERT_TRUE(hook_result.success) << hook_result.error; + auto patch_result = exec->apply_elfpatch_auto(); + ASSERT_TRUE(patch_result.success) << patch_result.error; + + std::ifstream log_file(log_path); + std::ostringstream log_buffer; + log_buffer << log_file.rdbuf(); + const std::string log = log_buffer.str(); + + auto rpath_pos = log.find("--set-rpath"); + auto interp_pos = log.find("--set-interpreter"); + ASSERT_NE(rpath_pos, std::string::npos) << "expected --set-rpath in log; got:\n" << log; + ASSERT_NE(interp_pos, std::string::npos) << "expected --set-interpreter in log; got:\n" << log; + EXPECT_LT(rpath_pos, interp_pos) + << "--set-rpath must be invoked BEFORE --set-interpreter to avoid " + "patchelf 0.18.0's compact-ELF corruption bug. Log:\n" << log; + + fs::remove_all(temp_dir); +} + TEST(ExecutorTest, ApplyElfpatchAuto_MacOsUsesInstallNameToolForMachO) { #ifdef _WIN32 GTEST_SKIP() << "macOS tool emulation test is POSIX-specific";