Misc runtime safety fixes (#13275)

Runtime safety violating scenarios found by GPT 5.6. Verified each one
manually. See each commit.

I'm going to keep searching so not going to merge this yet.
This commit is contained in:
Mitchell Hashimoto
2026-07-09 20:03:10 -07:00
committed by GitHub
5 changed files with 109 additions and 10 deletions

View File

@@ -6251,13 +6251,14 @@ pub const Pin = struct {
if (n <= remaining_in_row) return self.left(n);
const extra_after_remaining = n - remaining_in_row;
const rows_off = 1 + extra_after_remaining / cols;
const rows_off = 1 + (extra_after_remaining - 1) / cols;
switch (self.upOverflow(rows_off)) {
.offset => |v| {
var result = v;
result.x = @intCast(cols - extra_after_remaining % cols);
result.x = @intCast(
cols - 1 - (extra_after_remaining - 1) % cols,
);
return result;
},
.overflow => return null,
@@ -6278,13 +6279,12 @@ pub const Pin = struct {
if (n <= remaining_in_row) return self.right(n);
const extra_after_remaining = n - remaining_in_row;
const rows_off = 1 + extra_after_remaining / cols;
const rows_off = 1 + (extra_after_remaining - 1) / cols;
switch (self.downOverflow(rows_off)) {
.offset => |v| {
var result = v;
result.x = @intCast(extra_after_remaining % cols - 1);
result.x = @intCast((extra_after_remaining - 1) % cols);
return result;
},
.overflow => return null,
@@ -6451,6 +6451,58 @@ fn growColdPagesForTest(self: *PageList, count: usize) !void {
}
}
test "PageList Pin rightWrap exact row multiple" {
const testing = std.testing;
var s = try init(testing.allocator, 10, 3, null);
defer s.deinit();
const start = s.pin(.{ .active = .{ .x = 5, .y = 0 } }).?;
const wrapped = start.rightWrap(14).?;
_ = wrapped.rowAndCell();
try testing.expectEqual(
point.Point{ .active = .{ .x = 9, .y = 1 } },
s.pointFromPin(.active, wrapped),
);
}
test "PageList Pin leftWrap exact row multiple" {
const testing = std.testing;
var s = try init(testing.allocator, 10, 3, null);
defer s.deinit();
const start = s.pin(.{ .active = .{ .x = 5, .y = 2 } }).?;
const wrapped = start.leftWrap(15).?;
_ = wrapped.rowAndCell();
try testing.expectEqual(
point.Point{ .active = .{ .x = 0, .y = 1 } },
s.pointFromPin(.active, wrapped),
);
}
test "PageList Pin rightWrap maximum distance" {
const testing = std.testing;
var s = try init(testing.allocator, 1, 3, null);
defer s.deinit();
const start = s.pin(.{ .active = .{ .y = 0 } }).?;
try testing.expectEqual(null, start.rightWrap(std.math.maxInt(usize)));
}
test "PageList Pin leftWrap maximum distance" {
const testing = std.testing;
var s = try init(testing.allocator, 1, 3, null);
defer s.deinit();
const start = s.pin(.{ .active = .{ .y = 2 } }).?;
try testing.expectEqual(null, start.leftWrap(std.math.maxInt(usize)));
}
test "PageList incremental compression skips visible history" {
const testing = std.testing;

View File

@@ -2120,8 +2120,8 @@ pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void {
// Calculate our new x/y
const row = if (row_req == 0) 1 else row_req;
const col = if (col_req == 0) 1 else col_req;
const x = @min(params.x_max, col + params.x_offset) -| 1;
const y = @min(params.y_max, row + params.y_offset) -| 1;
const x = @min(params.x_max, col +| params.x_offset) -| 1;
const y = @min(params.y_max, row +| params.y_offset) -| 1;
// If the y is unchanged then this is fast pointer math
if (y == self.screens.active.cursor.y) {
@@ -3816,6 +3816,24 @@ fn clearDirty(t: *Terminal) void {
t.screens.active.pages.clearDirty();
}
test "Terminal: setCursorPos saturates overflowing origin offsets" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
t.scrolling_region = .{
.top = 2,
.bottom = 7,
.left = 3,
.right = 8,
};
t.modes.set(.origin, true);
t.setCursorPos(std.math.maxInt(usize), std.math.maxInt(usize));
try testing.expectEqual(@as(size.CellCountInt, 8), t.screens.active.cursor.x);
try testing.expectEqual(@as(size.CellCountInt, 7), t.screens.active.cursor.y);
}
test "Terminal: input with no control characters" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 40, .rows = 40 });

View File

@@ -56,6 +56,10 @@ pub const ActiveSearch = struct {
// Clear our previous sliding window
self.window.clearAndRetainCapacity();
// An empty needle represents an inactive search and has no overlap
// or history to load.
if (self.window.needle.len == 0) return null;
// First up, add enough pages to cover the active area.
var rem: usize = list.rows;
var node_ = list.pages.last;

View File

@@ -176,6 +176,11 @@ pub const SlidingWindow = struct {
/// or `append()`. If the caller wants to retain the flattened highlight
/// then they should clone it.
pub fn next(self: *SlidingWindow) ?FlattenedHighlight {
// An empty needle represents an inactive search. Searching for it
// would otherwise produce a zero-length match, which highlight()
// cannot represent because its end offset is inclusive.
if (self.needle.len == 0) return null;
const slices = slices: {
// If we have less data then the needle then we can't possibly match
const data_len = self.data.len();
@@ -704,6 +709,25 @@ test "SlidingWindow empty on init" {
try testing.expectEqual(0, w.meta.len());
}
test "SlidingWindow empty needle has no matches" {
const testing = std.testing;
const alloc = testing.allocator;
var w: SlidingWindow = try .init(alloc, .forward, "");
defer w.deinit();
var s = try Screen.init(alloc, .{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
});
defer s.deinit();
try s.testWriteString("hello");
_ = try w.append(s.pages.pages.first.?);
try testing.expectEqual(null, w.next());
}
test "SlidingWindow single append" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@@ -134,6 +134,7 @@ pub const ViewportSearch = struct {
// Add enough overlap to cover needle.len - 1 bytes (if it
// exists) so we can cover the overlap. We only do this for the
// soft-wrapped prior pages.
const overlap_len = self.window.needle.len -| 1;
var node_ = fingerprint.nodes[0].prev;
var added: usize = 0;
while (node_) |node| : (node_ = node.prev) {
@@ -142,7 +143,7 @@ pub const ViewportSearch = struct {
// wrap so this should be fine.
const appended = try self.window.appendIfWrapped(node) orelse break;
added += appended.content_len;
if (added >= self.window.needle.len - 1) break;
if (added >= overlap_len) break;
}
// We can use our fingerprint nodes to initialize our sliding
@@ -162,7 +163,7 @@ pub const ViewportSearch = struct {
while (node_) |node| : (node_ = node.next) {
const appended = try self.window.append(node);
added += appended.content_len;
if (added >= self.window.needle.len - 1) break;
if (added >= overlap_len) break;
// If this row doesn't wrap, then we can quit
if (!appended.last_row_wrapped) break;