Skip to content

HIR scope leak: outer arrow's (req, res) native-instance tag bleeds into inner same-named (res) parameter #1132

Description

@proggeramlug

Summary

When an outer arrow callback parameter is tagged as a native-instance (e.g. res in (req, res) => … of an http.createServer handler gets ("http", "ServerResponse")), the tag bleeds into any inner arrow callback that re-binds the same name as its own parameter — so the inner res (which should be a fresh untyped binding, or differently tagged) dispatches as if it were the outer res. Method calls on the inner binding go to the wrong NATIVE_MODULE_TABLE row.

Repro

import { createServer, get as httpGet } from "node:http";

const server = createServer((req: any, res: any) => {
  res.end(Buffer.from([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]));
});
server.listen(18993, () => {
  httpGet("http://127.0.0.1:18993/", (res: any) => {
    //                                ^^^ same name as outer (req, res)
    const chunks: any[] = [];
    res.on("data", (c: any) => chunks.push(c));
    res.on("end",  ()        => {
      console.log(Array.from(Buffer.concat(chunks).slice(0,8)).join(","));
    });
  });
});

The inner res is the response from http.get, which should be ("http", "IncomingMessage"). But the outer (req, res) callback's res was registered as ("http", "ServerResponse") first, and that tag leaks into the inner scope — so the inner res.on(...) routes through ServerResponse dispatch instead of IncomingMessage.

Workaround in current 1124-followup test: rename the inner param to resp (different identifier — no collision, no leak). That's how test-files/test_issue_1124_client_buffer.ts ships on branch fix/net-createserver-and-http-buffer-body at v0.5.1013.

Root cause hypothesis

The HIR native-instance tag table is keyed by identifier name and lives in some scope that's broader than the lexical arrow body — likely a function-scope or module-scope HashMap<String, NativeInstanceTag> rather than a properly nested scope stack. Pre-scans like pre_scan_node_http_client_callback_params (added in v0.5.1013) register names without first popping/shadowing any outer binding of the same name.

Worth checking:

  • Where native-instance tags are inserted (search crates/perry-hir/src/lower/expr_call.rs and crates/perry-hir/src/lower_patterns.rs for the registration sites added in v0.5.773, v0.5.1011, v0.5.1013).
  • Whether the lookup site walks a scope chain or just .get() on a flat map.
  • Whether arrow callbacks open a fresh scope at all in the HIR lowering for these param tags.

Fix sketch

Either:

  1. Proper scoping: convert the native-instance tag table to a scope stack — push on arrow-callback enter, pop on exit. Inner params shadow outer names cleanly.

  2. Shadow on registration: when a pre-scan registers an inner callback's param name, check for + temporarily save the outer tag, register the inner one, and restore on scope exit. Less invasive than (1).

  3. Identifier disambiguation: rename inner-scope identifiers in HIR to be unique (e.g. res$1, res$2). Most invasive but solves a class of related shadowing bugs.

Option 2 is probably the smallest diff if the lowering visitor already has an obvious arrow-enter/exit hook.

Impact

Any compilePackages program that uses both http.createServer and http.get/http.request and reuses the conventional (req, res) parameter name in both will misroute method dispatch in the inner callback. Without the rename workaround, res.on('data'), res.on('end'), res.setEncoding(), etc. on an IncomingMessage go through ServerResponse dispatch rows that don't have those methods, producing silent no-ops or TypeErrors.

Same bug shape probably affects:

  • net.createServer((sock) => { net.connect(..., (sock) => { sock.write(…) }) }) — inner sock tagged as outer-server's connection-socket type.
  • fastify.get('/', (req, reply) => { fetch(...).then((reply) => {...}) }) — inner reply mistagged.

Anywhere a user happens to reuse a conventional parameter name across nested native-instance callbacks.

Environment

Refs

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions