Skip to content

Proxy id band exhaustion is a wild pointer, not an error: the 65,536th new Proxy() in a thread returns a payload the next property read dereferences (SIGSEGV) #8218

Description

@proggeramlug

js_proxy_new mints proxy ids straight out of PROXIES.len() with no bound check against the band it encodes into:

// crates/perry-runtime/src/proxy.rs
const PROXY_TAG_BASE: u64 = addr_class::PROXY_ID_BAND_START as u64;  // 0xF0000
fn encode_proxy_id(id: u64) -> i64 { (PROXY_TAG_BASE + id) as i64 }

pub extern "C" fn js_proxy_new(target: f64, handler: f64) -> f64 {
    ...
    let id = v.len() as u64;          // <- unbounded
    v.push(Some(Box::new(ProxyEntry { ... })));
    let encoded = encode_proxy_id(id) as u64;
    f64::from_bits(POINTER_TAG | (encoded & POINTER_MASK))
}

The revocable-Proxy band is [PROXY_ID_BAND_START, HANDLE_BAND_MAX) = [0xF0000, 0x100000) — exactly 65,536 ids. Id 65,536 encodes to 0x100000, which is HANDLE_BAND_MAX itself: addr_class::is_proxy_id_band rejects it and addr_class::is_above_handle_band accepts it, i.e. every classifier in the tree reads that payload as a dereferenceable heap address. The next property read dereferences it.

This is not a lost proxy. It is a wild pointer handed to user code.

Reproduction

const keep: any[] = [];
for (let i = 0; i < 65600; i++) {
  const t: any = { i: i };
  keep.push(new Proxy(t, { get(o: any, k: any) { return o[k]; } }));
}
console.log("created", keep.length);
for (const idx of [0, 60000, 65530, 65534, 65535, 65536, 65540, 65599]) {
  const p: any = keep[idx];
  console.log(idx, typeof p, p.i);
}

Node prints all eight rows. Perry:

created 65600
0 object 0
60000 object 60000
65530 object 65530
65534 object 65534
        <- SIGSEGV, exit 139

keep[65534] is registry id 65,535 (index 0 is reserved), the last payload inside the band. keep[65535] is id 65,536 — the first one outside it.

Why it matters beyond a synthetic loop

PROXIES is append-only: js_proxy_new pushes, and the only slot.take() in the file is #[cfg(test)]. Nothing reclaims a slot, so the count is cumulative over process lifetime, not a live-proxy count. #8213 measured a warm Next.js App Route creating ~4 proxies per request (headers / cookies / mutableCookies / searchParams adapters), 10,037 entries after 2,478 requests — 15% of the band already consumed. At that rate the ceiling lands after roughly 16k requests, i.e. this is reachable by a server that merely stays up, not by a program that does anything unusual.

Every other handle band already guards its end — common/handle.rs (panic!("common native handle id range exhausted ...")) and fetch/mod.rs (panic!("Web Fetch handle id range exhausted")). The Proxy band is the one without a guard, and it is also the only band whose ids are minted directly by user code (new Proxy) with no matching free/close call.

A second, smaller inconsistency in the same area: lookup() accepts any payload below 0x1_0000_0000, so an out-of-band id is simultaneously "a live proxy" (per decode_proxy_id) and "a heap address" (per every addr_class consumer).

Fix

Refuse to mint past the band edge — throw a catchable RangeError rather than return a wild pointer, the same trade error::throw_allocation_failed makes for #5067 — and tighten decode_proxy_id so the two classifications cannot drift apart again.

That makes the failure safe and named; it does not make it go away. The ceiling exists because the registry never reclaims, which is #8213's mechanism (b). Raising the ceiling is not a cheap alternative: the band is boxed in by zlib below and by HANDLE_BAND_MAX above, and HANDLE_BAND_MAX is the "payloads below this are never dereferenceable" contract that every is_handle_band caller reads.

Refs #8213.

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

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions