Parent: #493 Related: #496, #540, #539, #461
Status: investigation from code inspection. No measurement exists yet, and no
performance claim is made here.
Suspected mechanism
Parser.checkPrivateUsesInNode's class_expr arm passes the enclosing declared-name
map into the nested class:
.class_expr => |c| {
if (c.superclass) |sc| try self.checkPrivateUsesInNode(declared, sc);
try self.checkPrivateNameUses(declared, c.members);
},and checkPrivateNameUses opens a fresh map and copies every inherited entry into it
before adding the class's own names:
var declared = self.secureStringMap(void);
var it = inherited.iterator();
while (it.next()) |entry| try declared.put(self.arena, entry.key_ptr.*, {});So an outer class declaring N private names, containing M inner classes as siblings
(field initializers, static blocks, methods returning class expressions), performs
M × N inherited insertions for a source of size O(N + M). With M ≈ N that is
quadratic work — and because the copies go into self.arena, which is not released
during the parse, it is quadratic retained allocation, not only CPU.
This is attacker-controlled shape: the source is linear in size.
Why this is not #540
#540 bounded checkPrivateNames, the pairwise duplicate comparison among the N members
of a single class, and is closed. This is a different function on a different axis:
the copy of inherited names into each nested class. The two can both be true.
Why no existing benchmark would have caught it
bench/frontend_parse.zig's privateClassSource emits one flat class:
class PrivateWidth { #field0; #field1; ... }representative_frontend_private_names_{1024,2048,4096} and their escaped controls all
use that shape, so no frozen workload nests a class inside another class. The
inherited-copy path is uncovered.
Plan, in this order
- Freeze workloads and controls before touching parser code, added to the owned
bench/frontend_parse.ziggrowth runner:- growth rows at 1024/2048/4096: outer class with N private names containing N sibling inner class expressions that reference an inherited name.
- controls that must not move: the existing flat
private_namesfamily; an outer class with N names and a single inner class; N sibling inner classes with no inherited names to copy; a deep-nesting variant to separate depth from breadth. - correctness controls: shadowing an inherited name in an inner class, heritage
evaluated in the enclosing environment
(
class C extends class { x = this.#foo; } { #foo; }must stay a SyntaxError),#x in objbrand checks, and a directevalinside a class body.
- Measure allocation requests/bytes and instructions per width with the owned runner under the 3072 MB bounded guard, and confirm the growth curve before proposing a fix.
- Only then consider a scoped change — an active-name set with rollback, or passing the inherited map by reference with a depth-scoped overlay — preserving sibling isolation, shadowing, heritage ordering, secure hashing of attacker-controlled names, exact byte equality, and atomic OOM behaviour.
Explicit non-goals
No arbitrary nesting limit, no benchmark-name special-casing, no weakening of early
errors, and no published benchmark claim until the owned generator
(tools/algorithmic-growth.ts) produces dated evidence under docs/.data/.