From 296b39c32324d536b13fe92fef42a7cca3487d35 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Tue, 7 Jul 2026 13:34:48 +0200 Subject: [PATCH] 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. --- doc/langref/grammar.peg | 8 +++-- lib/std/zig/parser_generated_oracle.zig | 42 ++++++++++++------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/doc/langref/grammar.peg b/doc/langref/grammar.peg index ad2433cbffd70c7739aecd7ae1ad0c1ea1673854..d78765560e548dec404fb1378952ef9a42a65676 100644 --- a/doc/langref/grammar.peg +++ b/doc/langref/grammar.peg @@ -64,7 +64,7 @@ DeclPrefix / KEYWORD_extern STRINGLITERALSINGLE? KEYWORD_fn / (KEYWORD_export / KEYWORD_extern STRINGLITERALSINGLE?)? KEYWORD_threadlocal? (KEYWORD_const / KEYWORD_var) -FnProto <- KEYWORD_fn IDENTIFIER? LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr +FnProto <- KEYWORD_fn IDENTIFIER? ParamDeclList ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr VarDeclProto <- (KEYWORD_const / KEYWORD_var) IDENTIFIER (COLON TypeExpr)? ByteAlign? AddrSpace? LinkSection? @@ -448,7 +448,11 @@ AsmOutputList <- (AsmOutputItem COMMA)* AsmOutputItem? AsmInputList <- (AsmInputItem COMMA)* AsmInputItem? -ParamDeclList <- (ParamDecl COMMA)* (ParamDecl / DOT3 COMMA?)? +ParamDeclList <- LPAREN ParamDeclListRest +ParamDeclListRest + <- RPAREN + / DOT3 COMMA? RPAREN + / ParamDecl (COMMA ParamDeclListRest / RPAREN) ExprList <- (Expr COMMA)* (Expr / !ExprPrefix) diff --git a/lib/std/zig/parser_generated_oracle.zig b/lib/std/zig/parser_generated_oracle.zig index 20366362b5c28029031d3e4c76da8ac5b60aefaf..f1427bd6101e09695c1dae7329d64b508ece9f5f 100644 --- a/lib/std/zig/parser_generated_oracle.zig +++ b/lib/std/zig/parser_generated_oracle.zig @@ -203,7 +203,7 @@ const Parser = struct { pub fn parseFnProto(p: *Parser) Error!bool { return blk_0: { const pos_0 = p.i; - 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; + 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; p.i = pos_0; break :blk_0 false; }; @@ -2286,26 +2286,26 @@ const Parser = struct { pub fn parseParamDeclList(p: *Parser) Error!bool { return blk_0: { const pos_0 = p.i; - if (blk_1: { - var i_1: usize = 0; - while (blk_3: { - const pos_3 = p.i; - if (try p.parseParamDecl() and try p.parseCOMMA()) break :blk_3 true; - p.i = pos_3; - break :blk_3 false; - }) { - if (i_1 > max_depth) return error.MaxDepth; - i_1 += 1; - } - break :blk_1 true; - } and (blk_3: { - const pos_3 = p.i; - if (try p.parseParamDecl()) break :blk_3 true; - p.i = pos_3; - if (try p.parseDOT3() and (try p.parseCOMMA() or true)) break :blk_3 true; - p.i = pos_3; - break :blk_3 false; - } or true)) break :blk_0 true; + if (try p.parseLPAREN() and try p.parseParamDeclListRest()) break :blk_0 true; + p.i = pos_0; + break :blk_0 false; + }; + } + pub fn parseParamDeclListRest(p: *Parser) Error!bool { + return blk_0: { + const pos_0 = p.i; + if (try p.parseRPAREN()) break :blk_0 true; + p.i = pos_0; + if (try p.parseDOT3() and (try p.parseCOMMA() or true) and try p.parseRPAREN()) break :blk_0 true; + p.i = pos_0; + if (try p.parseParamDecl() and blk_2: { + const pos_2 = p.i; + if (try p.parseCOMMA() and try p.parseParamDeclListRest()) break :blk_2 true; + p.i = pos_2; + if (try p.parseRPAREN()) break :blk_2 true; + p.i = pos_2; + break :blk_2 false; + }) break :blk_0 true; p.i = pos_0; break :blk_0 false; }; -- 2.54.0