Skip to content

fix(bundle): Solidity loader post-merge review: untrusted dependencies, forge/solc fidelity - #177

Merged
exo-nikita merged 1 commit into
mainfrom
claude/magical-davinci-bs3pob
Sep 28, 2026
Merged

exo-nikita merged 1 commit into
mainfrom
claude/magical-davinci-bs3pob

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Follow-up to #176. A post-merge review of the Solidity loader listed 15 findings. I reproduced each against main (6762667) and checked the forge and solc claims against the real tools:

  • forge: a reference binary built from foundry v1.8.3's config crate.
  • solc: solc-js 0.8.30.
  • Symlink walk: the walkdir 2.5 crate itself.

14 findings held as reported, and I fixed them. For finding 9 the reviewer's forge claim was half wrong; it is fixed to match what forge actually does. Details are below.

Security

Dependencies are input the project didn't write. On main, a dependency under lib/ containing import ".env"; put the project's .env into the bundle with no flag at all.

  1. An import must resolve to a .sol file inside the bundle root.
    • This covers every resolution path: a bare import ".env";, a relative import "../../../.env"; (a variant that predates Implement Foundry remappings discovery for Solidity imports #176), and remapping targets such as x/=/opt/evil/ (finding 5, which used to produce a bundle that stasis extract rejects) or ../elsewhere/.
    • A refused import is fatal, and the error says why. Example: Unresolved import: .env from lib/evil/src/E.sol (refused: it resolves to .env, which is not a .sol file).
  2. A dependency's imports must land on a dependency's file, compared by real path.
    • "Dependency" means a file under forge's libs, Soldeer's dependencies/, a git submodule from .gitmodules, or any node_modules.
    • A dependency can no longer reach the project's own files through any of these:
      • its own remappings (the steal/=../../secrets/ case);
      • a base-path lookup (import "script/Secrets.sol", a variant the review didn't list);
      • a relative path;
      • a symlink inside the dependency.
    • Importing another dependency is still allowed; forge-std's ds-test is the usual case.
    • I chose "stay within the dependencies" rather than "stay within your own package", which is what fix(bundle): Rust loader follow-ups: lexer offsets, dead items, paths through re-exports and macros, include!, review fixes, --cargo-target, --cargo-manifests #175 does for Rust. Cross-package imports in Solidity go through paths and remappings, so the stricter rule would break ordinary projects.
  3. A dependency's foundry.toml whose extends points outside it is skipped, with a warning (finding 3).
  4. --manifests no longer carries credentials (finding 4).
    • .toml configs lose their [rpc_endpoints] and [etherscan] tables, whether at the top level or inside a profile.
    • They also lose eth_rpc_url, etherscan_api_key and any other key named like a key, token, secret or password. Everything else in the file is kept verbatim.
    • Every carried file has the user info stripped from its URLs, including .gitmodules, the lock files and package.json.
    • Only *.toml/*.txt config files are carried.
    • hardhat.config.* is not carried any more: it is code and may hold keys.
    • The docs and the CLI help now describe what is carried.

Wrong resolution compared with forge or solc

# Finding Result
6 Legacy [default] table; top-level remappings Fixed. Legacy [<name>] tables are read as profiles, with [profile.<name>] winning key by key; checked against forge. forge itself rejects a top-level remappings = […], so it is accepted only in a --mapping file, which is how stasis read such files before #176.
7 Env vars change the output and are not recorded Fixed. stderr now reports when FOUNDRY_PROFILE or a remapping variable shaped the result, and buildBundle/bundleCommand accept env. The bundle format still doesn't record them; that would need a format change.
8 Root remappings.txt without a foundry.toml gets forge's slash rule Fixed. Without a foundry.toml the file applies as written, as solc and Hardhat read it (@oz/=lib/oz makes @oz/X.sol resolve to lib/ozX.sol). Next to a foundry.toml it is slash-terminated, as forge reads it.
9 Symlink walk Fixed, but not the way the finding suggested. Real walkdir runs its loop check only at symlinks, against the directories on the current walk. So forge does follow a link to the project root, and walks the root again, up to the point where it links back into the walk. The walk now matches walkdir exactly, verified against the crate on 428 random symlink trees. This includes the half of the finding that was right: two links to one directory are both walked.
10 --mapping turns off library lookups and ignores extends Fixed. --mapping now changes only the remappings.
11 FOUNDRY_PROFILE is case-sensitive Fixed. Names are matched case-insensitively in both directions ([profile.CI] with ci, and CI with [profile.ci]), checked against forge.
12 stasis bundle src test script fails without script/ Fixed. A missing or empty directory entry is skipped with a warning, and it is an error only when no entry yields a file. A missing extensionless path in a non-Solidity bundle now gets no such file or directory.
13 Library lookups start one directory too deep Fixed. They now start at the parent of the importer's directory, as resolve_absolute_library does, and are tried before the base-path lookup, in the same order foundry-compilers uses.
14 Scanner misses imports after \r and misdecodes \xNN Fixed. A // comment ends at \r as well as \n, and string escapes are decoded as UTF-8 bytes ("\xc3\xa9" is é). An unterminated literal no longer lets a later string be taken as the import path. All three checked against solc-js.
15 TOML reader misreads quoted dotted names and escapes Fixed. [profile."ci.fast"] is one profile, and \uXXXX, \UXXXXXXXX, \n and the other escapes decode.
— A ../ that climbs above the root is rejected, while solc clamps it Still rejected, which is the safer choice. The code comment and the docs now give the right reason.

Verification

  • New tests for every finding:
    • each refusal path: non-.sol files, outside-root targets, a dependency's own remapping, base-path lookup and symlink;
    • dependency-to-dependency imports still allowed;
    • the redaction output byte for byte, and URL credential stripping;
    • the env notice and env passthrough;
    • missing and empty directories;
    • --mapping with libs, extends and a top-level remappings;
    • as-written vs slash-terminated remappings.txt;
    • legacy, case-insensitive and quoted profiles;
    • library lookup depth;
    • the walkdir symlink shapes;
    • the scanner's \r, \xNN and unterminated-string cases.
  • forge oracle:
    • the 24 existing scenarios, plus 11 new profile cases (legacy tables, case, quoted names, escapes, standalone sections);
    • 700 randomized dependency trees, identical output.
  • solc-js: all 1,800 fuzzed remapping resolutions match. The fixture's bundled file set equals the set solc loads.
  • Scanner corpus: identical output to main on the 6,314 .sol files on the machine (36.6 MB). None of them has a CR-only line break or a \x import path.
  • Full suite: 2025 pass, 0 fail, 3 skipped; oxlint is clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C6oBS5QX4oqZcd2d3STiGA


Generated by Claude Code

…ies, forge/solc fidelity

Security (dependencies are input the project didn't write):
- An import must resolve to a .sol file inside the bundle root: `import ".env";`
  (bare or relative) and remapping targets like `/opt/x/` or `../x/` are refused.
- A dependency's import (forge libs, Soldeer dependencies/, git submodules,
  node_modules) must land, by real path, on a dependency's file: no project file
  through a relative path, a base-path lookup, its own remappings or a symlink.
- A dependency's foundry.toml whose `extends` lies outside it is skipped.
- --manifests drops [rpc_endpoints]/[etherscan] and secret-named keys from .toml
  configs, strips URL credentials from every carried file, carries only
  .toml/.txt configs, and no longer carries hardhat.config.* (code with keys).
- Refusals are fatal and say why.

Fidelity:
- Legacy top-level [<name>] profiles, case-insensitive profile names, quoted
  dotted table names and TOML string escapes (checked against forge v1.8.3).
- --mapping keeps forge's library lookups and honours the mapping's `extends`
  and a top-level `remappings`; a root remappings.txt without foundry.toml
  applies as written (solc/Hardhat), with one it is slash-terminated (forge).
- Library lookups start at the importer directory's parent and come before the
  base path, as foundry-compilers resolves them.
- Directory entries follow symlinks exactly like walkdir (checked against the
  crate on 428 random trees); a missing or empty entry dir is skipped.
- The scanner ends // comments at \r and reads string escapes as UTF-8 bytes
  (checked against solc-js 0.8.30).
- FOUNDRY_PROFILE / FOUNDRY_REMAPPINGS are reported when they apply; buildBundle
  and bundleCommand take `env`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6oBS5QX4oqZcd2d3STiGA
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.

2 participants