authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-07 13:34:48+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-08 11:49:02+02:00
log296b39c32324d536b13fe92fef42a7cca3487d35
tree0378d4cd33edd870e183bc79ce759a23b0952dba
parent664062a5f633913906fe5c4b7ecd672b51b5cdd9
signaturelock-open Commit is signed but in an unrecognized format.

grammar: fix ParamDeclList worst-case exponential time

AFL++ is able to identify "hangs" as well as crashes and has found several small inputs that take multiple seconds for the generated parser to parse. This is because our PEG is not yet LL(k) despite recognizing the same language as our handwritten LL(k) recursive descent parser. Consider the PEG semantics for the following non-terminal: ParamDeclList <- (ParamDecl COMMA)* (ParamDecl / DOT3 COMMA?)? Here, the PEG checks twice if the last ParamDecl matches when there is no trailing comma. Now, consider that the ParamDecl may recursively contain a nested ParamDeclList, which in turn has ParamDecls that contain further nested ParamDeclLists. It should be clear that this results in worst-case exponential runtime. This can be avoided by defining ParamDeclList using recursion instead of the * operator. This same pattern likely needs to be applied to other instances of this pattern in the grammar. The main motivation for fixing this is to improve fuzzer performance. Additionally, using recursion rather than the * operator makes it easier to translate from a PEG to a CFG, which I'm quite confident we will want to do for the language specification eventually. A CFG would also allow for smith-based fuzz testing to complement the oracle-based approach.

2 files changed, 27 insertions(+), 23 deletions(-)

doc/langref/grammar.peg+6-2
...@@ -64,7 +64,7 @@ DeclPrefix...@@ -64,7 +64,7 @@ DeclPrefix
64 / KEYWORD_extern STRINGLITERALSINGLE? KEYWORD_fn64 / KEYWORD_extern STRINGLITERALSINGLE? KEYWORD_fn
65 / (KEYWORD_export / KEYWORD_extern STRINGLITERALSINGLE?)? KEYWORD_threadlocal? (KEYWORD_const / KEYWORD_var)65 / (KEYWORD_export / KEYWORD_extern STRINGLITERALSINGLE?)? KEYWORD_threadlocal? (KEYWORD_const / KEYWORD_var)
6666
67FnProto <- KEYWORD_fn IDENTIFIER? LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr67FnProto <- KEYWORD_fn IDENTIFIER? ParamDeclList ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr
6868
69VarDeclProto <- (KEYWORD_const / KEYWORD_var) IDENTIFIER (COLON TypeExpr)? ByteAlign? AddrSpace? LinkSection?69VarDeclProto <- (KEYWORD_const / KEYWORD_var) IDENTIFIER (COLON TypeExpr)? ByteAlign? AddrSpace? LinkSection?
7070
...@@ -448,7 +448,11 @@ AsmOutputList <- (AsmOutputItem COMMA)* AsmOutputItem?...@@ -448,7 +448,11 @@ AsmOutputList <- (AsmOutputItem COMMA)* AsmOutputItem?
448448
449AsmInputList <- (AsmInputItem COMMA)* AsmInputItem?449AsmInputList <- (AsmInputItem COMMA)* AsmInputItem?
450450
451ParamDeclList <- (ParamDecl COMMA)* (ParamDecl / DOT3 COMMA?)?451ParamDeclList <- LPAREN ParamDeclListRest
452ParamDeclListRest
453 <- RPAREN
454 / DOT3 COMMA? RPAREN
455 / ParamDecl (COMMA ParamDeclListRest / RPAREN)
452456
453ExprList <- (Expr COMMA)* (Expr / !ExprPrefix)457ExprList <- (Expr COMMA)* (Expr / !ExprPrefix)
454458
lib/std/zig/parser_generated_oracle.zig+21-21
...@@ -203,7 +203,7 @@ const Parser = struct {...@@ -203,7 +203,7 @@ const Parser = struct {
203 pub fn parseFnProto(p: *Parser) Error!bool {203 pub fn parseFnProto(p: *Parser) Error!bool {
204 return blk_0: {204 return blk_0: {
205 const pos_0 = p.i;205 const pos_0 = p.i;
206 if (try p.parseKEYWORD_fn() and (try p.parseIDENTIFIER() or true) and try p.parseLPAREN() and try p.parseParamDeclList() and try p.parseRPAREN() and (try p.parseByteAlign() or true) and (try p.parseAddrSpace() or true) and (try p.parseLinkSection() or true) and (try p.parseCallConv() or true) and (try p.parseEXCLAMATIONMARK() or true) and try p.parseTypeExpr()) break :blk_0 true;206 if (try p.parseKEYWORD_fn() and (try p.parseIDENTIFIER() or true) and try p.parseParamDeclList() and (try p.parseByteAlign() or true) and (try p.parseAddrSpace() or true) and (try p.parseLinkSection() or true) and (try p.parseCallConv() or true) and (try p.parseEXCLAMATIONMARK() or true) and try p.parseTypeExpr()) break :blk_0 true;
207 p.i = pos_0;207 p.i = pos_0;
208 break :blk_0 false;208 break :blk_0 false;
209 };209 };
...@@ -2286,26 +2286,26 @@ const Parser = struct {...@@ -2286,26 +2286,26 @@ const Parser = struct {
2286 pub fn parseParamDeclList(p: *Parser) Error!bool {2286 pub fn parseParamDeclList(p: *Parser) Error!bool {
2287 return blk_0: {2287 return blk_0: {
2288 const pos_0 = p.i;2288 const pos_0 = p.i;
2289 if (blk_1: {2289 if (try p.parseLPAREN() and try p.parseParamDeclListRest()) break :blk_0 true;
2290 var i_1: usize = 0;2290 p.i = pos_0;
2291 while (blk_3: {2291 break :blk_0 false;
2292 const pos_3 = p.i;2292 };
2293 if (try p.parseParamDecl() and try p.parseCOMMA()) break :blk_3 true;2293 }
2294 p.i = pos_3;2294 pub fn parseParamDeclListRest(p: *Parser) Error!bool {
2295 break :blk_3 false;2295 return blk_0: {
2296 }) {2296 const pos_0 = p.i;
2297 if (i_1 > max_depth) return error.MaxDepth;2297 if (try p.parseRPAREN()) break :blk_0 true;
2298 i_1 += 1;2298 p.i = pos_0;
2299 }2299 if (try p.parseDOT3() and (try p.parseCOMMA() or true) and try p.parseRPAREN()) break :blk_0 true;
2300 break :blk_1 true;2300 p.i = pos_0;
2301 } and (blk_3: {2301 if (try p.parseParamDecl() and blk_2: {
2302 const pos_3 = p.i;2302 const pos_2 = p.i;
2303 if (try p.parseParamDecl()) break :blk_3 true;2303 if (try p.parseCOMMA() and try p.parseParamDeclListRest()) break :blk_2 true;
2304 p.i = pos_3;2304 p.i = pos_2;
2305 if (try p.parseDOT3() and (try p.parseCOMMA() or true)) break :blk_3 true;2305 if (try p.parseRPAREN()) break :blk_2 true;
2306 p.i = pos_3;2306 p.i = pos_2;
2307 break :blk_3 false;2307 break :blk_2 false;
2308 } or true)) break :blk_0 true;2308 }) break :blk_0 true;
2309 p.i = pos_0;2309 p.i = pos_0;
2310 break :blk_0 false;2310 break :blk_0 false;
2311 };2311 };