Skip to content

Add cross-platform memory map abstraction and use it in libfuzzer. #21083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 24 additions & 61 deletions lib/fuzzer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const Fuzzer = struct {
/// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.
/// Stored in a memory-mapped file so that it can be shared with other
/// processes and viewed while the fuzzer is running.
seen_pcs: MemoryMappedList,
seen_pcs: std.fs.MemoryMap,
cache_dir: std.fs.Dir,
/// Identifies the file name that will be used to store coverage
/// information, available to other processes.
Expand Down Expand Up @@ -229,11 +229,17 @@ const Fuzzer = struct {
} else if (existing_len != bytes_len) {
fatal("incompatible existing coverage file (differing lengths)", .{});
}
f.seen_pcs = MemoryMappedList.init(coverage_file, existing_len, bytes_len) catch |err| {
f.seen_pcs = std.fs.MemoryMap.init(coverage_file, .{
.exclusivity = .shared,
.protection = .{ .write = true },
.length = bytes_len,
}) catch |err| {
fatal("unable to init coverage memory map: {s}", .{@errorName(err)});
};
if (existing_len != 0) {
const existing_pcs_bytes = f.seen_pcs.items[@sizeOf(SeenPcsHeader) + @sizeOf(usize) * n_bitset_elems ..][0 .. flagged_pcs.len * @sizeOf(usize)];
const existing_pcs_start = @sizeOf(SeenPcsHeader) + @sizeOf(usize) * n_bitset_elems;
const existing_pcs_end = existing_pcs_start + flagged_pcs.len * @sizeOf(usize);
const existing_pcs_bytes = f.seen_pcs.mapped[existing_pcs_start..existing_pcs_end];
const existing_pcs = std.mem.bytesAsSlice(usize, existing_pcs_bytes);
for (existing_pcs, flagged_pcs, 0..) |old, new, i| {
if (old != new.addr) {
Expand All @@ -249,11 +255,18 @@ const Fuzzer = struct {
.pcs_len = flagged_pcs.len,
.lowest_stack = std.math.maxInt(usize),
};
f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header));
f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems * @sizeOf(usize));
for (flagged_pcs) |flagged_pc| {
f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&flagged_pc.addr));
}
f.seen_pcs.cast(SeenPcsHeader).* = header;
const bitset_elems_start = @sizeOf(SeenPcsHeader);
const bitset_elems_end = bitset_elems_start + n_bitset_elems * @sizeOf(usize);
const bitset_elems_bytes = f.seen_pcs.mapped[bitset_elems_start..bitset_elems_end];
const bitset_elems_dest = std.mem.bytesAsSlice(usize, bitset_elems_bytes);
@memset(bitset_elems_dest, 0);
const flagged_pcs_start = bitset_elems_end;
const flagged_pcs_end = flagged_pcs_start + flagged_pcs.len * @sizeOf(usize);
const flagged_pcs_bytes = f.seen_pcs.mapped[flagged_pcs_start..flagged_pcs_end];
const flagged_pcs_dest = std.mem.bytesAsSlice(usize, flagged_pcs_bytes);
for (flagged_pcs, flagged_pcs_dest) |item, *slot|
slot.* = item.addr;
}
}

Expand Down Expand Up @@ -306,7 +319,7 @@ const Fuzzer = struct {
{
// Track code coverage from all runs.
comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);
const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]);
const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.mapped[@sizeOf(SeenPcsHeader)..]);
const remainder = f.flagged_pcs.len % @bitSizeOf(usize);
const aligned_len = f.flagged_pcs.len - remainder;
const seen_pcs = header_end_ptr[0..aligned_len];
Expand All @@ -330,7 +343,7 @@ const Fuzzer = struct {
}
}

const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
const header = f.seen_pcs.cast(SeenPcsHeader);
_ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
}

Expand Down Expand Up @@ -360,7 +373,7 @@ const Fuzzer = struct {
try f.mutate();

f.n_runs += 1;
const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
const header = f.seen_pcs.cast(SeenPcsHeader);
_ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
_ = @atomicRmw(usize, &header.lowest_stack, .Min, __sancov_lowest_stack, .monotonic);
@memset(f.pc_counters, 0);
Expand Down Expand Up @@ -468,53 +481,3 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {

fuzzer.init(cache_dir) catch |err| fatal("unable to init fuzzer: {s}", .{@errorName(err)});
}

/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.
pub const MemoryMappedList = struct {
/// Contents of the list.
///
/// Pointers to elements in this slice are invalidated by various functions
/// of this ArrayList in accordance with the respective documentation. In
/// all cases, "invalidated" means that the memory has been passed to this
/// allocator's resize or free function.
items: []align(std.mem.page_size) volatile u8,
/// How many bytes this list can hold without allocating additional memory.
capacity: usize,

pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList {
const ptr = try std.posix.mmap(
null,
capacity,
std.posix.PROT.READ | std.posix.PROT.WRITE,
.{ .TYPE = .SHARED },
file.handle,
0,
);
return .{
.items = ptr[0..length],
.capacity = capacity,
};
}

/// Append the slice of items to the list.
/// Asserts that the list can hold the additional items.
pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void {
const old_len = l.items.len;
const new_len = old_len + items.len;
assert(new_len <= l.capacity);
l.items.len = new_len;
@memcpy(l.items[old_len..][0..items.len], items);
}

/// Append a value to the list `n` times.
/// Never invalidates element pointers.
/// The function is inline so that a comptime-known `value` parameter will
/// have better memset codegen in case it has a repeated byte pattern.
/// Asserts that the list can hold the additional items.
pub inline fn appendNTimesAssumeCapacity(l: *MemoryMappedList, value: u8, n: usize) void {
const new_len = l.items.len + n;
assert(new_len <= l.capacity);
@memset(l.items.ptr[l.items.len..new_len], value);
l.items.len = new_len;
}
};
2 changes: 2 additions & 0 deletions lib/std/fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const is_darwin = native_os.isDarwin();
pub const AtomicFile = @import("fs/AtomicFile.zig");
pub const Dir = @import("fs/Dir.zig");
pub const File = @import("fs/File.zig");
pub const MemoryMap = @import("fs/MemoryMap.zig");
pub const path = @import("fs/path.zig");

pub const has_executable_bit = switch (native_os) {
Expand Down Expand Up @@ -710,6 +711,7 @@ test {
_ = &AtomicFile;
_ = &Dir;
_ = &File;
_ = &MemoryMap;
_ = &path;
_ = @import("fs/test.zig");
_ = @import("fs/get_app_data_dir.zig");
Expand Down
206 changes: 206 additions & 0 deletions lib/std/fs/MemoryMap.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//! A cross-platform abstraction for memory-mapping files.
//!
//! The API here implements the common subset of functionality present in the supported operating
//! systems. Presently, Windows and all POSIX environments are supported.
//!
//! Operating system specific behavior is intended to be minimized; however, the following leak
//! through the abstraction:
//!
//! - Child processes sharing:
//! - POSIX: Shared with child processes upon `fork` and unshared upon `exec*`.
//! - Windows: Not shared with child processes.

const std = @import("../std.zig");
const builtin = @import("builtin");

const MemoryMap = @This();

/// An OS-specific reference to a kernel object for this mapping.
handle: switch (builtin.os.tag) {
.windows => std.os.windows.HANDLE,
else => void,
},
/// The region of virtual memory in which the file is mapped.
///
/// Accesses to this are subject to the protection semantics specified upon
/// initialization of the mapping. Failure to abide by those semantics has undefined
/// behavior (though should be well-defined by the OS).
mapped: []align(std.mem.page_size) volatile u8,

test MemoryMap {
if (builtin.os.tag == .wasi) return error.SkipZigTest;

var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();

var file = try tmp.dir.createFile("mmap.bin", .{
.exclusive = true,
.truncate = true,
.read = true,
});
defer file.close();

const magic = "\xde\xca\xfb\xad";
try file.writeAll(magic);

const len = try file.getEndPos();

var view = try MemoryMap.init(file, .{ .length = @intCast(len) });
defer view.deinit();

try std.testing.expectEqualSlices(u8, magic, @volatileCast(view.mapped));
}

pub const InitOptions = struct {
protection: ProtectionFlags = .{},
/// Whether changes to the memory-mapped region should be propogated into the backing file.
///
/// This only refers to the exclusivity of the memory-mapped region with respect to *other*
/// instances of `MemoryMap` of that same file. A single `MemoryMap` instance can be shared
/// within a process regardless of this option. Whether a single `MemoryMap` instance is shared
/// with child processes is operating system specific and independent of this option.
exclusivity: Exclusivity = .private,
/// The desired length of the mapping.
///
/// The backing file must be of at least `offset + length` size.
length: usize,
/// The desired offset of the mapping.
///
/// The backing file must be of at least `offset` size.
offset: usize = 0,
hint: ?[*]align(std.mem.page_size) u8 = null,
};

/// A description of OS protections to be applied to a memory-mapped region.
pub const ProtectionFlags = struct {
write: bool = false,
execute: bool = false,
};

pub const Exclusivity = enum {
/// The file's content may be read or written by external processes.
shared,
/// The file's content is exclusive to this process.
private,
};

/// Create a memory-mapped view into `file`.
///
/// Asserts `opts.length` is non-zero.
pub fn init(file: std.fs.File, opts: InitOptions) !MemoryMap {
std.debug.assert(opts.length > 0);
switch (builtin.os.tag) {
.wasi => @compileError("MemoryMap not supported on WASI OS; see also " ++
"https://github.com/WebAssembly/WASI/issues/304"),
.windows => {
// Create the kernel resource for the memory mapping.
var access: std.os.windows.ACCESS_MASK =
std.os.windows.STANDARD_RIGHTS_REQUIRED |
std.os.windows.SECTION_QUERY |
std.os.windows.SECTION_MAP_READ;
var page_attributes: std.os.windows.ULONG = 0;
if (opts.protection.execute) {
access |= std.os.windows.SECTION_MAP_EXECUTE;
if (opts.protection.write) {
access |= std.os.windows.SECTION_MAP_WRITE;
page_attributes = switch (opts.exclusivity) {
.shared => std.os.windows.PAGE_EXECUTE_READWRITE,
.private => std.os.windows.PAGE_EXECUTE_WRITECOPY,
};
} else {
page_attributes = std.os.windows.PAGE_EXECUTE_READ;
}
} else {
if (opts.protection.write) {
access |= std.os.windows.SECTION_MAP_WRITE;
page_attributes = switch (opts.exclusivity) {
.shared => std.os.windows.PAGE_READWRITE,
.private => std.os.windows.PAGE_WRITECOPY,
};
} else {
page_attributes = std.os.windows.PAGE_READONLY;
}
}
const handle = try std.os.windows.CreateSection(.{
.file = file.handle,
.access = access,
.size = opts.length,
.page_attributes = page_attributes,
});
errdefer std.os.windows.CloseHandle(handle);

// Create the mapping.
const mapped = try std.os.windows.MapViewOfSection(handle, .{
.inheritance = .ViewUnmap,
.protection = page_attributes,
.offset = opts.offset,
.length = opts.length,
.hint = opts.hint,
});

return .{
.handle = handle,
.mapped = mapped,
};
},
else => {
// The man page indicates the flags must be either `NONE` or an OR of the
// flags. That doesn't explicitly state that the absence of those flags is
// the same as `NONE`, so this static assertion is made. That'll break the
// build rather than behaving unexpectedly if some weird system comes up.
comptime std.debug.assert(std.posix.PROT.NONE == 0);

// Convert the public options into POSIX specific options.
var prot: u32 = std.posix.PROT.READ;
if (opts.protection.write)
prot |= std.posix.PROT.WRITE;
if (opts.protection.execute)
prot |= std.posix.PROT.EXEC;
const flags: std.posix.MAP = .{
.TYPE = switch (opts.exclusivity) {
.shared => .SHARED,
.private => .PRIVATE,
},
};

// Create the mapping.
const mapped = try std.posix.mmap(
opts.hint,
opts.length,
prot,
@bitCast(flags),
file.handle,
opts.offset,
);

return .{
.handle = {},
.mapped = mapped,
};
},
}
}

/// Unmap the file from virtual memory and deallocate kernel resources.
///
/// Invalidates references to `self.mapped`.
pub fn deinit(self: MemoryMap) void {
switch (builtin.os.tag) {
.windows => {
std.os.windows.UnmapViewOfSection(@volatileCast(self.mapped.ptr));
std.os.windows.CloseHandle(self.handle);
},
else => {
std.posix.munmap(@volatileCast(self.mapped));
},
}
}

/// Reinterpret `self.mapped` as `T`.
///
/// The returned pointer is aligned to the beginning of the mapping. The mapping may be
/// larger than `T`. The caller is responsible for determining whether volatility can be
/// stripped away through external synchronization.
pub inline fn cast(self: MemoryMap, comptime T: type) *align(std.mem.page_size) volatile T {
return std.mem.bytesAsValue(T, self.mapped[0..@sizeOf(T)]);
}
Loading