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

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