Keep native private methods out of reach of Node islands

This commit is contained in:
Chris Tate
2026-10-25 09:28:49 -05:00
parent 365d14ee1c
commit 4458830db1
9 changed files with 79 additions and 2 deletions
@@ -642,6 +642,14 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
}
}
/** Whether an island reads the lowered method `name` by that name: a
* public, string-keyed prototype method. The constructor, #private methods,
* accessor halves (`get:x`, `set:x`) and symbol-keyed slots (`sym:iterator`)
* are not properties an island can name. */
function islandMethodName(name: string): boolean {
return name !== "constructor" && !name.startsWith("#") && !/^(?:get|set|sym):/.test(name);
}
/** A class instance crossing into an island: its capsule answers island
* reads of its fields, and its prototype methods come from a table the
* runtime builds once per class (nodeIsland.classMethods): the unbound
@@ -661,7 +669,7 @@ function withClassMethods(
c = c.def.base ? lowerer.classes.get(c.def.base) : undefined
) {
for (const m of c.def.methods ?? [])
if (!names.includes(m) && m !== "constructor") names.push(m);
if (!names.includes(m) && islandMethodName(m)) names.push(m);
}
if (names.length === 0) return value;
const loc = value.loc;
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6838-node-island-private-methods/main.ts"
],
"diags": []
}
@@ -373,11 +373,17 @@ ScrDyn *scr_node_island_class_methods(ScrDyn *value, ScrDyn *builder) {
return scr_dyn_retain(value);
}
/* A prototype method of a class capsule (+1), or NULL. */
/* A prototype method of a class capsule (+1), or NULL. An own property the
* island wrote under the method's name (kept in the capsule's materialized
* view) hides the method, as an own property hides a prototype's in Node. */
static ScrDyn *ni_class_method(const ScrDyn *target, const ScrDyn *key) {
if (key->kind != SCR_DYN_STR) return NULL;
ScrDyn *table = ni_methods_of(target);
if (table == NULL) return NULL;
const ScrDyn *view = target->v.typed_ref.materialized;
if (view != NULL && view->kind == SCR_DYN_OBJ &&
scr_dyn_obj_get(view, key->v.str->data, key->v.str->len) != NULL)
return NULL;
ScrDyn *m = scr_dyn_obj_get(table, key->v.str->data, key->v.str->len);
return m != NULL ? scr_dyn_retain(m) : NULL;
}
@@ -0,0 +1,30 @@
// @requires-libnode
// A native class instance passed into a Node island shows the island only
// its public members: its #private methods, and the names the compiler
// gives accessors and symbol-keyed methods, read as undefined, as in Node.
import { probe, shadow } from "private-pkg";
class Account {
#balance = 10;
owner: string;
constructor(owner: string) {
this.owner = owner;
}
#secret(): string {
return `secret of ${this.owner}: ${this.#balance}`;
}
get label(): string {
return `account ${this.owner}`;
}
describe(): string {
return `${this.owner} (${this.#secret().length})`;
}
*[Symbol.iterator](): Generator<string> {
yield this.owner;
}
}
const a = new Account("ada");
console.log(probe(a));
console.log(a.describe(), [...a].join(","));
console.log(shadow(new Account("bob")));
@@ -0,0 +1,2 @@
export declare function probe(o: object): string;
export declare function shadow(o: object): string;
@@ -0,0 +1,22 @@
"use strict";
// Reads a native instance's #private method and internal member names by
// string: an island sees only its public members.
function probe(o) {
const out = [typeof o["#secret"], "#secret" in o];
// A property with the private method's spelling, holding undefined.
o["#secret"] = undefined;
const m = o["#secret"];
out.push(typeof m, "#secret" in o);
if (typeof m === "function") out.push(m.call(o));
out.push(typeof o["get:label"], typeof o["sym:iterator"], typeof o.describe);
return out.join(" ");
}
// An own property holding undefined hides the prototype method.
function shadow(o) {
o.describe = undefined;
return `${typeof o.describe} ${Object.keys(o).includes("describe")}`;
}
module.exports = { probe, shadow };
@@ -0,0 +1 @@
{ "name": "private-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1 @@
{ "name": "island-private-methods", "private": true, "type": "module", "scriptc": { "packages": { "private-pkg": "node" } } }
@@ -0,0 +1 @@
{ "plain": 2500, "san": 3300 }