Skip to content

Re-exporting a Node builtin's named export through a local module is broken: export { createHash } from "crypto" fails to link, import { createHash } from "crypto"; export { createHash } yields a function that returns undefined #10432

Description

@proggeramlug

Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. A named export of a Node builtin (crypto, fs, path, ...) that is re-exported from a user/package module is unusable in the importer: the export { x } from "<builtin>" form produces an undefined perry_fn_<module>__<x> symbol at link time, and the import { x } from "<builtin>"; export { x } form links but the imported value returns undefined when called.

Reproduction

Case 1 (link error):

mkdir case1 && cd case1
echo '{"name":"r","private":true,"type":"module"}' > package.json
cat > dep.ts <<'EOF'
export { createHash } from "crypto";
EOF
cat > main.ts <<'EOF'
import { createHash } from "./dep.ts";
console.log(createHash("sha256").update("x").digest("hex"));
EOF
node main.ts
perry compile main.ts -o out && ./out

Case 2 (links, wrong value):

mkdir case2 && cd case2
echo '{"name":"r","private":true,"type":"module"}' > package.json
cat > dep.ts <<'EOF'
import { createHash } from "crypto";
export { createHash };
EOF
cat > main.ts <<'EOF'
import { createHash } from "./dep.ts";
console.log(typeof createHash);
console.log(createHash("sha256").update("x").digest("hex"));
EOF
node main.ts
perry compile main.ts -o out && ./out

Expected (Node 26.5.1)

Case 1:

2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881

Case 2:

function
2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881

Actual (Perry)

Case 1 (object-file path shortened, strip-dedup lines omitted):

/usr/bin/ld: node_modules/.cache/perry/objects/host/<hash>.o: in function `main':
perry_module:(.text+0x4a): undefined reference to `perry_fn_dep_ts__createHash'
collect2: error: ld returned 1 exit status
Error: Linking failed

Case 2:

function
TypeError: Cannot read properties of undefined (reading 'update')
    at <anonymous>

Same result with the default build and with PERRY_NO_AUTO_OPTIMIZE=1.

Impact

  • ethers 6.17.0: src.ts/crypto/crypto.ts:1-3 is export { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto"; -> link fails with undefined reference to perry_fn_..._crypto_crypto_ts__createHmac (and pbkdf2Sync, randomBytes, createHash).
  • jimp 1.6.1: @jimp/file-ops/dist/esm/index.js:2 is export { existsSync } from "fs"; (imported by @jimp/core/dist/esm/index.js:4) -> undefined reference to perry_fn_node_modules__jimp_file_ops_dist_esm_index_js__existsSync.
  • Any barrel/facade module that forwards builtin functions (a common pattern for isomorphic packages that swap crypto/fs per platform).

Notes

Variant matrix (all with a local dep module and main.ts importing from it; Node prints the correct value for every row):

dep module Perry
export { createHash } from "crypto" (.ts or .js) link error perry_fn_dep_ts__createHash
export { existsSync } from "fs" (.js) link error perry_fn_dep_js__existsSync
export { join } from "node:path" link error perry_fn_dep_ts__join
export * from "node:crypto" link error __perry_wrap_perry_fn_dep_ts__createHash / perry_fn_dep_ts__createHash
case-1 dep, but importer uses import * as dep from "./dep.ts" links; typeof dep.createHash is undefined
import { existsSync } from "fs"; export { existsSync } links; typeof is function, call returns undefined
import { join } from "path"; export { join } links; typeof is function, call returns undefined
import * as c from "crypto"; export const createHash = c.createHash works
import { createHash as h } from "crypto"; export function createHash(a) { return h(a); } works
export { hello } from "./impl.ts" (user module, not a builtin) works
builtin used as a value inside one module (const h = createHash; h("sha256")) works

Suspected locations (inferred, not verified):

  • Case 1: the driver's re-export propagation only records an origin when the re-export source resolves to a compiled module (crates/perry/src/commands/compile/run_pipeline.rs:1913-1950, resolve_import_with_context(...) returns nothing for a builtin). The importer then falls back to "the name is a function defined in the immediate module" (run_pipeline.rs:4425-4469: when no origin is known, import_function_prefixes gets the immediate module's prefix), so codegen references perry_fn_dep_ts__createHash, which nothing defines. HIR records the specifier as Export::ReExport (crates/perry-hir/src/lower/module_decl.rs:1491) with no builtin-specific handling.
  • Case 2: the local builtin import is exported as a plain Export::Named (crates/perry-hir/src/lower/module_decl.rs:1562); the exported slot does not hold the callable builtin value.

The same "unresolved named import becomes a perry_fn_ reference" fallback produces #10433.

Activity

  1. added
    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
    on Sep 17, 2026
  2. proggeramlug commented on Sep 22, 2026

    @proggeramlug
    ContributorAuthor

    Fixed on main by PR #10867, which landed with both Closes #10802 and Closes #10432 in its body.

    Closing manually because merge trains close source PRs without firing close keywords, so neither reference took effect. Verified by content rather than PR state: crates/perry-hir/tests/node_named_export_hygiene.rs and the named-re-export handling in module_decl.rs are both present on main.

    This issue documented the defect more completely than #10802 did — it covered both forms, the export { x } from "<builtin>" link failure and the import { x } from "<builtin>"; export { x } form that links but returns undefined when called. Worth noting for anyone auditing the fix that the second form is the easier one to regress silently, since it produces no link error.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions