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 {...@@ -138,7 +138,7 @@ pub fn deinit(tree: *Ast, gpa: Allocator) void {
138 tree.* = undefined;138 tree.* = undefined;
139}139}
140140
141pub const Mode = enum { zig, zon };141pub const Mode = enum { zig, zon, zig_no_recover };
142142
143/// Result should be freed with tree.deinit() when there are143/// Result should be freed with tree.deinit() when there are
144/// no more references to any of the tokens or nodes.144/// no more references to any of the tokens or nodes.
...@@ -180,6 +180,7 @@ pub fn parseTokens(...@@ -180,6 +180,7 @@ pub fn parseTokens(
180 .extra_data = .empty,180 .extra_data = .empty,
181 .scratch = .empty,181 .scratch = .empty,
182 .tok_i = 0,182 .tok_i = 0,
183 .recover = true,
183 };184 };
184 defer parser.errors.deinit(gpa);185 defer parser.errors.deinit(gpa);
185 defer parser.nodes.deinit(gpa);186 defer parser.nodes.deinit(gpa);
...@@ -193,6 +194,10 @@ pub fn parseTokens(...@@ -193,6 +194,10 @@ pub fn parseTokens(
193194
194 switch (mode) {195 switch (mode) {
195 .zig => try parser.parseRoot(),196 .zig => try parser.parseRoot(),
197 .zig_no_recover => {
198 parser.recover = false;
199 try parser.parseRoot();
200 },
196 .zon => try parser.parseZon(),201 .zon => try parser.parseZon(),
197 }202 }
198203
lib/std/zig/Parse.zig+14-1
...@@ -23,6 +23,7 @@ errors: std.ArrayList(AstError),...@@ -23,6 +23,7 @@ errors: std.ArrayList(AstError),
23nodes: Ast.NodeList,23nodes: Ast.NodeList,
24extra_data: std.ArrayList(u32),24extra_data: std.ArrayList(u32),
25scratch: std.ArrayList(Node.Index),25scratch: std.ArrayList(Node.Index),
26recover: bool,
2627
27fn tokenTag(p: *const Parse, token_index: TokenIndex) Token.Tag {28fn tokenTag(p: *const Parse, token_index: TokenIndex) Token.Tag {
28 return p.tokens.items(.tag)[token_index];29 return p.tokens.items(.tag)[token_index];
...@@ -497,6 +498,12 @@ fn parseContainerMembers(p: *Parse) Allocator.Error!Members {...@@ -497,6 +498,12 @@ fn parseContainerMembers(p: *Parse) Allocator.Error!Members {
497498
498/// Attempts to find next container member by searching for certain tokens499/// Attempts to find next container member by searching for certain tokens
499fn findNextContainerMember(p: *Parse) void {500fn 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 }
500 var level: u32 = 0;507 var level: u32 = 0;
501 while (true) {508 while (true) {
502 const tok = p.nextToken();509 const tok = p.nextToken();
...@@ -554,6 +561,12 @@ fn findNextContainerMember(p: *Parse) void {...@@ -554,6 +561,12 @@ fn findNextContainerMember(p: *Parse) void {
554561
555/// Attempts to find the next statement by searching for a semicolon562/// Attempts to find the next statement by searching for a semicolon
556fn findNextStmt(p: *Parse) void {563fn 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 }
557 var level: u32 = 0;570 var level: u32 = 0;
558 while (true) {571 while (true) {
559 const tok = p.nextToken();572 const tok = p.nextToken();
...@@ -3615,7 +3628,7 @@ fn expectSemicolon(p: *Parse, error_tag: AstError.Tag, recoverable: bool) Error!...@@ -3615,7 +3628,7 @@ fn expectSemicolon(p: *Parse, error_tag: AstError.Tag, recoverable: bool) Error!
3615 return;3628 return;
3616 }3629 }
3617 try p.warn(error_tag);3630 try p.warn(error_tag);
3618 if (!recoverable) return error.ParseError;3631 if (!recoverable or !p.recover) return error.ParseError;
3619}3632}
36203633
3621fn nextToken(p: *Parse) TokenIndex {3634fn nextToken(p: *Parse) TokenIndex {
lib/std/zig/parser_fuzz.zig+9-2
...@@ -123,10 +123,17 @@ fn checkAgainstOracle(source: [:0]const u8) !void {...@@ -123,10 +123,17 @@ fn checkAgainstOracle(source: [:0]const u8) !void {
123 var fba_buf: [1 << 18]u8 = undefined;123 var fba_buf: [1 << 18]u8 = undefined;
124 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);124 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);
125125
126 const ast = try std.zig.Ast.parse(fba.allocator(), source, .zig);
127
128 const expected = try oracle.parse(source);126 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
130 errdefer logBadSource(source, ast);137 errdefer logBadSource(source, ast);
131 try std.testing.expectEqual(expected, ast.errors.len == 0);138 try std.testing.expectEqual(expected, ast.errors.len == 0);
132}139}