authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-09-18 04:54:15+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-09-17 19:54:15-07:00
log6dd0270a1926661e339d993a825fffa82be6bd51
tree6f7e7cfa8f35001fea399408cfa39b91c96ee1c9
parent4314c9653a874451d1c9b3dbb8be08d966344028
signaturebadge-check Signed by PGP key B5690EEEBB952194

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 files changed, 48 insertions(+), 1 deletions(-)

lib/std/sort/pdq.zig+48-1
......@@ -227,7 +227,7 @@ fn partialInsertionSort(a: usize, b: usize, context: anytype) bool {
227227 // shift the smaller element to the left.
228228 if (i - a >= 2) {
229229 var j = i - 1;
230 while (j >= 1) : (j -= 1) {
230 while (j > a) : (j -= 1) {
231231 if (!context.lessThan(j, j - 1)) break;
232232 context.swap(j, j - 1);
233233 }
......@@ -328,3 +328,50 @@ fn reverseRange(a: usize, b: usize, context: anytype) void {
328328 j -= 1;
329329 }
330330}
331
332test "pdqContext respects arbitrary range boundaries" {
333 // Regression test for issue #25250
334 // pdqsort should never access indices outside the specified [a, b) range
335 var data: [2000]i32 = @splat(0);
336
337 // Fill with data that triggers the partialInsertionSort path
338 for (0..data.len) |i| {
339 data[i] = @intCast(@mod(@as(i32, @intCast(i)) * 7, 100));
340 }
341
342 const TestContext = struct {
343 items: []i32,
344 range_start: usize,
345 range_end: usize,
346
347 pub fn lessThan(ctx: @This(), a: usize, b: usize) bool {
348 // Assert indices are within the expected range
349 testing.expect(a >= ctx.range_start and a < ctx.range_end) catch @panic("index a out of range");
350 testing.expect(b >= ctx.range_start and b < ctx.range_end) catch @panic("index b out of range");
351 return ctx.items[a] < ctx.items[b];
352 }
353
354 pub fn swap(ctx: @This(), a: usize, b: usize) void {
355 // Assert indices are within the expected range
356 testing.expect(a >= ctx.range_start and a < ctx.range_end) catch @panic("index a out of range");
357 testing.expect(b >= ctx.range_start and b < ctx.range_end) catch @panic("index b out of range");
358 mem.swap(i32, &ctx.items[a], &ctx.items[b]);
359 }
360 };
361
362 // Test sorting a sub-range that doesn't start at 0
363 const start = 1118;
364 const end = 1764;
365 const ctx = TestContext{
366 .items = &data,
367 .range_start = start,
368 .range_end = end,
369 };
370
371 pdqContext(start, end, ctx);
372
373 // Verify the range is sorted
374 for ((start + 1)..end) |i| {
375 try testing.expect(data[i - 1] <= data[i]);
376 }
377}