mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Keep native private methods out of reach of Node islands
This commit is contained in:
@@ -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;
|
||||
|
||||
+6
@@ -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")));
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare function probe(o: object): string;
|
||||
export declare function shadow(o: object): string;
|
||||
+22
@@ -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 };
|
||||
Generated
Vendored
+1
@@ -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 }
|
||||
Reference in New Issue
Block a user