authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-03 15:43:35+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-08 11:49:00+02:00
log62720ad9f9e65dd8c62579c90142cb043d60b01c
tree357853cfd85e750f49021c5f34bc9721bba98f1a
parent4fa5b59d4470daa595b67e36ceb8290064cbc8a7
signaturelock-open Commit is signed but in an unrecognized format.

std.zig.Ast: allow disabling parser recovery

This is necessary for fuzz testing. Consider the case where there is a parse error right at the beginning of the file, followed by a valid declaration with a million nested parens. The oracle will not skip this input due to the max depth being exceeded since the oracle hits a parse error right away and does no recovery. However, std.zig.Ast.parse() does recovery by default and will hit a stack overflow rather than returning after the parser error. Stack overflows are not interesting and we do not want the fuzzer to be able to find them.

3 files changed, 29 insertions(+), 4 deletions(-)

lib/std/zig/Ast.zig+6-1
......@@ -138,7 +138,7 @@ pub fn deinit(tree: *Ast, gpa: Allocator) void {
138138 tree.* = undefined;
139139}
140140
141pub const Mode = enum { zig, zon };
141pub const Mode = enum { zig, zon, zig_no_recover };
142142
143143/// Result should be freed with tree.deinit() when there are
144144/// no more references to any of the tokens or nodes.
......@@ -180,6 +180,7 @@ pub fn parseTokens(
180180 .extra_data = .empty,
181181 .scratch = .empty,
182182 .tok_i = 0,
183 .recover = true,
183184 };
184185 defer parser.errors.deinit(gpa);
185186 defer parser.nodes.deinit(gpa);
......@@ -193,6 +194,10 @@ pub fn parseTokens(
193194
194195 switch (mode) {
195196 .zig => try parser.parseRoot(),
197 .zig_no_recover => {
198 parser.recover = false;
199 try parser.parseRoot();
200 },
196201 .zon => try parser.parseZon(),
197202 }
198203
lib/std/zig/Parse.zig+14-1
......@@ -23,6 +23,7 @@ errors: std.ArrayList(AstError),
2323nodes: Ast.NodeList,
2424extra_data: std.ArrayList(u32),
2525scratch: std.ArrayList(Node.Index),
26recover: bool,
2627
2728fn tokenTag(p: *const Parse, token_index: TokenIndex) Token.Tag {
2829 return p.tokens.items(.tag)[token_index];
......@@ -497,6 +498,12 @@ fn parseContainerMembers(p: *Parse) Allocator.Error!Members {
497498
498499/// Attempts to find next container member by searching for certain tokens
499500fn findNextContainerMember(p: *Parse) void {
501 if (!p.recover) {
502 while (p.tokenTag(p.tok_i) != .eof) {
503 p.tok_i += 1;
504 }
505 return;
506 }
500507 var level: u32 = 0;
501508 while (true) {
502509 const tok = p.nextToken();
......@@ -554,6 +561,12 @@ fn findNextContainerMember(p: *Parse) void {
554561
555562/// Attempts to find the next statement by searching for a semicolon
556563fn findNextStmt(p: *Parse) void {
564 if (!p.recover) {
565 while (p.tokenTag(p.tok_i) != .eof) {
566 p.tok_i += 1;
567 }
568 return;
569 }
557570 var level: u32 = 0;
558571 while (true) {
559572 const tok = p.nextToken();
......@@ -3615,7 +3628,7 @@ fn expectSemicolon(p: *Parse, error_tag: AstError.Tag, recoverable: bool) Error!
36153628 return;
36163629 }
36173630 try p.warn(error_tag);
3618 if (!recoverable) return error.ParseError;
3631 if (!recoverable or !p.recover) return error.ParseError;
36193632}
36203633
36213634fn nextToken(p: *Parse) TokenIndex {
lib/std/zig/parser_fuzz.zig+9-2
......@@ -123,10 +123,17 @@ fn checkAgainstOracle(source: [:0]const u8) !void {
123123 var fba_buf: [1 << 18]u8 = undefined;
124124 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);
125125
126 const ast = try std.zig.Ast.parse(fba.allocator(), source, .zig);
127
128126 const expected = try oracle.parse(source);
129127
128 // It is important to disable recovery for fuzz testing.
129 // Consider the case where there is a parse error right at the beginning of the file,
130 // followed by a valid declaration with a million nested parens. The oracle will not
131 // skip this input due to the max depth being exceeded since the oracle hits a parse
132 // error right away and does no recovery. However, std.zig.Ast.parse() does recovery
133 // by default and will hit a stack overflow rather than returning after the parser error.
134 // Stack overflows are not interesting and we do not want the fuzzer to be able to find them.
135 const ast = try std.zig.Ast.parse(fba.allocator(), source, .zig_no_recover);
136
130137 errdefer logBadSource(source, ast);
131138 try std.testing.expectEqual(expected, ast.errors.len == 0);
132139}