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