Skip to content

fix(bifrost): detect package manager for non-Debian Linux hosts - #709

Open
Jon-Becker wants to merge 1 commit into
mainfrom
jon-becker/fix-installer-arm64
Open

fix(bifrost): detect package manager for non-Debian Linux hosts#709
Jon-Becker wants to merge 1 commit into
mainfrom
jon-becker/fix-installer-arm64

Conversation

@Jon-Becker

Copy link
Copy Markdown
Owner

What changed? Why?

Reproduction: On an ARM64/aarch64 Linux host running Amazon Linux 2023, the bundled bifrost installer aborted before heimdall could build. Its ensure_apt_package helper ran on every Linux host and probed packages with dpkg -l, then installed build dependencies with sudo apt-get install -y. Amazon Linux 2023 ships dnf/yum and has neither dpkg nor apt-get, so the step failed and forced a manual binary-path workaround before the native aarch64 binary (heimdall-cli v0.9.2) could be compiled.

Root cause: The installer's supported-platform contract silently assumed Debian/Ubuntu for all of Linux. Package existence checks (dpkg) and installation (apt-get) were hard-coded, and the Debian-only package names (libssl-dev, build-essential) had no equivalent mapping for other distributions.

Fix: bifrost now detects the host package manager — apt-get, dnf, yum, pacman, zypper, or apk — preferring apt-get so existing Debian/Ubuntu behavior is unchanged. It maps the required OpenSSL development headers and C toolchain to the correct package names per manager and runs that manager's non-interactive install command. If no supported package manager is detected, it prints the dependencies to install manually rather than silently invoking the wrong one. Architecture selection (x86_64/amd64 and aarch64/arm64) for the pre-compiled --binary path was already correct and is unchanged; the release workflow already publishes a linux-arm64 artifact.

Remaining platform limitations: Distribution support is limited to hosts providing one of the six package managers above; anything else falls back to a manual-install message (no automated dependency installation). Live end-to-end installation was verified only for the Amazon Linux 2023 aarch64 reproduction and the preserved Debian apt-get decision — the other package managers are covered by decision-level tests, not live installs on each distro. macOS/other non-Linux hosts continue to assume the toolchain and OpenSSL are provided by the system (or homebrew).

Notes to reviewers

Key changes:

  • bifrost/bifrost — replaced ensure_apt_package with detect_package_manager, build_dependency_packages, package_install_command, and ensure_build_dependencies; added a BASH_SOURCE guard so the script can be sourced by tests without running main.
  • bifrost/tests/bifrost-tests — new regression suite (no .sh extension, since the repo .gitignore excludes *.sh).
  • bifrost/README.md — new "Supported platforms" section; README.md — brief platform-support note.

How has it been tested?

  • bash bifrost/tests/bifrost-tests — 16 assertions, all passing. Tests mock package managers via fake executables on a controlled PATH, so they require no apt-get, root, network, or particular host distribution. Coverage includes: apt-get preferred when multiple managers coexist (Debian preserved); dnf detected on the Amazon Linux 2023 reproduction with the resolved command asserted to never contain apt-get; yum/pacman/zypper/apk detection; unsupported-host yields no command; and the exact Debian package list/install command are unchanged.
  • bash -n syntax check passes on the installer and test script.
  • This change is shell-only and touches no Rust code, so the Rust test/clippy/fmt suites are unaffected; CI still runs them on this PR.

The installer unconditionally assumed a Debian host, probing packages with
`dpkg` and installing build dependencies with `sudo apt-get`. On non-Debian
Linux (e.g. ARM64 Amazon Linux 2023, which ships `dnf`/`yum` and no `apt-get`)
this aborted before heimdall could build, forcing manual binary-path
workarounds.

Detect the host package manager (apt-get, dnf, yum, pacman, zypper, apk),
preferring apt-get so existing Debian/Ubuntu behavior is preserved, and map
the OpenSSL headers + C toolchain to the correct packages for each. When no
supported package manager is found, print actionable guidance instead of
silently invoking the wrong one. Both x86_64 and aarch64 hosts are supported.

Add a sourceable guard plus a network/root-free regression suite covering the
detection and install-command decisions, and document supported platforms.
@github-actions

Copy link
Copy Markdown
Contributor

❌ Eval Report for 4a8537e

Test Case CFG Decompilation
NestedMapping 100 10
NestedMappings 100 30
TransientStorage 100 22
SimpleStorage 100 90
Mapping 100 96
WhileLoop 80 20
SimpleLoop 68 15
NestedLoop 50 20
WETH9 100 68
Events 90 63
Average 88 43
⚠️ 8 eval(s) scoring <70%

NestedMapping (CFG: 100, Decompilation: 10)

Decompilation

{
  "score": 10,
  "summary": "The decompilation fails to capture the fundamental behavior of the contract. Every original function performs a nested-mapping storage write (setAllowance, setGrid, setDeepNested) or read (getAllowance and the auto-generated public getters), requiring keccak-based slot computation and SSTORE/SLOAD operations. The decompiled output contains none of these: all functions are emitted as 'pure' with only tautological requires (e.g. `require(arg0 == arg0)`) and no storage access, no slot calculation, no return values, and no state changes.",
  "differences": [
    "All storage writes are missing: setAllowance, setGrid, and setDeepNested each perform an SSTORE into a nested mapping slot, but no decompiled function writes to storage (all are marked pure).",
    "All storage reads are missing: getAllowance and the auto-generated public getters (allowances, grid, deepNested) perform SLOADs and return values, but no decompiled function reads storage or returns a value.",
    "Nested mapping slot computation (iterated keccak256 hashing of key + slot) is entirely absent from every function.",
    "Return values are lost: getter functions that should return uint256/bool return nothing.",
    "Function bodies are reduced to tautological no-op requires (arg0 == arg0, arg0 == address(arg0)) that do not correspond to any logic in the original source.",
    "State mutability is incorrect: setter functions are non-payable state-mutating in the original but are reported as pure in the decompilation."
  ]
}

NestedMappings (CFG: 100, Decompilation: 30)

Decompilation

{
  "score": 30,
  "summary": "The decompilation fails to capture the core behavior of the contract. The nested mapping structure is lost entirely: both read functions (allowance and the public getter) contain no storage read and no return value, and the approve function omits the msg.sender component of the nested mapping key, storing to a single-level mapping keyed only by the spender.",
  "differences": [
    "allowance(address,address) (selector 0xdd62ed3e) is decompiled as a pure function with only an argument type-check and no body — it neither reads storage nor returns the stored allowance value, losing the entire function logic",
    "The public getter allowances(address,address) (selector 0x55b6ed5c) is likewise decompiled as a pure no-op with no storage read and no return value",
    "approve stores to storage_map_a[spender] using only the spender as the key, dropping the msg.sender part of the nested mapping slot derivation (original writes allowances[msg.sender][spender]); the nested keccak slot computation is not represented",
    "No return values are present for the two view functions, whereas the originals return uint256"
  ]
}

TransientStorage (CFG: 100, Decompilation: 22)

Decompilation

{
  "score": 22,
  "summary": "The decompilation captures only one of six functions with recognizable logic. setTempOwner preserves the transient storage write, but incrementCounter, lock, unlock, getCounter, and isLocked are reduced to malformed constants with no operational logic, losing nearly all functional behavior.",
  "differences": [
    "incrementCounter() is rendered as an empty 'bytes public constant' with no logic; the transient counter read-increment-write (counter = counter + 1) is entirely missing.",
    "lock() is rendered as an empty constant; the transient store of 'true' to locked is missing.",
    "unlock() is rendered as an empty constant; the transient store of 'false' to locked is missing.",
    "getCounter() is rendered as 'uint256 public constant getCounter = 1' instead of reading the transient counter slot and returning it; returns a hardcoded value rather than state.",
    "isLocked() is rendered as a constant bool rather than reading the transient locked slot and returning it; no storage read is preserved.",
    "setTempOwner is marked 'pure' though it performs a transient storage write (should be non-pure/state-modifying), and includes a spurious require(arg0 == address(arg0)) address-mask check plus a bit-packing mask '| uint96(transient[0x01])' that does not correspond to the original simple assignment tempOwner = owner."
  ]
}

WhileLoop (CFG: 80, Decompilation: 20)

Decompilation

{
  "score": 20,
  "summary": "The decompilation fails to capture the core behavior of the function. The original executes a while loop that increments the storage variable `number` once per iteration up to `loops` times. The decompiled output contains no loop, performs no storage write, and is incorrectly marked as `view`. The increment and comparison logic appears only as degenerate require statements rather than actual state changes or control flow.",
  "differences": [
    "The while loop control flow is entirely missing — there is no iteration construct in the decompiled output.",
    "The storage write `number = number + 1` is not preserved; `number` is only read inside a require expression and never assigned, so no state change occurs.",
    "Function mutability is wrong: it is marked `public view` but the original mutates storage (non-view/payable behavior).",
    "The require statements (`require(arg0 == arg0)`, `require(!0 < arg0)`, `require(!number > (number + 0x01))`) do not correspond to any revert conditions in the original and misrepresent the loop's comparison logic as reverts."
  ]
}

SimpleLoop (CFG: 68, Decompilation: 15)

Decompilation

{
  "score": 15,
  "summary": "The decompilation fails to capture the core behavior of the loop function. The original iterates `loops` times, incrementing the storage variable `number` on each pass. The decompiled output contains no loop, performs no storage write to `number`, and is incorrectly marked `view` (non-mutating), so the essential state change and control flow are entirely lost.",
  "differences": [
    "The for-loop iterating over `loops` (arg0) is completely absent from the decompiled output",
    "The storage write incrementing `number` (number++) is missing; the function performs no state change",
    "Function is incorrectly marked `view`, indicating no state mutation, whereas the original modifies storage",
    "Decompiled output contains meaningless tautological/degenerate require statements (require(arg0 == arg0), require(!0 < arg0), a subtraction with no comparison) that do not correspond to any logic in the original"
  ]
}

CFG

{
  "score": 68,
  "summary": "The CFG fully captures the function dispatcher, both function entry points (loop and the number getter), all validation/revert paths, and the for-loop's condition, body-entry, and exit branches. However, the loop's back-edge is missing: the body node completes only to the compiler overflow-panic branch, and the successful continuation (store number, increment i, jump back to the condition) is not represented, so the loop's iteration/repeat structure is incomplete.",
  "missing_paths": [
    "For-loop back-edge: after the body (number++) executes on the non-overflow path (JUMPI target 0x019e), control stores the value, increments i, and jumps back to the loop condition (0x77 / node 7). This iteration edge and the continuation node are absent, so the loop is represented as a forward-only construct rather than a repeating one.",
    "Completion of the number++ body (the SSTORE / write-back of the incremented storage value) on the success path is not present as a reachable node."
  ],
  "extra_paths": [
    "Node 9: 0x11 arithmetic panic revert reachable from the increment overflow check (compiler-added overflow check).",
    "Node 6 / Node 11: reverts from ABI calldata length/SLT validity checks (compiler-added input decoding guards).",
    "Node 1: non-payable CALLVALUE ISZERO revert guard (compiler-added).",
    "Node 14: calldata-size / unknown-selector fallback revert (compiler-added dispatch)."
  ],
  "observations": [
    "The dispatcher correctly branches on both 4-byte selectors (0x0b7d796e for loop, 0x8381f58a for the auto-generated number() getter), and both branches terminate correctly (RETURN for the getter, STOP for loop).",
    "The loop condition node (7) has both required outgoing edges: enter-body (7->8) and exit-loop (7->10), so the loop's forward decision is intact.",
    "The only genuine source-level control flow not captured is the loop's iteration back-edge; every other logical branch in the source is represented.",
    "CFG extraction appears to have truncated at the JUMPI whose taken target (0x019e) leads to the loop continuation, likely stopping exploration at the compiler overflow-check split."
  ]
}

NestedLoop (CFG: 50, Decompilation: 20)

Decompilation

{
  "score": 20,
  "summary": "The decompilation fails to preserve the core behavior of the contract. The nested loop structure, the loop iteration control flow, and the state-mutating storage write (number += 1) are all absent. The function is incorrectly marked as 'view' when it actually mutates state, and the loop body has been reduced to a series of meaningless require statements.",
  "differences": [
    "The nested for-loops iterating i and j from 0 to loops are not represented; there is no loop control flow at all",
    "The storage write 'number += 1' (a state mutation to the public storage variable) is completely missing; the increment is only vestigially hinted at inside a nonsensical require condition",
    "The function is incorrectly declared 'view' even though the original mutates the 'number' storage slot, misrepresenting mutability",
    "The loop bound comparison (i/j < loops) is mangled into 'require(!0 < arg0)' rather than proper loop conditions",
    "No actual arithmetic accumulation is performed; the loops*loops increments of the counter are lost entirely"
  ]
}

CFG

{
  "score": 50,
  "summary": "Function dispatch, both function entries/exits, the getter, and the outer-loop condition/body/exit are captured, but the CFG is entirely acyclic: neither nested loop's back-edge (iteration latch) is present, and the inner loop's exit path is missing. For a contract whose entire logic is a nested for-loop, the defining iteration control flow is absent.",
  "missing_paths": [
    "Inner loop exit branch: node 8 (inner condition at 0x84, 'j < loops') JUMPIs to 0xb1 when false, but that edge and the target block (0xb1: i++ increment + jump back to outer head) are absent from the CFG.",
    "Inner loop back-edge (latch): after 'number += 1' the loop should increment j and jump back to the inner condition head (0x84); no such back-edge exists. The normal (non-overflow) continuation from node 9 (JUMPI to 0x01ac) is also missing.",
    "Outer loop back-edge (latch): after the inner loop completes, i is incremented and control returns to the outer condition head (0x77); this cycle is not represented.",
    "The entire graph is a DAG with zero cycles, so neither for-loop is actually modeled as a loop."
  ],
  "extra_paths": [
    "Arithmetic overflow (panic 0x11) revert path from node 9 to node 10 (0x0152 block) - compiler-added checked-arithmetic branch.",
    "CALLVALUE non-payable revert (node 0 -> 1) - compiler dispatch guard.",
    "Calldata size < 4 revert (node 2 -> 15) and calldata decode SLT/length reverts (nodes 4 -> 12, 5 -> 6) - compiler ABI-decode guards."
  ],
  "observations": [
    "Outer loop is partially captured: its condition head (0x77), body entry (node 8), and exit (node 7 -> 11 -> STOP) are present; only its back-edge is missing.",
    "Inner loop is captured only up to its body entry (node 8 -> 9); both its false/exit branch and its back-edge are missing.",
    "Both selector branches are correctly represented (loop: 0x0b7d796e, number getter: 0x8381f58a), including their revert fall-through to node 15.",
    "Because the missing edges are static jump targets (0xb1, 0x01ac), this appears to be an extraction gap in resolving back-edges rather than genuinely unreachable code."
  ]
}

WETH9 (CFG: 100, Decompilation: 68)

Decompilation

{
  "score": 68,
  "summary": "Core token mechanics are preserved: deposit, withdraw (with ETH transfer and Withdrawal event), totalSupply, and the balance-movement/event/return logic of transfer and transferFrom are all captured accurately. However, the allowance handling is significantly muddled: the nested allowance mapping is flattened and misrepresented, approve writes to the balance storage map, and the conditional allowance-spend logic in transferFrom is garbled with trivially-true guards. Read/write storage slots are also inconsistent between accessors and mutators.",
  "differences": [
    "approve() writes the allowance to storage_map_c (the balance mapping) keyed only by the spender (arg0), losing the nested allowance[owner][spender] structure and writing to the wrong storage slot; original sets allowance[msg.sender][guy].",
    "The transferFrom conditional 'if (src != msg.sender && allowance[src][msg.sender] != uint(-1))' is garbled: the src != msg.sender guard renders as require(msg.sender == msg.sender) (always true) and the allowance == max check plus allowance decrement are keyed against the balance map rather than the nested allowance mapping, so the allowance-spend path is not faithfully represented.",
    "balanceOf() and allowance() read from storage_map_d while deposit/transfer/withdraw write balances to storage_map_c, an inconsistent slot mapping between reads and writes.",
    "allowance() collapses the nested allowance[owner][spender] mapping into a single-key lookup, dropping one dimension of the key derivation.",
    "transfer()/transferFrom() contain duplicated and unreachable code paths after return statements, obscuring the actual control flow (original transfer simply delegates to transferFrom)."
  ]
}

Events (CFG: 90, Decompilation: 63)

Decompilation

{
  "score": 63,
  "summary": "Five of the seven functions are well preserved, including the multi-event emitter and the dynamic-type (string/bytes) emitters, with events correctly captured (parameter/name loss expected). However, the two three-argument functions (emitTransfer and emitApproval) completely lost their event emissions: they decompile to a bare require with only one of three arguments recovered and no emit at all. Since emitting the event is the sole behavior of these functions, this is a full loss of their logic, and the Approval event is not even declared.",
  "differences": [
    "emitTransfer (selector 0x5687f2b8) does not emit the Transfer event; it decompiles to only a require statement and recovers just one of the three original arguments (from, to, value).",
    "emitApproval (selector 0x23de6651) does not emit the Approval event; it decompiles to only a require statement and recovers just one of the three original arguments (owner, spender, value).",
    "The Approval event is missing from the decompiled contract's event declarations entirely.",
    "The recovered emit calls for Log/LogBytes use raw memory-offset expressions (e.g. '(var_b + 0x20) - var_b') for the dynamic data pointer, which is a low-level but semantically acceptable representation."
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Coverage Report for 4a8537e

Metric Value
Base branch 66.26%
PR branch 66.15%
Diff -0.12%

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark for 4a8537e

Click to view benchmark
Test Base PR %
heimdall_cfg/complex 14.3±0.25ms 13.8±0.29ms -3.50%
heimdall_cfg/simple 1407.9±14.22µs 1403.0±37.70µs -0.35%
heimdall_decoder/seaport 56.9±2.20µs 57.1±2.50µs +0.35%
heimdall_decoder/transfer 3.8±0.16µs 3.8±0.28µs 0.00%
heimdall_decoder/uniswap 16.1±0.64µs 15.9±0.66µs -1.24%
heimdall_decompiler/abi_complex 57.9±0.77ms 59.5±1.26ms +2.76%
heimdall_decompiler/abi_simple 1500.4±20.52µs 1502.7±33.91µs +0.15%
heimdall_decompiler/sol_complex 80.3±1.45ms 82.0±1.28ms +2.12%
heimdall_decompiler/sol_simple 2.2±0.01ms 2.3±0.03ms +4.55%
heimdall_decompiler/yul_complex 62.8±1.26ms 63.9±0.79ms +1.75%
heimdall_decompiler/yul_simple 1639.2±15.15µs 1640.9±14.89µs +0.10%
heimdall_disassembler/complex 1240.0±21.00µs 1250.8±22.51µs +0.87%
heimdall_disassembler/simple 61.3±2.62µs 62.1±2.53µs +1.31%
heimdall_vm/erc20_transfer 262.8±8.04µs 263.8±11.27µs +0.38%
heimdall_vm/fib 853.0±6.16µs 849.4±43.03µs -0.42%
heimdall_vm/ten_thousand_hashes 703.4±14.37ms 708.4±17.49ms +0.71%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant