Skip to content

Commit 303969a

Browse files
Loris CroLoris Cro
authored andcommitted
fuzz subcommand & fixes from frist round of fuzzing
1 parent 0657499 commit 303969a

9 files changed

Lines changed: 154 additions & 62 deletions

File tree

build.zig

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,9 @@ pub fn build(b: *std.Build) !void {
66
const mode = .{ .target = target, .optimize = optimize };
77

88
const scripty = b.dependency("scripty", mode);
9-
10-
const super = b.addModule("super", .{
11-
.root_source_file = b.path("src/root.zig"),
12-
});
13-
9+
const super = b.addModule("super", .{ .root_source_file = b.path("src/root.zig") });
1410
super.addImport("scripty", scripty.module("scripty"));
1511

16-
// super.include_dirs.append(b.allocator, .{ .other_step = ts.artifact("tree-sitter") }) catch unreachable;
17-
1812
const unit_tests = b.addTest(.{
1913
.root_source_file = b.path("src/root.zig"),
2014
.target = target,
@@ -164,14 +158,26 @@ pub fn build(b: *std.Build) !void {
164158
super_fuzz.root_module.link_libc = true; // afl runtime depends on libc
165159
_ = super_fuzz.getEmittedBin(); // hack around build system bug
166160

167-
const afl_clang_fast_path = try b.findProgram(
161+
const afl_clang_fast_path = b.findProgram(
168162
&.{ "afl-clang-fast", "afl-clang" },
169163
if (b.option([]const u8, "afl-path", "Path to AFLplusplus")) |afl_path| &.{afl_path} else &.{},
170-
);
164+
) catch "afl-clang";
165+
166+
const fuzz = b.step("fuzz", "Generate an executable to fuzz html/Parser");
171167
const run_afl_clang_fast = b.addSystemCommand(&.{ afl_clang_fast_path, "-o" });
172168
const prog_exe = run_afl_clang_fast.addOutputFileArg(super_fuzz_name);
173169
run_afl_clang_fast.addFileArg(super_fuzz.getEmittedLlvmBc());
174-
175-
const fuzz = b.step("fuzz", "Generate an executable to fuzz html/Parser");
176170
fuzz.dependOn(&b.addInstallBinFile(prog_exe, super_fuzz_name).step);
171+
172+
const fuzz_tests = b.addTest(.{
173+
.root_source_file = b.path("src/fuzz.zig"),
174+
.target = target,
175+
.optimize = .Debug,
176+
// .strip = true,
177+
// .filter = "nesting",
178+
});
179+
180+
fuzz_tests.root_module.addImport("super", super);
181+
const run_fuzz_tests = b.addRunArtifact(fuzz_tests);
182+
test_step.dependOn(&run_fuzz_tests.step);
177183
}

src/fuzz.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,18 @@ pub fn main() !void {
1717
const ast = try super.html.Ast.init(gpa, data, .html);
1818
defer ast.deinit(gpa);
1919
}
20+
21+
test "afl++ fuzz cases" {
22+
const cases: []const []const u8 = &.{
23+
@embedFile("fuzz/2.html"),
24+
@embedFile("fuzz/3.html"),
25+
@embedFile("fuzz/12.html"),
26+
};
27+
28+
for (cases) |c| {
29+
std.debug.print("test: \n\n{s}\n\n", .{c});
30+
const ast = try super.html.Ast.init(std.testing.allocator, c, .html);
31+
defer ast.deinit(std.testing.allocator);
32+
ast.debug(c);
33+
}
34+
}

src/fuzz/12.html

83 Bytes
Binary file not shown.

src/fuzz/2.html

66 Bytes
Binary file not shown.

src/fuzz/3.html

171 Bytes
Binary file not shown.

src/html.zig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
pub const Ast = @import("html/Ast.zig");
22
pub const Tokenizer = @import("html/Tokenizer.zig");
3-
pub const max_size = 4 * 1024 * 1024 * 1024;
3+
4+
test {
5+
_ = @import("html/Tokenizer.zig");
6+
_ = @import("html/Ast.zig");
7+
}

src/html/Ast.zig

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,10 @@ pub fn init(
360360
const original_current_idx = current_idx;
361361

362362
if (current.isClosed()) {
363+
log.debug("current {} is closed, going up to {}", .{
364+
current_idx,
365+
current.parent_idx,
366+
});
363367
current_idx = current.parent_idx;
364368
current = &nodes.items[current.parent_idx];
365369
}
@@ -385,12 +389,12 @@ pub fn init(
385389
// malformed HTML and we also expect in all of
386390
// those cases that errors were already emitted
387391
// by the tokenizer
388-
const maybe_name = temp_tok.next(tag_src) orelse break;
389-
390-
switch (maybe_name) {
391-
.tag_name => |n| break :blk n.slice(tag_src),
392-
else => break,
393-
}
392+
const name_span = temp_tok.getName(tag_src) orelse {
393+
current = original_current;
394+
current_idx = original_current_idx;
395+
break;
396+
};
397+
break :blk name_span.slice(tag_src);
394398
};
395399

396400
log.debug("matching cn: {s} tag: {s}", .{
@@ -415,7 +419,7 @@ pub fn init(
415419
.return_attrs = true,
416420
};
417421
const tag_src = cur.open.slice(src);
418-
const rel_name = temp_tok.next(tag_src).?.tag_name;
422+
const rel_name = temp_tok.getName(tag_src).?;
419423
break :blk .{
420424
.start = rel_name.start + cur.open.start,
421425
.end = rel_name.end + cur.open.start,
@@ -447,6 +451,9 @@ pub fn init(
447451
switch (current.direction()) {
448452
.in => {
449453
new.parent_idx = current_idx;
454+
if (current.first_child_idx != 0) {
455+
debugNodes(nodes.items, src);
456+
}
450457
std.debug.assert(current.first_child_idx == 0);
451458
current_idx = @intCast(nodes.items.len);
452459
current.first_child_idx = current_idx;
@@ -826,6 +833,52 @@ const Formatter = struct {
826833
}
827834
};
828835

836+
pub fn debug(ast: Ast, src: []const u8) void {
837+
var c = ast.cursor(0);
838+
var last_depth: u32 = 0;
839+
std.debug.print(" \n node count: {}\n", .{ast.nodes.len});
840+
while (c.next()) |n| {
841+
if (c.dir == .out) {
842+
std.debug.print("\n", .{});
843+
while (last_depth > c.depth) : (last_depth -= 1) {
844+
for (0..last_depth - 2) |_| std.debug.print(" ", .{});
845+
std.debug.print(")", .{});
846+
if (last_depth - c.depth > 1) {
847+
std.debug.print("\n", .{});
848+
}
849+
}
850+
last_depth = c.depth;
851+
continue;
852+
}
853+
std.debug.print("\n", .{});
854+
for (0..c.depth - 1) |_| std.debug.print(" ", .{});
855+
const range = n.open.range(src);
856+
std.debug.print("({s} #{} @{} [{}, {}] - [{}, {}]", .{
857+
@tagName(n.kind),
858+
c.idx,
859+
c.depth,
860+
range.start.row,
861+
range.start.col,
862+
range.end.row,
863+
range.end.col,
864+
});
865+
if (n.first_child_idx == 0) {
866+
std.debug.print(")", .{});
867+
}
868+
last_depth = c.depth;
869+
}
870+
std.debug.print("\n", .{});
871+
while (last_depth > 1) : (last_depth -= 1) {
872+
for (0..last_depth - 2) |_| std.debug.print(" ", .{});
873+
std.debug.print(")\n", .{});
874+
}
875+
}
876+
877+
fn debugNodes(nodes: []const Node, src: []const u8) void {
878+
const ast = Ast{ .nodes = nodes, .errors = &.{}, .language = .html };
879+
ast.debug(src);
880+
}
881+
829882
test "basics" {
830883
const case = "<html><head></head><body><div><link></div></body></html>";
831884

src/html/Tokenizer.zig

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,17 @@ fn consume(self: *Tokenizer, src: []const u8) bool {
382382
return true;
383383
}
384384

385+
pub fn getName(tokenizer: *Tokenizer, tag_src: []const u8) ?Span {
386+
std.debug.assert(tokenizer.return_attrs);
387+
return while (tokenizer.next(tag_src)) |maybe_name| {
388+
switch (maybe_name) {
389+
.tag_name => |n| break n,
390+
.tag => break null,
391+
else => continue,
392+
}
393+
} else null;
394+
}
395+
385396
pub fn next(self: *Tokenizer, src: []const u8) ?Token {
386397
if (self.deferred_token) |t| {
387398
const token_copy = t;
@@ -2131,20 +2142,17 @@ fn next2(self: *Tokenizer, src: []const u8) ?struct {
21312142
.before_attribute_name => |state| {
21322143
// See EOF case from below
21332144
if (!self.consume(src)) {
2134-
self.state = .data;
2135-
var tag = state;
2136-
tag.span.end = self.idx;
2137-
return .{ .token = .{ .tag = tag } };
2138-
// self.idx -= 1;
2139-
// self.state = .{
2140-
// .after_attribute_name = .{
2141-
// .tag = state,
2142-
// .name = .{
2143-
// .start = self.idx,
2144-
// .end = self.idx + 1,
2145-
// },
2146-
// },
2147-
// };
2145+
// self.state = .data;
2146+
// var tag = state;
2147+
// tag.span.end = self.idx;
2148+
// return .{ .token = .{ .tag = tag } };
2149+
self.idx -= 1;
2150+
self.state = .{
2151+
.after_attribute_name = .{
2152+
.tag = state,
2153+
.name = .{ .start = 0, .end = 0 },
2154+
},
2155+
};
21482156
} else switch (self.current) {
21492157
// U+0009 CHARACTER TABULATION (tab)
21502158
// U+000A LINE FEED (LF)
@@ -2157,27 +2165,22 @@ fn next2(self: *Tokenizer, src: []const u8) ?struct {
21572165
// U+003E GREATER-THAN SIGN (>)
21582166
// EOF
21592167
// Reconsume in the after attribute name state.
2160-
// NOTE: handled differently to simplify
2161-
// control flow in after_attribute_name
2162-
// as otherwise it would need to keep
2163-
// track of when we don't have an attribute
2164-
// at all
21652168
'/', '>' => {
2166-
self.state = .data;
2167-
var tag = state;
2168-
tag.span.end = self.idx;
2169-
if (self.current == '/') {
2170-
std.debug.assert(tag.kind == .start);
2171-
tag.kind = .start_self;
2172-
}
2173-
return .{ .token = .{ .tag = tag } };
2174-
// self.idx -= 1;
2175-
// self.state = .{
2176-
// .after_attribute_name = .{
2177-
// .tag = state,
2178-
// .name = .{ .start = 0, .end = 0 },
2179-
// },
2180-
// };
2169+
// self.state = .data;
2170+
// var tag = state;
2171+
// tag.span.end = self.idx;
2172+
// if (self.current == '/') {
2173+
// std.debug.assert(tag.kind == .start);
2174+
// tag.kind = .start_self;
2175+
// }
2176+
// return .{ .token = .{ .tag = tag } };
2177+
self.idx -= 1;
2178+
self.state = .{
2179+
.after_attribute_name = .{
2180+
.tag = state,
2181+
.name = .{ .start = 0, .end = 0 },
2182+
},
2183+
};
21812184
},
21822185

21832186
//U+003D EQUALS SIGN (=)
@@ -2301,6 +2304,8 @@ fn next2(self: *Tokenizer, src: []const u8) ?struct {
23012304
// EOF
23022305
// This is an eof-in-tag parse error. Emit an end-of-file token.
23032306
self.state = .eof;
2307+
// here, in > and in / there could be no attr name
2308+
// to return.
23042309
return .{
23052310
.token = .{
23062311
.parse_error = .{
@@ -2323,9 +2328,14 @@ fn next2(self: *Tokenizer, src: []const u8) ?struct {
23232328
// Switch to the self-closing start tag state.
23242329
'/' => {
23252330
var tag = state.tag;
2326-
tag.attr_count += 1;
2331+
2332+
// here, in > and in EOF there could be no attr name
2333+
// to return.
2334+
if (state.name.len() > 0) {
2335+
tag.attr_count += 1;
2336+
}
23272337
self.state = .{ .self_closing_start_tag = tag };
2328-
if (self.return_attrs) {
2338+
if (self.return_attrs and state.name.len() > 0) {
23292339
return .{
23302340
.token = .{
23312341
.attr = .{
@@ -2350,10 +2360,14 @@ fn next2(self: *Tokenizer, src: []const u8) ?struct {
23502360
'>' => {
23512361
var tag = state.tag;
23522362
tag.span.end = self.idx;
2353-
tag.attr_count += 1;
2363+
// here, in / and in EOF there could be no attr name
2364+
// to return.
2365+
if (state.name.len() > 0) {
2366+
tag.attr_count += 1;
2367+
}
23542368

23552369
self.state = .data;
2356-
if (self.return_attrs) {
2370+
if (self.return_attrs and state.name.len() > 0) {
23572371
return .{
23582372
.token = .{
23592373
.attr = .{

src/root.zig

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
// const interpreter = @import("interpreter.zig");
22
const std = @import("std");
33
pub const html = @import("html.zig");
4-
pub const max_size = html.max_size;
54
pub const Ast = @import("Ast.zig");
6-
7-
pub const Language = enum { html, superhtml };
8-
95
// pub const SuperVM = interpreter.SuperVM;
106
// pub const Exception = interpreter.Exception;
117

8+
pub const Language = enum { html, superhtml };
9+
pub const max_size = 4 * 1024 * 1024 * 1024;
10+
1211
const Range = struct {
1312
start: Pos,
1413
end: Pos,
@@ -78,6 +77,7 @@ pub const Span = struct {
7877
};
7978

8079
test {
80+
_ = @import("html.zig");
81+
// _ = @import("Ast.zig");
8182
// _ = @import("template.zig");
82-
_ = @import("Ast.zig");
8383
}

0 commit comments

Comments
 (0)