constraint solving
This commit is contained in:
@@ -51,7 +51,7 @@ pub fn build(b: *std.Build) !void {
|
|||||||
try compileTestApplications(b, target, optimize, false, true);
|
try compileTestApplications(b, target, optimize, false, true);
|
||||||
try compileTestApplications(b, target, optimize, true, true);
|
try compileTestApplications(b, target, optimize, true, true);
|
||||||
|
|
||||||
const exe_tests = b.addTest(.{ .root_module = mod });
|
const exe_tests = b.addTest(.{ .root_module = mod, .use_llvm = true });
|
||||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||||
const test_step = b.step("test", "Run tests");
|
const test_step = b.step("test", "Run tests");
|
||||||
test_step.dependOn(b.getInstallStep());
|
test_step.dependOn(b.getInstallStep());
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,447 +0,0 @@
|
|||||||
//! Iterates through all possible valid address ranges for a `jmp rel33` instruction based on a
|
|
||||||
//! 4-byte pattern of "free" and "used" bytes.
|
|
||||||
//!
|
|
||||||
//! This is the core utility for implementing E9Patch-style instruction punning (B2) and padded
|
|
||||||
//! jumps (T1).
|
|
||||||
const std = @import("std");
|
|
||||||
const testing = std.testing;
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.patch_location_iterator);
|
|
||||||
|
|
||||||
const Range = @import("Range.zig");
|
|
||||||
|
|
||||||
/// Represents a single byte in the 4-byte `rel32` offset pattern.
|
|
||||||
pub const PatchByte = union(enum) {
|
|
||||||
/// This byte can be any value (0x00-0xFF).
|
|
||||||
free: void,
|
|
||||||
/// This byte is constrained to a specific value.
|
|
||||||
used: u8,
|
|
||||||
|
|
||||||
pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
|
||||||
switch (self) {
|
|
||||||
.free => try writer.print("free", .{}),
|
|
||||||
.used => |val| try writer.print("used({x})", .{val}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const patch_size = 4;
|
|
||||||
const PatchInt = std.meta.Int(.signed, patch_size * 8);
|
|
||||||
const PatchLocationIterator = @This();
|
|
||||||
/// The base address (e.g., RIP of the *next* instruction) that the 32-bit relative offset is
|
|
||||||
/// calculated from.
|
|
||||||
offset: i64,
|
|
||||||
/// The 4-byte little-endian pattern of `used` and `free` bytes that constrain the `rel32` offset.
|
|
||||||
patch_bytes: [patch_size]PatchByte,
|
|
||||||
/// Internal state: the byte-level representation of the *start* of the current `rel32` offset being
|
|
||||||
/// iterated.
|
|
||||||
start: [patch_size]u8,
|
|
||||||
/// Internal state: the byte-level representation of the *end* of the current `rel32` offset being
|
|
||||||
/// iterated.
|
|
||||||
end: [patch_size]u8,
|
|
||||||
/// Internal state: flag to handle the first call to `next()` uniquely.
|
|
||||||
first: bool,
|
|
||||||
/// Internal state: optimization cache for the number of contiguous `.free` bytes at the *end* of
|
|
||||||
/// `patch_bytes`.
|
|
||||||
trailing_free_count: u8,
|
|
||||||
|
|
||||||
/// Initializes the iterator.
|
|
||||||
/// - `patch_bytes`: The 4-byte pattern of the `rel32` offset, in little-endian order.
|
|
||||||
/// The base address (e.g., RIP of the *next* instruction) that the 32-bit relative offset is
|
|
||||||
/// calculated from.
|
|
||||||
pub fn init(patch_bytes: [patch_size]PatchByte, addr: u64) PatchLocationIterator {
|
|
||||||
log.debug("hi", .{});
|
|
||||||
assert(patch_bytes.len == patch_size);
|
|
||||||
|
|
||||||
// Find the number of contiguous free bytes at the end of the pattern.
|
|
||||||
var trailing_free: u8 = 0;
|
|
||||||
for (0..patch_bytes.len) |i| {
|
|
||||||
if (patch_bytes[i] == .free) {
|
|
||||||
trailing_free += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = std.mem.zeroes([patch_size]u8);
|
|
||||||
var end = std.mem.zeroes([patch_size]u8);
|
|
||||||
for (patch_bytes, 0..) |byte, i| {
|
|
||||||
switch (byte) {
|
|
||||||
.free => {
|
|
||||||
start[i] = 0;
|
|
||||||
end[i] = if (i < trailing_free) 0xff else 0;
|
|
||||||
},
|
|
||||||
.used => |val| {
|
|
||||||
start[i] = val;
|
|
||||||
end[i] = val;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const out = PatchLocationIterator{
|
|
||||||
.offset = @intCast(addr),
|
|
||||||
.patch_bytes = patch_bytes,
|
|
||||||
.trailing_free_count = trailing_free,
|
|
||||||
.start = start,
|
|
||||||
.end = end,
|
|
||||||
.first = true,
|
|
||||||
};
|
|
||||||
log.debug("init: {f}", .{out});
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the next valid `Range` of target addresses, or `null` if the iteration is complete.
|
|
||||||
pub fn next(self: *PatchLocationIterator) ?Range {
|
|
||||||
// If all bytes are free we can just return the maximum range.
|
|
||||||
if (self.trailing_free_count == patch_size) {
|
|
||||||
defer self.first = false;
|
|
||||||
if (self.first) {
|
|
||||||
var range = Range{
|
|
||||||
.start = self.offset + std.math.minInt(i32),
|
|
||||||
.end = self.offset + std.math.maxInt(i32),
|
|
||||||
};
|
|
||||||
// Clamp to valid positive address space
|
|
||||||
if (range.start < 0) range.start = 0;
|
|
||||||
if (range.end <= 0) {
|
|
||||||
log.info("next: All bytes free, but range entirely negative.", .{});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("next: All bytes free, returning full range: {f}", .{range});
|
|
||||||
return range;
|
|
||||||
} else {
|
|
||||||
log.info("next: All bytes free, iteration finished.", .{});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
var range: Range = undefined;
|
|
||||||
|
|
||||||
if (self.first) {
|
|
||||||
self.first = false;
|
|
||||||
const start = std.mem.readInt(PatchInt, self.start[0..], .little);
|
|
||||||
const end = std.mem.readInt(PatchInt, self.end[0..], .little);
|
|
||||||
range = Range{
|
|
||||||
.start = start + self.offset,
|
|
||||||
.end = end + self.offset,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
var overflow: u1 = 1;
|
|
||||||
for (self.patch_bytes, 0..) |byte, i| {
|
|
||||||
if (i < self.trailing_free_count or byte == .used) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
assert(byte == .free);
|
|
||||||
assert(self.start[i] == self.end[i]);
|
|
||||||
defer assert(self.start[i] == self.end[i]);
|
|
||||||
|
|
||||||
if (overflow == 1) {
|
|
||||||
if (self.start[i] == std.math.maxInt(u8)) {
|
|
||||||
self.start[i] = 0;
|
|
||||||
self.end[i] = 0;
|
|
||||||
} else {
|
|
||||||
self.start[i] += 1;
|
|
||||||
self.end[i] += 1;
|
|
||||||
overflow = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (overflow == 1) {
|
|
||||||
log.info("next: Iteration finished, no more ranges.", .{});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = std.mem.readInt(PatchInt, self.start[0..], .little);
|
|
||||||
const end = std.mem.readInt(PatchInt, self.end[0..], .little);
|
|
||||||
assert(end >= start);
|
|
||||||
range = Range{
|
|
||||||
.start = start + self.offset,
|
|
||||||
.end = end + self.offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out ranges that are entirely negative (invalid memory addresses).
|
|
||||||
if (range.end <= 0) continue;
|
|
||||||
// Clamp ranges that start negative but end positive.
|
|
||||||
if (range.start < 0) range.start = 0;
|
|
||||||
|
|
||||||
log.debug("next: new range: {f}", .{range});
|
|
||||||
return range;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format(self: PatchLocationIterator, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
|
||||||
try writer.print(".{{ ", .{});
|
|
||||||
try writer.print(".offset = {x}, ", .{self.offset});
|
|
||||||
try writer.print(
|
|
||||||
".patch_bytes = .{{ {f}, {f}, {f}, {f} }}, ",
|
|
||||||
.{ self.patch_bytes[0], self.patch_bytes[1], self.patch_bytes[2], self.patch_bytes[3] },
|
|
||||||
);
|
|
||||||
try writer.print(
|
|
||||||
".start: 0x{x}, .end: 0x{x}, first: {}, trailing_free_count: {}",
|
|
||||||
.{ self.start, self.end, self.first, self.trailing_free_count },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "free bytes" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
};
|
|
||||||
var it = PatchLocationIterator.init(pattern, 0);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0, .end = std.math.maxInt(i32) },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(null, it.next());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "predetermined negative" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0xe9 },
|
|
||||||
};
|
|
||||||
var it = PatchLocationIterator.init(pattern, 0);
|
|
||||||
try testing.expectEqual(null, it.next());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "trailing free bytes" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0x79 },
|
|
||||||
};
|
|
||||||
var it = PatchLocationIterator.init(pattern, 0);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x79000000, .end = 0x79ffffff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(null, it.next());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "inner and trailing free bytes" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0xe8 },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0x79 },
|
|
||||||
};
|
|
||||||
var it = PatchLocationIterator.init(pattern, 0);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x7900e800, .end = 0x7900e8ff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x7901e800, .end = 0x7901e8ff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip to the last range
|
|
||||||
var r_last: ?Range = null;
|
|
||||||
var count: u32 = 2; // We already consumed two
|
|
||||||
while (it.next()) |r| {
|
|
||||||
r_last = r;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x79ffe800, .end = 0x79ffe8ff },
|
|
||||||
r_last,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(256, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "no free bytes" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .used = 0xe9 },
|
|
||||||
.{ .used = 0x00 },
|
|
||||||
.{ .used = 0x00 },
|
|
||||||
.{ .used = 0x78 },
|
|
||||||
};
|
|
||||||
var it = PatchLocationIterator.init(pattern, 0);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x780000e9, .end = 0x780000e9 },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(null, it.next());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "inner and leading free bytes" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .used = 0xe9 },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0xe8 },
|
|
||||||
.{ .free = {} },
|
|
||||||
};
|
|
||||||
var it = PatchLocationIterator.init(pattern, 0);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x00e800e9, .end = 0x00e800e9 },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x00e801e9, .end = 0x00e801e9 },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip to the last range
|
|
||||||
var r_last: ?Range = null;
|
|
||||||
var count: u32 = 2; // We already consumed two
|
|
||||||
while (it.next()) |r| {
|
|
||||||
r_last = r;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x7fe8ffe9, .end = 0x7fe8ffe9 },
|
|
||||||
r_last,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(256 * 128, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "only inner" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .used = 0xe9 },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0x78 },
|
|
||||||
};
|
|
||||||
var it = PatchLocationIterator.init(pattern, 0);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x780000e9, .end = 0x780000e9 },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x780001e9, .end = 0x780001e9 },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip to the last range
|
|
||||||
var r_last: ?Range = null;
|
|
||||||
var count: u32 = 2; // We already consumed two
|
|
||||||
while (it.next()) |r| {
|
|
||||||
r_last = r;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = 0x78ffffe9, .end = 0x78ffffe9 },
|
|
||||||
r_last,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(256 * 256, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "trailing free bytes offset" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0x79 },
|
|
||||||
};
|
|
||||||
const offset = 0x12345678;
|
|
||||||
var it = PatchLocationIterator.init(pattern, offset);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = offset + 0x79000000, .end = offset + 0x79ffffff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(null, it.next());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "trailing and leading offset" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0xe9 },
|
|
||||||
.{ .used = 0xe8 },
|
|
||||||
.{ .free = {} },
|
|
||||||
};
|
|
||||||
const offset = 0x12345678;
|
|
||||||
var it = PatchLocationIterator.init(pattern, offset);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = offset + 0x00e8e900, .end = offset + 0x00e8e9ff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = offset + 0x01e8e900, .end = offset + 0x01e8e9ff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip to the last range
|
|
||||||
var r_last: ?Range = null;
|
|
||||||
var count: u32 = 2; // We already consumed two
|
|
||||||
while (it.next()) |r| {
|
|
||||||
r_last = r;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{
|
|
||||||
.start = offset + @as(i32, @bitCast(@as(u32, 0xffe8e900))),
|
|
||||||
.end = offset + @as(i32, @bitCast(@as(u32, 0xffe8e9ff))),
|
|
||||||
},
|
|
||||||
r_last,
|
|
||||||
);
|
|
||||||
try testing.expect(count > 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "trailing free bytes large offset" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0x79 },
|
|
||||||
};
|
|
||||||
const offset = 0x12345678;
|
|
||||||
var it = PatchLocationIterator.init(pattern, offset);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = offset + 0x79000000, .end = offset + 0x79ffffff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(null, it.next());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "trailing and leading large offset" {
|
|
||||||
const pattern = [_]PatchByte{
|
|
||||||
.{ .free = {} },
|
|
||||||
.{ .used = 0xe9 },
|
|
||||||
.{ .used = 0xe8 },
|
|
||||||
.{ .free = {} },
|
|
||||||
};
|
|
||||||
const offset = 0x123456789a;
|
|
||||||
var it = PatchLocationIterator.init(pattern, offset);
|
|
||||||
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = offset + 0x00e8e900, .end = offset + 0x00e8e9ff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{ .start = offset + 0x01e8e900, .end = offset + 0x01e8e9ff },
|
|
||||||
it.next().?,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip to the last range
|
|
||||||
var r_last: ?Range = null;
|
|
||||||
var count: u32 = 2; // We already consumed two
|
|
||||||
while (it.next()) |r| {
|
|
||||||
r_last = r;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
try testing.expectEqual(
|
|
||||||
Range{
|
|
||||||
.start = offset + @as(i64, @intCast(@as(i32, @bitCast(@as(u32, 0xffe8e900))))),
|
|
||||||
.end = offset + @as(i64, @intCast(@as(i32, @bitCast(@as(u32, 0xffe8e9ff))))),
|
|
||||||
},
|
|
||||||
r_last,
|
|
||||||
);
|
|
||||||
try testing.expectEqual(256, count);
|
|
||||||
}
|
|
||||||
1695
src/Patcher.zig
1695
src/Patcher.zig
File diff suppressed because it is too large
Load Diff
@@ -17,16 +17,6 @@ pub fn size(range: Range) u64 {
|
|||||||
return @intCast(range.end - range.start);
|
return @intCast(range.end - range.start);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alignTo(range: Range, alignment: u64) Range {
|
|
||||||
assert(range.end >= range.start);
|
|
||||||
assert(std.math.isPowerOfTwo(alignment));
|
|
||||||
assert(alignment <= std.math.maxInt(i64));
|
|
||||||
const lower = std.mem.alignBackward(i64, range.start, @intCast(alignment));
|
|
||||||
const upper = std.mem.alignForward(i64, range.end, @intCast(alignment));
|
|
||||||
assert(upper >= lower);
|
|
||||||
return .{ .start = lower, .end = upper };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn overlaps(range: Range, other: Range) bool {
|
pub fn overlaps(range: Range, other: Range) bool {
|
||||||
assert(range.end >= range.start);
|
assert(range.end >= range.start);
|
||||||
assert(other.end >= other.start);
|
assert(other.end >= other.start);
|
||||||
@@ -52,18 +42,17 @@ pub fn touches(range: Range, other: Range) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Ranges are considered equal if they touch.
|
/// Ranges are considered equal if they touch.
|
||||||
pub fn compare(lhs: Range, rhs: Range) std.math.Order {
|
pub fn compareTouching(lhs: Range, rhs: Range) std.math.Order {
|
||||||
assert(lhs.end >= lhs.start);
|
assert(lhs.end >= lhs.start);
|
||||||
assert(rhs.end >= rhs.start);
|
assert(rhs.end >= rhs.start);
|
||||||
return if (lhs.start > rhs.end) .gt else if (lhs.end < rhs.start) .lt else .eq;
|
return if (lhs.start > rhs.end) .gt else if (lhs.end < rhs.start) .lt else .eq;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getStart(range: Range, T: type) T {
|
/// Ranges are considered equal if they overlap.
|
||||||
return @intCast(range.start);
|
pub fn compareOverlapping(lhs: Range, rhs: Range) std.math.Order {
|
||||||
}
|
assert(lhs.end >= lhs.start);
|
||||||
|
assert(rhs.end >= rhs.start);
|
||||||
pub fn getEnd(range: Range, T: type) T {
|
return if (lhs.start >= rhs.end) .gt else if (lhs.end <= rhs.start) .lt else .eq;
|
||||||
return @intCast(range.end);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(
|
pub fn format(
|
||||||
@@ -73,25 +62,23 @@ pub fn format(
|
|||||||
try writer.print(".{{ .start = 0x{x}, .end = 0x{x} }}", .{ self.start, self.end });
|
try writer.print(".{{ .start = 0x{x}, .end = 0x{x} }}", .{ self.start, self.end });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fromSlice(T: type, slice: []T) Range {
|
||||||
|
const start = @intFromPtr(slice.ptr);
|
||||||
|
return .{
|
||||||
|
.start = @intCast(start),
|
||||||
|
.end = @intCast(start + slice.len * @sizeOf(T)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fromPtr(ptr: [*]u8, len: usize) Range {
|
||||||
|
return .fromSlice(u8, ptr[0..len]);
|
||||||
|
}
|
||||||
|
|
||||||
test "AddressRange size" {
|
test "AddressRange size" {
|
||||||
const range = Range{ .start = 100, .end = 250 };
|
const range = Range{ .start = 100, .end = 250 };
|
||||||
try std.testing.expectEqual(@as(u64, 150), range.size());
|
try std.testing.expectEqual(@as(u64, 150), range.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "AddressRange alignTo unaligned" {
|
|
||||||
const range = Range{ .start = 101, .end = 199 };
|
|
||||||
const aligned = range.alignTo(16);
|
|
||||||
try std.testing.expectEqual(@as(i64, 96), aligned.start);
|
|
||||||
try std.testing.expectEqual(@as(i64, 208), aligned.end);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "AddressRange alignTo already aligned" {
|
|
||||||
const range = Range{ .start = 64, .end = 128 };
|
|
||||||
const aligned = range.alignTo(64);
|
|
||||||
try std.testing.expectEqual(@as(i64, 64), aligned.start);
|
|
||||||
try std.testing.expectEqual(@as(i64, 128), aligned.end);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "AddressRange no overlap before" {
|
test "AddressRange no overlap before" {
|
||||||
const base = Range{ .start = 100, .end = 200 };
|
const base = Range{ .start = 100, .end = 200 };
|
||||||
const other = Range{ .start = 0, .end = 100 };
|
const other = Range{ .start = 0, .end = 100 };
|
||||||
|
|||||||
46
src/Statistics.zig
Normal file
46
src/Statistics.zig
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
|
||||||
|
const Statistics = @This();
|
||||||
|
|
||||||
|
/// Direct jumps
|
||||||
|
jump: u64,
|
||||||
|
/// Punning - index represents number of prefixes used
|
||||||
|
punning: [4]u64,
|
||||||
|
/// Successor Eviction
|
||||||
|
successor_eviction: u64,
|
||||||
|
/// Neighbor Eviction
|
||||||
|
neighbor_eviction: u64,
|
||||||
|
/// Failed to patch
|
||||||
|
failed: u64,
|
||||||
|
|
||||||
|
pub const empty = mem.zeroes(Statistics);
|
||||||
|
|
||||||
|
pub fn punningSum(stats: *const Statistics) u64 {
|
||||||
|
return stats.punning[0] + stats.punning[1] + stats.punning[2] + stats.punning[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn successful(stats: *const Statistics) u64 {
|
||||||
|
return stats.jump + stats.punningSum() + stats.successor_eviction + stats.neighbor_eviction;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total(stats: *const Statistics) u64 {
|
||||||
|
return stats.successful() + stats.failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn percentage(stats: *const Statistics) f64 {
|
||||||
|
if (stats.total() == 0) return 1;
|
||||||
|
const s: f64 = @floatFromInt(stats.successful());
|
||||||
|
const t: f64 = @floatFromInt(stats.total());
|
||||||
|
return s / t;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(self: *Statistics, other: *const Statistics) void {
|
||||||
|
self.jump += other.jump;
|
||||||
|
for (0..self.punning.len) |i| {
|
||||||
|
self.punning[i] += other.punning[i];
|
||||||
|
}
|
||||||
|
self.successor_eviction += other.successor_eviction;
|
||||||
|
self.neighbor_eviction += other.neighbor_eviction;
|
||||||
|
self.failed += other.failed;
|
||||||
|
}
|
||||||
49
src/backend.zig
Normal file
49
src/backend.zig
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const p = std.posix;
|
||||||
|
|
||||||
|
const page_size_min = std.heap.page_size_min;
|
||||||
|
|
||||||
|
pub const backend = switch (@import("builtin").is_test) {
|
||||||
|
true => testing,
|
||||||
|
false => posix,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Maybe log?
|
||||||
|
pub const testing = struct {
|
||||||
|
pub fn mmap(
|
||||||
|
ptr: [*]align(page_size_min) u8,
|
||||||
|
length: usize,
|
||||||
|
prot: u32,
|
||||||
|
flags: p.MAP,
|
||||||
|
fd: p.fd_t,
|
||||||
|
offset: u64,
|
||||||
|
) p.MMapError![]align(page_size_min) u8 {
|
||||||
|
_ = .{ ptr, length, prot, flags, fd, offset };
|
||||||
|
return ptr[0..length];
|
||||||
|
}
|
||||||
|
pub fn mprotect(memory: []align(page_size_min) u8, protection: u32) p.MProtectError!void {
|
||||||
|
_ = .{ memory, protection };
|
||||||
|
}
|
||||||
|
pub fn munmap(memory: []align(page_size_min) const u8) void {
|
||||||
|
_ = memory;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const posix = struct {
|
||||||
|
pub fn mmap(
|
||||||
|
ptr: ?[*]align(page_size_min) u8,
|
||||||
|
length: usize,
|
||||||
|
prot: u32,
|
||||||
|
flags: p.MAP,
|
||||||
|
fd: p.fd_t,
|
||||||
|
offset: u64,
|
||||||
|
) p.MMapError![]align(page_size_min) u8 {
|
||||||
|
return p.mmap(ptr, length, prot, flags, fd, offset);
|
||||||
|
}
|
||||||
|
pub fn mprotect(memory: []align(page_size_min) u8, protection: u32) p.MProtectError!void {
|
||||||
|
return p.mprotect(memory, protection);
|
||||||
|
}
|
||||||
|
pub fn munmap(memory: []align(page_size_min) const u8) void {
|
||||||
|
p.munmap(memory);
|
||||||
|
}
|
||||||
|
};
|
||||||
94
src/loader.zig
Normal file
94
src/loader.zig
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const elf = std.elf;
|
||||||
|
const mem = std.mem;
|
||||||
|
const posix = std.posix;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.loader);
|
||||||
|
const page_size = std.heap.pageSize();
|
||||||
|
|
||||||
|
pub const UnfinishedReadError = error{UnfinishedRead};
|
||||||
|
|
||||||
|
pub const LoadResult = struct {
|
||||||
|
base: usize,
|
||||||
|
size: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Loads all `PT_LOAD` segments of an ELF file into memory.
|
||||||
|
///
|
||||||
|
/// For `ET_EXEC` (non-PIE), segments are mapped at their fixed virtual addresses (`p_vaddr`).
|
||||||
|
/// For `ET_DYN` (PIE), segments are mapped at a random base address chosen by the kernel.
|
||||||
|
///
|
||||||
|
/// It handles zero-initialized(e.g., .bss) sections by mapping anonymous memory and only reading
|
||||||
|
/// `p_filesz` bytes from the file, ensuring `p_memsz` bytes are allocated.
|
||||||
|
pub fn loadStaticElf(ehdr: elf.Header, file_reader: *std.fs.File.Reader) !LoadResult {
|
||||||
|
// NOTE: In theory we could also just look at the first and last loadable segment because the
|
||||||
|
// ELF spec mandates these to be in ascending order of `p_vaddr`, but better be safe than sorry.
|
||||||
|
// https://gabi.xinuos.com/elf/08-pheader.html#:~:text=ascending%20order
|
||||||
|
const minva, const maxva = bounds: {
|
||||||
|
var minva: u64 = std.math.maxInt(u64);
|
||||||
|
var maxva: u64 = 0;
|
||||||
|
var phdrs = ehdr.iterateProgramHeaders(file_reader);
|
||||||
|
while (try phdrs.next()) |phdr| {
|
||||||
|
if (phdr.p_type != elf.PT_LOAD) continue;
|
||||||
|
minva = @min(minva, phdr.p_vaddr);
|
||||||
|
maxva = @max(maxva, phdr.p_vaddr + phdr.p_memsz);
|
||||||
|
}
|
||||||
|
minva = mem.alignBackward(usize, minva, page_size);
|
||||||
|
maxva = mem.alignForward(usize, maxva, page_size);
|
||||||
|
log.debug("Calculated bounds: minva=0x{x}, maxva=0x{x}", .{ minva, maxva });
|
||||||
|
break :bounds .{ minva, maxva };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check, that the needed memory region can be allocated as a whole. We do this
|
||||||
|
const dynamic = ehdr.type == elf.ET.DYN;
|
||||||
|
log.debug("ELF type is {s}", .{if (dynamic) "DYN" else "EXEC (static)"});
|
||||||
|
const hint = if (dynamic) null else @as(?[*]align(page_size) u8, @ptrFromInt(minva));
|
||||||
|
log.debug("mmap pre-flight hint: {*}", .{hint});
|
||||||
|
const base = try posix.mmap(
|
||||||
|
hint,
|
||||||
|
maxva - minva,
|
||||||
|
posix.PROT.WRITE,
|
||||||
|
.{ .TYPE = .PRIVATE, .ANONYMOUS = true, .FIXED_NOREPLACE = !dynamic },
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
log.debug("Pre-flight reservation at: {*}, size: 0x{x}", .{ base.ptr, base.len });
|
||||||
|
|
||||||
|
var phdrs = ehdr.iterateProgramHeaders(file_reader);
|
||||||
|
var phdr_idx: u32 = 0;
|
||||||
|
errdefer posix.munmap(base);
|
||||||
|
while (try phdrs.next()) |phdr| : (phdr_idx += 1) {
|
||||||
|
if (phdr.p_type != elf.PT_LOAD) continue;
|
||||||
|
if (phdr.p_memsz == 0) continue;
|
||||||
|
|
||||||
|
const offset = phdr.p_vaddr & (page_size - 1);
|
||||||
|
const size = mem.alignForward(usize, phdr.p_memsz + offset, page_size);
|
||||||
|
var start = mem.alignBackward(usize, phdr.p_vaddr, page_size);
|
||||||
|
const base_for_dyn = if (dynamic) @intFromPtr(base.ptr) else 0;
|
||||||
|
start += base_for_dyn;
|
||||||
|
log.debug(
|
||||||
|
" - phdr[{}]: mapping 0x{x} - 0x{x} (vaddr=0x{x}, dyn_base=0x{x})",
|
||||||
|
.{ phdr_idx, start, start + size, phdr.p_vaddr, base_for_dyn },
|
||||||
|
);
|
||||||
|
const ptr: []align(page_size) u8 = @as([*]align(page_size) u8, @ptrFromInt(start))[0..size];
|
||||||
|
// TODO: we should likely just use mmap instead because then not touched memory isn't loaded
|
||||||
|
// unnecessarily
|
||||||
|
try file_reader.seekTo(phdr.p_offset);
|
||||||
|
if (try file_reader.read(ptr[offset..][0..phdr.p_filesz]) != phdr.p_filesz)
|
||||||
|
return UnfinishedReadError.UnfinishedRead;
|
||||||
|
|
||||||
|
const protections = elfToMmapProt(phdr.p_flags);
|
||||||
|
try posix.mprotect(ptr, protections);
|
||||||
|
}
|
||||||
|
log.debug("loadElf returning base: 0x{x}, size: 0x{x}", .{ @intFromPtr(base.ptr), base.len });
|
||||||
|
return .{ .base = @intFromPtr(base.ptr), .size = base.len };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts ELF program header protection flags to mmap protection flags.
|
||||||
|
pub fn elfToMmapProt(elf_prot: u64) u32 {
|
||||||
|
var result: u32 = posix.PROT.NONE;
|
||||||
|
if ((elf_prot & elf.PF_R) != 0) result |= posix.PROT.READ;
|
||||||
|
if ((elf_prot & elf.PF_W) != 0) result |= posix.PROT.WRITE;
|
||||||
|
if ((elf_prot & elf.PF_X) != 0) result |= posix.PROT.EXEC;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
180
src/main.zig
180
src/main.zig
@@ -8,6 +8,7 @@ const testing = std.testing;
|
|||||||
|
|
||||||
const log = std.log.scoped(.flicker);
|
const log = std.log.scoped(.flicker);
|
||||||
const Patcher = @import("Patcher.zig");
|
const Patcher = @import("Patcher.zig");
|
||||||
|
const loader = @import("loader.zig");
|
||||||
|
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
@@ -16,8 +17,8 @@ pub const std_options: std.Options = .{
|
|||||||
.log_scope_levels = &.{
|
.log_scope_levels = &.{
|
||||||
.{ .scope = .disassembler, .level = .info },
|
.{ .scope = .disassembler, .level = .info },
|
||||||
.{ .scope = .patcher, .level = .debug },
|
.{ .scope = .patcher, .level = .debug },
|
||||||
.{ .scope = .patch_location_iterator, .level = .warn },
|
|
||||||
.{ .scope = .flicker, .level = .info },
|
.{ .scope = .flicker, .level = .info },
|
||||||
|
.{ .scope = .loader, .level = .info },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const page_size = std.heap.pageSize();
|
const page_size = std.heap.pageSize();
|
||||||
@@ -32,6 +33,12 @@ const help =
|
|||||||
|
|
||||||
const UnfinishedReadError = error{UnfinishedRead};
|
const UnfinishedReadError = error{UnfinishedRead};
|
||||||
|
|
||||||
|
/// This needs to be a public global, such that it has a static memory location. This is needed
|
||||||
|
/// for the syscall interception, in particular for patching new maps of the `mmap` call.
|
||||||
|
pub var patcher: Patcher = undefined;
|
||||||
|
pub var target_exec_path_buf: [std.fs.max_path_bytes]u8 = @splat(0);
|
||||||
|
pub var target_exec_path: []const u8 = undefined;
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
var arg_index: u64 = 1; // Skip own name
|
var arg_index: u64 = 1; // Skip own name
|
||||||
@@ -51,27 +58,29 @@ pub fn main() !void {
|
|||||||
|
|
||||||
const file = try lookupFile(mem.sliceTo(std.os.argv[arg_index], 0));
|
const file = try lookupFile(mem.sliceTo(std.os.argv[arg_index], 0));
|
||||||
|
|
||||||
{
|
patcher = try .init(std.heap.page_allocator);
|
||||||
// Initialize patcher
|
|
||||||
try Patcher.init();
|
// Resolve the absolute path of the target executable for /proc/self/exe spoofing
|
||||||
// Resolve the absolute path of the target executable. This is needed for the
|
const fd_path = try std.fmt.bufPrint(&target_exec_path_buf, "/proc/self/fd/{d}", .{file.handle});
|
||||||
// readlink("/proc/self/exe") interception. We use the file descriptor to get the
|
target_exec_path = try std.fs.readLinkAbsolute(fd_path, &target_exec_path_buf);
|
||||||
// authoritative path.
|
log.debug("Resolved target executable path: {s}", .{target_exec_path});
|
||||||
var self_buf: [128]u8 = undefined;
|
|
||||||
const fd_path = try std.fmt.bufPrint(&self_buf, "/proc/self/fd/{d}", .{file.handle});
|
try bootstrapMemoryMap(&patcher);
|
||||||
Patcher.target_exec_path = try std.fs.readLinkAbsolute(fd_path, &Patcher.target_exec_path_buf);
|
// TODO:
|
||||||
log.debug("Resolved target executable path: {s}", .{Patcher.target_exec_path});
|
// block until `mmap_min_addr`
|
||||||
}
|
// block all entries in `proc/self/maps`
|
||||||
|
|
||||||
// Map file into memory
|
// Map file into memory
|
||||||
var file_buffer: [128]u8 = undefined;
|
var file_buffer: [128]u8 = undefined;
|
||||||
var file_reader = file.reader(&file_buffer);
|
var file_reader = file.reader(&file_buffer);
|
||||||
log.info("--- Loading executable: {s} ---", .{std.os.argv[arg_index]});
|
log.info("--- Loading executable: {s} ---", .{std.os.argv[arg_index]});
|
||||||
const ehdr = try elf.Header.read(&file_reader.interface);
|
const ehdr = try elf.Header.read(&file_reader.interface);
|
||||||
const base = try loadStaticElf(ehdr, &file_reader);
|
const load_result = try loader.loadStaticElf(ehdr, &file_reader);
|
||||||
|
const base = load_result.base;
|
||||||
const entry = ehdr.entry + if (ehdr.type == .DYN) base else 0;
|
const entry = ehdr.entry + if (ehdr.type == .DYN) base else 0;
|
||||||
log.info("Executable loaded: base=0x{x}, entry=0x{x}", .{ base, entry });
|
log.info("Executable loaded: base=0x{x}, entry=0x{x}", .{ base, entry });
|
||||||
try patchLoadedElf(base);
|
try patcher.address_allocator.block(.fromPtr(@ptrFromInt(base), load_result.size));
|
||||||
|
try patchLoadedElf(load_result.base);
|
||||||
|
|
||||||
// Check for dynamic linker
|
// Check for dynamic linker
|
||||||
var maybe_interp_base: ?usize = null;
|
var maybe_interp_base: ?usize = null;
|
||||||
@@ -96,13 +105,15 @@ pub fn main() !void {
|
|||||||
var interp_reader = interp.reader(&interp_buffer);
|
var interp_reader = interp.reader(&interp_buffer);
|
||||||
const interp_ehdr = try elf.Header.read(&interp_reader.interface);
|
const interp_ehdr = try elf.Header.read(&interp_reader.interface);
|
||||||
assert(interp_ehdr.type == elf.ET.DYN);
|
assert(interp_ehdr.type == elf.ET.DYN);
|
||||||
const interp_base = try loadStaticElf(interp_ehdr, &interp_reader);
|
const interp_result = try loader.loadStaticElf(interp_ehdr, &interp_reader);
|
||||||
|
const interp_base = interp_result.base;
|
||||||
maybe_interp_base = interp_base;
|
maybe_interp_base = interp_base;
|
||||||
maybe_interp_entry = interp_ehdr.entry + if (interp_ehdr.type == .DYN) interp_base else 0;
|
maybe_interp_entry = interp_ehdr.entry + if (interp_ehdr.type == .DYN) interp_base else 0;
|
||||||
log.info(
|
log.info(
|
||||||
"Interpreter loaded: base=0x{x}, entry=0x{x}",
|
"Interpreter loaded: base=0x{x}, entry=0x{x}",
|
||||||
.{ interp_base, maybe_interp_entry.? },
|
.{ interp_base, maybe_interp_entry.? },
|
||||||
);
|
);
|
||||||
|
try patcher.address_allocator.block(.fromPtr(@ptrFromInt(interp_base), interp_result.size));
|
||||||
try patchLoadedElf(interp_base);
|
try patchLoadedElf(interp_base);
|
||||||
interp.close();
|
interp.close();
|
||||||
}
|
}
|
||||||
@@ -118,9 +129,12 @@ pub fn main() !void {
|
|||||||
elf.AT_ENTRY => entry,
|
elf.AT_ENTRY => entry,
|
||||||
elf.AT_EXECFN => @intFromPtr(std.os.argv[arg_index]),
|
elf.AT_EXECFN => @intFromPtr(std.os.argv[arg_index]),
|
||||||
elf.AT_SYSINFO_EHDR => blk: {
|
elf.AT_SYSINFO_EHDR => blk: {
|
||||||
log.info("Found vDSO at 0x{x}", .{auxv[i].a_un.a_val});
|
const vdso_base = auxv[i].a_un.a_val;
|
||||||
try patchLoadedElf(auxv[i].a_un.a_val);
|
log.info("Found vDSO at 0x{x}", .{vdso_base});
|
||||||
break :blk auxv[i].a_un.a_val;
|
try patchLoadedElf(vdso_base);
|
||||||
|
break :blk vdso_base;
|
||||||
|
// NOTE: We do not need to block this, because it's already done by the initial
|
||||||
|
// `/proc/self/maps` pass.
|
||||||
},
|
},
|
||||||
elf.AT_EXECFD => {
|
elf.AT_EXECFD => {
|
||||||
@panic("Got AT_EXECFD auxv value");
|
@panic("Got AT_EXECFD auxv value");
|
||||||
@@ -163,77 +177,6 @@ pub fn main() !void {
|
|||||||
trampoline(final_entry, argc);
|
trampoline(final_entry, argc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads all `PT_LOAD` segments of an ELF file into memory.
|
|
||||||
///
|
|
||||||
/// For `ET_EXEC` (non-PIE), segments are mapped at their fixed virtual addresses (`p_vaddr`).
|
|
||||||
/// For `ET_DYN` (PIE), segments are mapped at a random base address chosen by the kernel.
|
|
||||||
///
|
|
||||||
/// It handles zero-initialized(e.g., .bss) sections by mapping anonymous memory and only reading
|
|
||||||
/// `p_filesz` bytes from the file, ensuring `p_memsz` bytes are allocated.
|
|
||||||
fn loadStaticElf(ehdr: elf.Header, file_reader: *std.fs.File.Reader) !usize {
|
|
||||||
// NOTE: In theory we could also just look at the first and last loadable segment because the
|
|
||||||
// ELF spec mandates these to be in ascending order of `p_vaddr`, but better be safe than sorry.
|
|
||||||
// https://gabi.xinuos.com/elf/08-pheader.html#:~:text=ascending%20order
|
|
||||||
const minva, const maxva = bounds: {
|
|
||||||
var minva: u64 = std.math.maxInt(u64);
|
|
||||||
var maxva: u64 = 0;
|
|
||||||
var phdrs = ehdr.iterateProgramHeaders(file_reader);
|
|
||||||
while (try phdrs.next()) |phdr| {
|
|
||||||
if (phdr.p_type != elf.PT_LOAD) continue;
|
|
||||||
minva = @min(minva, phdr.p_vaddr);
|
|
||||||
maxva = @max(maxva, phdr.p_vaddr + phdr.p_memsz);
|
|
||||||
}
|
|
||||||
minva = mem.alignBackward(usize, minva, page_size);
|
|
||||||
maxva = mem.alignForward(usize, maxva, page_size);
|
|
||||||
log.debug("Calculated bounds: minva=0x{x}, maxva=0x{x}", .{ minva, maxva });
|
|
||||||
break :bounds .{ minva, maxva };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check, that the needed memory region can be allocated as a whole. We do this
|
|
||||||
const dynamic = ehdr.type == elf.ET.DYN;
|
|
||||||
log.debug("ELF type is {s}", .{if (dynamic) "DYN" else "EXEC (static)"});
|
|
||||||
const hint = if (dynamic) null else @as(?[*]align(page_size) u8, @ptrFromInt(minva));
|
|
||||||
log.debug("mmap pre-flight hint: {*}", .{hint});
|
|
||||||
const base = try posix.mmap(
|
|
||||||
hint,
|
|
||||||
maxva - minva,
|
|
||||||
posix.PROT.WRITE,
|
|
||||||
.{ .TYPE = .PRIVATE, .ANONYMOUS = true, .FIXED_NOREPLACE = !dynamic },
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
log.debug("Pre-flight reservation at: {*}, size: 0x{x}", .{ base.ptr, base.len });
|
|
||||||
|
|
||||||
var phdrs = ehdr.iterateProgramHeaders(file_reader);
|
|
||||||
var phdr_idx: u32 = 0;
|
|
||||||
errdefer posix.munmap(base);
|
|
||||||
while (try phdrs.next()) |phdr| : (phdr_idx += 1) {
|
|
||||||
if (phdr.p_type != elf.PT_LOAD) continue;
|
|
||||||
if (phdr.p_memsz == 0) continue;
|
|
||||||
|
|
||||||
const offset = phdr.p_vaddr & (page_size - 1);
|
|
||||||
const size = mem.alignForward(usize, phdr.p_memsz + offset, page_size);
|
|
||||||
var start = mem.alignBackward(usize, phdr.p_vaddr, page_size);
|
|
||||||
const base_for_dyn = if (dynamic) @intFromPtr(base.ptr) else 0;
|
|
||||||
start += base_for_dyn;
|
|
||||||
log.debug(
|
|
||||||
" - phdr[{}]: mapping 0x{x} - 0x{x} (vaddr=0x{x}, dyn_base=0x{x})",
|
|
||||||
.{ phdr_idx, start, start + size, phdr.p_vaddr, base_for_dyn },
|
|
||||||
);
|
|
||||||
const ptr: []align(page_size) u8 = @as([*]align(page_size) u8, @ptrFromInt(start))[0..size];
|
|
||||||
// TODO: we should likely just use mmap instead because then not touched memory isn't loaded
|
|
||||||
// unnecessarily
|
|
||||||
try file_reader.seekTo(phdr.p_offset);
|
|
||||||
if (try file_reader.read(ptr[offset..][0..phdr.p_filesz]) != phdr.p_filesz)
|
|
||||||
return UnfinishedReadError.UnfinishedRead;
|
|
||||||
|
|
||||||
const protections = elfToMmapProt(phdr.p_flags);
|
|
||||||
try posix.mprotect(ptr, protections);
|
|
||||||
}
|
|
||||||
log.debug("loadElf returning base: 0x{x}", .{@intFromPtr(base.ptr)});
|
|
||||||
return @intFromPtr(base.ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn patchLoadedElf(base: usize) !void {
|
fn patchLoadedElf(base: usize) !void {
|
||||||
const ehdr = @as(*const elf.Ehdr, @ptrFromInt(base));
|
const ehdr = @as(*const elf.Ehdr, @ptrFromInt(base));
|
||||||
if (!mem.eql(u8, ehdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;
|
if (!mem.eql(u8, ehdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;
|
||||||
@@ -263,20 +206,11 @@ fn patchLoadedElf(base: usize) !void {
|
|||||||
|
|
||||||
const region = @as([*]align(page_size) u8, @ptrFromInt(page_start))[0..size];
|
const region = @as([*]align(page_size) u8, @ptrFromInt(page_start))[0..size];
|
||||||
|
|
||||||
try Patcher.patchRegion(region);
|
try patcher.patchRegion(region);
|
||||||
try posix.mprotect(region, elfToMmapProt(phdr.p_flags));
|
try posix.mprotect(region, loader.elfToMmapProt(phdr.p_flags));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts ELF program header protection flags to mmap protection flags.
|
|
||||||
fn elfToMmapProt(elf_prot: u64) u32 {
|
|
||||||
var result: u32 = posix.PROT.NONE;
|
|
||||||
if ((elf_prot & elf.PF_R) != 0) result |= posix.PROT.READ;
|
|
||||||
if ((elf_prot & elf.PF_W) != 0) result |= posix.PROT.WRITE;
|
|
||||||
if ((elf_prot & elf.PF_X) != 0) result |= posix.PROT.EXEC;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens the file by either opening via a (absolute or relative) path or searching through `PATH`
|
/// Opens the file by either opening via a (absolute or relative) path or searching through `PATH`
|
||||||
/// for a file with the name.
|
/// for a file with the name.
|
||||||
// TODO: support paths starting with ~
|
// TODO: support paths starting with ~
|
||||||
@@ -317,10 +251,50 @@ fn trampoline(entry: usize, sp: [*]usize) noreturn {
|
|||||||
unreachable;
|
unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bootstrapMemoryMap(p: *Patcher) !void {
|
||||||
|
{
|
||||||
|
var min_addr: u64 = 0x10000;
|
||||||
|
if (std.fs.openFileAbsolute("/proc/sys/vm/mmap_min_addr", .{})) |file| {
|
||||||
|
defer file.close();
|
||||||
|
var buf: [32]u8 = undefined;
|
||||||
|
if (file.readAll(&buf)) |len| {
|
||||||
|
const trimmed = std.mem.trim(u8, buf[0..len], " \n\r\t");
|
||||||
|
if (std.fmt.parseInt(u64, trimmed, 10)) |val| {
|
||||||
|
min_addr = val;
|
||||||
|
} else |_| {}
|
||||||
|
} else |_| {}
|
||||||
|
} else |_| {}
|
||||||
|
try p.address_allocator.block(.{ .start = 0, .end = @intCast(min_addr) });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var maps_file = try std.fs.openFileAbsolute("/proc/self/maps", .{});
|
||||||
|
defer maps_file.close();
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
var reader = maps_file.reader(&buf);
|
||||||
|
while (true) {
|
||||||
|
const line = reader.interface.takeDelimiterInclusive('\n') catch |err| switch (err) {
|
||||||
|
error.EndOfStream => break,
|
||||||
|
error.ReadFailed => |e| return reader.err orelse e,
|
||||||
|
else => |e| return e,
|
||||||
|
};
|
||||||
|
std.debug.print("{s}", .{line});
|
||||||
|
const dash = mem.indexOfScalar(u8, line, '-') orelse continue;
|
||||||
|
const space = mem.indexOfScalar(u8, line, ' ') orelse continue;
|
||||||
|
assert(space > dash);
|
||||||
|
const start = std.fmt.parseInt(u64, line[0..dash], 16) catch unreachable;
|
||||||
|
const end = std.fmt.parseInt(u64, line[dash + 1 .. space], 16) catch unreachable;
|
||||||
|
// TODO: remove when Range is `u64`
|
||||||
|
try p.address_allocator.block(.{
|
||||||
|
.start = @as(u63, @truncate(start)),
|
||||||
|
.end = @as(u63, @truncate(end)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = @import("AddressAllocator.zig");
|
_ = @import("Patcher.zig");
|
||||||
_ = @import("Range.zig");
|
|
||||||
_ = @import("PatchLocationIterator.zig");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this be passed in from the build system
|
// TODO: make this be passed in from the build system
|
||||||
|
|||||||
98
src/relocation.zig
Normal file
98
src/relocation.zig
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
const dis = @import("disassembler.zig");
|
||||||
|
const std = @import("std");
|
||||||
|
const math = std.math;
|
||||||
|
const mem = std.mem;
|
||||||
|
const zydis = @import("zydis").zydis;
|
||||||
|
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
pub const RelocInfo = struct {
|
||||||
|
instr: dis.BundledInstruction,
|
||||||
|
old_addr: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Fixes RIP-relative operands in an instruction that has been moved to a new address.
|
||||||
|
pub fn relocateInstruction(
|
||||||
|
instruction: dis.BundledInstruction,
|
||||||
|
address: u64,
|
||||||
|
buffer: []u8,
|
||||||
|
) !void {
|
||||||
|
const instr = instruction.instruction;
|
||||||
|
// Iterate all operands
|
||||||
|
for (0..instr.operand_count) |i| {
|
||||||
|
const operand = &instruction.operands[i];
|
||||||
|
|
||||||
|
// Check for RIP-relative memory operand
|
||||||
|
const is_rip_rel = operand.type == zydis.ZYDIS_OPERAND_TYPE_MEMORY and
|
||||||
|
operand.unnamed_0.mem.base == zydis.ZYDIS_REGISTER_RIP;
|
||||||
|
// Check for relative immediate (e.g. JMP rel32)
|
||||||
|
const is_rel_imm = operand.type == zydis.ZYDIS_OPERAND_TYPE_IMMEDIATE and
|
||||||
|
operand.unnamed_0.imm.is_relative == zydis.ZYAN_TRUE;
|
||||||
|
if (!is_rip_rel and !is_rel_imm) continue;
|
||||||
|
|
||||||
|
// We have to apply a relocation
|
||||||
|
var result_address: u64 = 0;
|
||||||
|
const status = zydis.ZydisCalcAbsoluteAddress(
|
||||||
|
instr,
|
||||||
|
operand,
|
||||||
|
instruction.address,
|
||||||
|
&result_address,
|
||||||
|
);
|
||||||
|
assert(zydis.ZYAN_SUCCESS(status)); // TODO: maybe return an error instead
|
||||||
|
|
||||||
|
// Calculate new displacement relative to the new address
|
||||||
|
// The instruction length remains the same.
|
||||||
|
const next_rip: i64 = @intCast(address + instr.length);
|
||||||
|
const new_disp = @as(i64, @intCast(result_address)) - next_rip;
|
||||||
|
|
||||||
|
var offset: u16 = 0;
|
||||||
|
var size_bits: u8 = 0;
|
||||||
|
|
||||||
|
if (is_rip_rel) {
|
||||||
|
offset = instr.raw.disp.offset;
|
||||||
|
size_bits = instr.raw.disp.size;
|
||||||
|
} else {
|
||||||
|
assert(is_rel_imm);
|
||||||
|
// For relative immediate, find the matching raw immediate.
|
||||||
|
var found = false;
|
||||||
|
for (&instr.raw.imm) |*imm| {
|
||||||
|
if (imm.is_relative == zydis.ZYAN_TRUE) {
|
||||||
|
offset = imm.offset;
|
||||||
|
size_bits = imm.size;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(offset != 0);
|
||||||
|
assert(size_bits != 0);
|
||||||
|
const size_bytes = size_bits / 8;
|
||||||
|
|
||||||
|
if (offset + size_bytes > buffer.len) {
|
||||||
|
return error.RelocationFail;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fits = switch (size_bits) {
|
||||||
|
8 => new_disp >= math.minInt(i8) and new_disp <= math.maxInt(i8),
|
||||||
|
16 => new_disp >= math.minInt(i16) and new_disp <= math.maxInt(i16),
|
||||||
|
32 => new_disp >= math.minInt(i32) and new_disp <= math.maxInt(i32),
|
||||||
|
64 => true,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fits) {
|
||||||
|
return error.RelocationOverflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ptr = buffer[offset..];
|
||||||
|
switch (size_bits) {
|
||||||
|
8 => ptr[0] = @as(u8, @bitCast(@as(i8, @intCast(new_disp)))),
|
||||||
|
16 => mem.writeInt(u16, ptr[0..2], @bitCast(@as(i16, @intCast(new_disp))), .little),
|
||||||
|
32 => mem.writeInt(u32, ptr[0..4], @bitCast(@as(i32, @intCast(new_disp))), .little),
|
||||||
|
64 => mem.writeInt(u64, ptr[0..8], @bitCast(@as(i64, @intCast(new_disp))), .little),
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const linux = std.os.linux;
|
const linux = std.os.linux;
|
||||||
const posix = std.posix;
|
const posix = std.posix;
|
||||||
const Patcher = @import("Patcher.zig");
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
|
const assert = std.debug.assert;
|
||||||
const page_size = std.heap.pageSize();
|
const page_size = std.heap.pageSize();
|
||||||
|
|
||||||
|
const main = @import("main.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.syscalls);
|
const log = std.log.scoped(.syscalls);
|
||||||
|
|
||||||
/// Represents the stack layout pushed by `syscallEntry` before calling the handler.
|
/// Represents the stack layout pushed by `syscallEntry` before calling the handler.
|
||||||
@@ -114,7 +115,7 @@ export fn syscall_handler(ctx: *SavedContext) callconv(.c) void {
|
|||||||
// mmap addresses are always page aligned
|
// mmap addresses are always page aligned
|
||||||
const ptr = @as([*]align(page_size) u8, @ptrFromInt(addr));
|
const ptr = @as([*]align(page_size) u8, @ptrFromInt(addr));
|
||||||
// Check if we can patch it
|
// Check if we can patch it
|
||||||
Patcher.patchRegion(ptr[0..len]) catch |err| {
|
main.patcher.patchRegion(ptr[0..len]) catch |err| {
|
||||||
std.log.warn("JIT Patching failed: {}", .{err});
|
std.log.warn("JIT Patching failed: {}", .{err});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ export fn syscall_handler(ctx: *SavedContext) callconv(.c) void {
|
|||||||
// mprotect requires addr to be page aligned.
|
// mprotect requires addr to be page aligned.
|
||||||
if (len > 0 and std.mem.isAligned(addr, page_size)) {
|
if (len > 0 and std.mem.isAligned(addr, page_size)) {
|
||||||
const ptr = @as([*]align(page_size) u8, @ptrFromInt(addr));
|
const ptr = @as([*]align(page_size) u8, @ptrFromInt(addr));
|
||||||
Patcher.patchRegion(ptr[0..len]) catch |err| {
|
main.patcher.patchRegion(ptr[0..len]) catch |err| {
|
||||||
std.log.warn("mprotect Patching failed: {}", .{err});
|
std.log.warn("mprotect Patching failed: {}", .{err});
|
||||||
};
|
};
|
||||||
// patchRegion leaves it R|W.
|
// patchRegion leaves it R|W.
|
||||||
@@ -250,7 +251,7 @@ fn isProcSelfExe(path: [*:0]const u8) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handleReadlink(buf_addr: u64, buf_size: u64, ctx: *SavedContext) void {
|
fn handleReadlink(buf_addr: u64, buf_size: u64, ctx: *SavedContext) void {
|
||||||
const target = Patcher.target_exec_path;
|
const target = main.target_exec_path;
|
||||||
const len = @min(target.len, buf_size);
|
const len = @min(target.len, buf_size);
|
||||||
const dest = @as([*]u8, @ptrFromInt(buf_addr));
|
const dest = @as([*]u8, @ptrFromInt(buf_addr));
|
||||||
@memcpy(dest[0..len], target[0..len]);
|
@memcpy(dest[0..len], target[0..len]);
|
||||||
|
|||||||
Reference in New Issue
Block a user