Commit 6dd0270a19

Frank Denis <124872+jedisct1@users.noreply.github.com>
2025-09-18 04:54:15
std.sort.pdq: fix out-of-bounds access in partialInsertionSort (#25253)
* std.sort.pdq: fix out-of-bounds access in partialInsertionSort When sorting a sub-range that doesn't start at index 0, the partialInsertionSort function could access indices below the range start. The loop condition `while (j >= 1)` didn't respect the arbitrary range boundaries [a, b). This changes the condition to `while (j > a)` to ensure indices never go below the range start, fixing the issue where pdqContext would access out-of-bounds indices. Fixes #25250
1 parent 4314c96
Changed files (1)
lib
std
sort
lib/std/sort/pdq.zig
@@ -227,7 +227,7 @@ fn partialInsertionSort(a: usize, b: usize, context: anytype) bool {
         // shift the smaller element to the left.
         if (i - a >= 2) {
             var j = i - 1;
-            while (j >= 1) : (j -= 1) {
+            while (j > a) : (j -= 1) {
                 if (!context.lessThan(j, j - 1)) break;
                 context.swap(j, j - 1);
             }
@@ -328,3 +328,50 @@ fn reverseRange(a: usize, b: usize, context: anytype) void {
         j -= 1;
     }
 }
+
+test "pdqContext respects arbitrary range boundaries" {
+    // Regression test for issue #25250
+    // pdqsort should never access indices outside the specified [a, b) range
+    var data: [2000]i32 = @splat(0);
+
+    // Fill with data that triggers the partialInsertionSort path
+    for (0..data.len) |i| {
+        data[i] = @intCast(@mod(@as(i32, @intCast(i)) * 7, 100));
+    }
+
+    const TestContext = struct {
+        items: []i32,
+        range_start: usize,
+        range_end: usize,
+
+        pub fn lessThan(ctx: @This(), a: usize, b: usize) bool {
+            // Assert indices are within the expected range
+            testing.expect(a >= ctx.range_start and a < ctx.range_end) catch @panic("index a out of range");
+            testing.expect(b >= ctx.range_start and b < ctx.range_end) catch @panic("index b out of range");
+            return ctx.items[a] < ctx.items[b];
+        }
+
+        pub fn swap(ctx: @This(), a: usize, b: usize) void {
+            // Assert indices are within the expected range
+            testing.expect(a >= ctx.range_start and a < ctx.range_end) catch @panic("index a out of range");
+            testing.expect(b >= ctx.range_start and b < ctx.range_end) catch @panic("index b out of range");
+            mem.swap(i32, &ctx.items[a], &ctx.items[b]);
+        }
+    };
+
+    // Test sorting a sub-range that doesn't start at 0
+    const start = 1118;
+    const end = 1764;
+    const ctx = TestContext{
+        .items = &data,
+        .range_start = start,
+        .range_end = end,
+    };
+
+    pdqContext(start, end, ctx);
+
+    // Verify the range is sorted
+    for ((start + 1)..end) |i| {
+        try testing.expect(data[i - 1] <= data[i]);
+    }
+}