authorgravatar for carl@astholm.seCarl Åstholm <carl@astholm.se> 2025-11-05 01:00:44+01:00
committergravatar for carl@astholm.seCarl Åstholm <carl@astholm.se> 2025-11-11 01:11:45+01:00
logcca2d0995099fc37ac792e6322cad88cc67ac056
treeb1e73ce9c370c2cdf9bf47ef045c2cc6381e785c
parent5f1392247d8bb0c6355f2986ff6fbf09a0885b6f

io: Correctly align async closure contexts

This fixes package fetching on Windows. Previously, `Async/GroupClosure` allocations were only aligned for the closure struct type, which resulted in panics when `context_alignment` (or `result_alignment` for that matter) had a greater alignment.

2 files changed, 165 insertions(+), 83 deletions(-)

lib/std/Io/Threaded.zig+106-83
...@@ -389,6 +389,7 @@ const AsyncClosure = struct {...@@ -389,6 +389,7 @@ const AsyncClosure = struct {
389 select_condition: ?*ResetEvent,389 select_condition: ?*ResetEvent,
390 context_alignment: std.mem.Alignment,390 context_alignment: std.mem.Alignment,
391 result_offset: usize,391 result_offset: usize,
392 alloc_len: usize,
392393
393 const done_reset_event: *ResetEvent = @ptrFromInt(@alignOf(ResetEvent));394 const done_reset_event: *ResetEvent = @ptrFromInt(@alignOf(ResetEvent));
394395
...@@ -425,18 +426,59 @@ const AsyncClosure = struct {...@@ -425,18 +426,59 @@ const AsyncClosure = struct {
425426
426 fn contextPointer(ac: *AsyncClosure) [*]u8 {427 fn contextPointer(ac: *AsyncClosure) [*]u8 {
427 const base: [*]u8 = @ptrCast(ac);428 const base: [*]u8 = @ptrCast(ac);
428 return base + ac.context_alignment.forward(@sizeOf(AsyncClosure));429 const context_offset = ac.context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure)) - @intFromPtr(ac);
430 return base + context_offset;
431 }
432
433 fn init(
434 gpa: Allocator,
435 mode: enum { async, concurrent },
436 result_len: usize,
437 result_alignment: std.mem.Alignment,
438 context: []const u8,
439 context_alignment: std.mem.Alignment,
440 func: *const fn (context: *const anyopaque, result: *anyopaque) void,
441 ) Allocator.Error!*AsyncClosure {
442 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(AsyncClosure);
443 const worst_case_context_offset = context_alignment.forward(@sizeOf(AsyncClosure) + max_context_misalignment);
444 const worst_case_result_offset = result_alignment.forward(worst_case_context_offset + context.len);
445 const alloc_len = worst_case_result_offset + result_len;
446
447 const ac: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), alloc_len)));
448 errdefer comptime unreachable;
449
450 const actual_context_addr = context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure));
451 const actual_result_addr = result_alignment.forward(actual_context_addr + context.len);
452 const actual_result_offset = actual_result_addr - @intFromPtr(ac);
453 ac.* = .{
454 .closure = .{
455 .cancel_tid = .none,
456 .start = start,
457 .is_concurrent = switch (mode) {
458 .async => false,
459 .concurrent => true,
460 },
461 },
462 .func = func,
463 .context_alignment = context_alignment,
464 .result_offset = actual_result_offset,
465 .alloc_len = alloc_len,
466 .reset_event = .unset,
467 .select_condition = null,
468 };
469 @memcpy(ac.contextPointer()[0..context.len], context);
470 return ac;
429 }471 }
430472
431 fn waitAndFree(ac: *AsyncClosure, gpa: Allocator, result: []u8) void {473 fn waitAndDeinit(ac: *AsyncClosure, gpa: Allocator, result: []u8) void {
432 ac.reset_event.waitUncancelable();474 ac.reset_event.waitUncancelable();
433 @memcpy(result, ac.resultPointer()[0..result.len]);475 @memcpy(result, ac.resultPointer()[0..result.len]);
434 free(ac, gpa, result.len);476 ac.deinit(gpa);
435 }477 }
436478
437 fn free(ac: *AsyncClosure, gpa: Allocator, result_len: usize) void {479 fn deinit(ac: *AsyncClosure, gpa: Allocator) void {
438 const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac);480 const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac);
439 gpa.free(base[0 .. ac.result_offset + result_len]);481 gpa.free(base[0..ac.alloc_len]);
440 }482 }
441};483};
442484
...@@ -452,6 +494,7 @@ fn async(...@@ -452,6 +494,7 @@ fn async(
452 start(context.ptr, result.ptr);494 start(context.ptr, result.ptr);
453 return null;495 return null;
454 }496 }
497
455 const t: *Threaded = @ptrCast(@alignCast(userdata));498 const t: *Threaded = @ptrCast(@alignCast(userdata));
456 const cpu_count = t.cpu_count catch {499 const cpu_count = t.cpu_count catch {
457 return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch {500 return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch {
...@@ -459,37 +502,20 @@ fn async(...@@ -459,37 +502,20 @@ fn async(
459 return null;502 return null;
460 };503 };
461 };504 };
505
462 const gpa = t.allocator;506 const gpa = t.allocator;
463 const context_offset = context_alignment.forward(@sizeOf(AsyncClosure));507 const ac = AsyncClosure.init(gpa, .async, result.len, result_alignment, context, context_alignment, start) catch {
464 const result_offset = result_alignment.forward(context_offset + context.len);
465 const n = result_offset + result.len;
466 const ac: *AsyncClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch {
467 start(context.ptr, result.ptr);508 start(context.ptr, result.ptr);
468 return null;509 return null;
469 }));
470
471 ac.* = .{
472 .closure = .{
473 .cancel_tid = .none,
474 .start = AsyncClosure.start,
475 .is_concurrent = false,
476 },
477 .func = start,
478 .context_alignment = context_alignment,
479 .result_offset = result_offset,
480 .reset_event = .unset,
481 .select_condition = null,
482 };510 };
483511
484 @memcpy(ac.contextPointer()[0..context.len], context);
485
486 t.mutex.lock();512 t.mutex.lock();
487513
488 const thread_capacity = cpu_count - 1 + t.concurrent_count;514 const thread_capacity = cpu_count - 1 + t.concurrent_count;
489515
490 t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {516 t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {
491 t.mutex.unlock();517 t.mutex.unlock();
492 ac.free(gpa, result.len);518 ac.deinit(gpa);
493 start(context.ptr, result.ptr);519 start(context.ptr, result.ptr);
494 return null;520 return null;
495 };521 };
...@@ -501,7 +527,7 @@ fn async(...@@ -501,7 +527,7 @@ fn async(
501 if (t.threads.items.len == 0) {527 if (t.threads.items.len == 0) {
502 assert(t.run_queue.popFirst() == &ac.closure.node);528 assert(t.run_queue.popFirst() == &ac.closure.node);
503 t.mutex.unlock();529 t.mutex.unlock();
504 ac.free(gpa, result.len);530 ac.deinit(gpa);
505 start(context.ptr, result.ptr);531 start(context.ptr, result.ptr);
506 return null;532 return null;
507 }533 }
...@@ -530,27 +556,11 @@ fn concurrent(...@@ -530,27 +556,11 @@ fn concurrent(
530556
531 const t: *Threaded = @ptrCast(@alignCast(userdata));557 const t: *Threaded = @ptrCast(@alignCast(userdata));
532 const cpu_count = t.cpu_count catch 1;558 const cpu_count = t.cpu_count catch 1;
559
533 const gpa = t.allocator;560 const gpa = t.allocator;
534 const context_offset = context_alignment.forward(@sizeOf(AsyncClosure));561 const ac = AsyncClosure.init(gpa, .concurrent, result_len, result_alignment, context, context_alignment, start) catch {
535 const result_offset = result_alignment.forward(context_offset + context.len);
536 const n = result_offset + result_len;
537 const ac_bytes = gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch
538 return error.ConcurrencyUnavailable;562 return error.ConcurrencyUnavailable;
539 const ac: *AsyncClosure = @ptrCast(@alignCast(ac_bytes));
540
541 ac.* = .{
542 .closure = .{
543 .cancel_tid = .none,
544 .start = AsyncClosure.start,
545 .is_concurrent = true,
546 },
547 .func = start,
548 .context_alignment = context_alignment,
549 .result_offset = result_offset,
550 .reset_event = .unset,
551 .select_condition = null,
552 };563 };
553 @memcpy(ac.contextPointer()[0..context.len], context);
554564
555 t.mutex.lock();565 t.mutex.lock();
556566
...@@ -559,7 +569,7 @@ fn concurrent(...@@ -559,7 +569,7 @@ fn concurrent(
559569
560 t.threads.ensureTotalCapacity(gpa, thread_capacity) catch {570 t.threads.ensureTotalCapacity(gpa, thread_capacity) catch {
561 t.mutex.unlock();571 t.mutex.unlock();
562 ac.free(gpa, result_len);572 ac.deinit(gpa);
563 return error.ConcurrencyUnavailable;573 return error.ConcurrencyUnavailable;
564 };574 };
565575
...@@ -569,7 +579,7 @@ fn concurrent(...@@ -569,7 +579,7 @@ fn concurrent(
569 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {579 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
570 assert(t.run_queue.popFirst() == &ac.closure.node);580 assert(t.run_queue.popFirst() == &ac.closure.node);
571 t.mutex.unlock();581 t.mutex.unlock();
572 ac.free(gpa, result_len);582 ac.deinit(gpa);
573 return error.ConcurrencyUnavailable;583 return error.ConcurrencyUnavailable;
574 };584 };
575 t.threads.appendAssumeCapacity(thread);585 t.threads.appendAssumeCapacity(thread);
...@@ -588,7 +598,7 @@ const GroupClosure = struct {...@@ -588,7 +598,7 @@ const GroupClosure = struct {
588 node: std.SinglyLinkedList.Node,598 node: std.SinglyLinkedList.Node,
589 func: *const fn (*Io.Group, context: *anyopaque) void,599 func: *const fn (*Io.Group, context: *anyopaque) void,
590 context_alignment: std.mem.Alignment,600 context_alignment: std.mem.Alignment,
591 context_len: usize,601 alloc_len: usize,
592602
593 fn start(closure: *Closure) void {603 fn start(closure: *Closure) void {
594 const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure));604 const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure));
...@@ -616,22 +626,48 @@ const GroupClosure = struct {...@@ -616,22 +626,48 @@ const GroupClosure = struct {
616 if (prev_state == (sync_one_pending | sync_is_waiting)) reset_event.set();626 if (prev_state == (sync_one_pending | sync_is_waiting)) reset_event.set();
617 }627 }
618628
619 fn free(gc: *GroupClosure, gpa: Allocator) void {
620 const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(gc);
621 gpa.free(base[0..contextEnd(gc.context_alignment, gc.context_len)]);
622 }
623
624 fn contextOffset(context_alignment: std.mem.Alignment) usize {
625 return context_alignment.forward(@sizeOf(GroupClosure));
626 }
627
628 fn contextEnd(context_alignment: std.mem.Alignment, context_len: usize) usize {
629 return contextOffset(context_alignment) + context_len;
630 }
631
632 fn contextPointer(gc: *GroupClosure) [*]u8 {629 fn contextPointer(gc: *GroupClosure) [*]u8 {
633 const base: [*]u8 = @ptrCast(gc);630 const base: [*]u8 = @ptrCast(gc);
634 return base + contextOffset(gc.context_alignment);631 const context_offset = gc.context_alignment.forward(@intFromPtr(gc) + @sizeOf(GroupClosure)) - @intFromPtr(gc);
632 return base + context_offset;
633 }
634
635 /// Does not initialize the `node` field.
636 fn init(
637 gpa: Allocator,
638 t: *Threaded,
639 group: *Io.Group,
640 context: []const u8,
641 context_alignment: std.mem.Alignment,
642 func: *const fn (*Io.Group, context: *const anyopaque) void,
643 ) Allocator.Error!*GroupClosure {
644 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);
645 const worst_case_context_offset = context_alignment.forward(@sizeOf(GroupClosure) + max_context_misalignment);
646 const alloc_len = worst_case_context_offset + context.len;
647
648 const gc: *GroupClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(GroupClosure), alloc_len)));
649 errdefer comptime unreachable;
650
651 gc.* = .{
652 .closure = .{
653 .cancel_tid = .none,
654 .start = start,
655 .is_concurrent = false,
656 },
657 .t = t,
658 .group = group,
659 .node = undefined,
660 .func = func,
661 .context_alignment = context_alignment,
662 .alloc_len = alloc_len,
663 };
664 @memcpy(gc.contextPointer()[0..context.len], context);
665 return gc;
666 }
667
668 fn deinit(gc: *GroupClosure, gpa: Allocator) void {
669 const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(gc);
670 gpa.free(base[0..gc.alloc_len]);
635 }671 }
636672
637 const sync_is_waiting: usize = 1 << 0;673 const sync_is_waiting: usize = 1 << 0;
...@@ -646,27 +682,14 @@ fn groupAsync(...@@ -646,27 +682,14 @@ fn groupAsync(
646 start: *const fn (*Io.Group, context: *const anyopaque) void,682 start: *const fn (*Io.Group, context: *const anyopaque) void,
647) void {683) void {
648 if (builtin.single_threaded) return start(group, context.ptr);684 if (builtin.single_threaded) return start(group, context.ptr);
685
649 const t: *Threaded = @ptrCast(@alignCast(userdata));686 const t: *Threaded = @ptrCast(@alignCast(userdata));
650 const cpu_count = t.cpu_count catch 1;687 const cpu_count = t.cpu_count catch 1;
688
651 const gpa = t.allocator;689 const gpa = t.allocator;
652 const n = GroupClosure.contextEnd(context_alignment, context.len);690 const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch {
653 const gc: *GroupClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(GroupClosure), n) catch {
654 return start(group, context.ptr);691 return start(group, context.ptr);
655 }));
656 gc.* = .{
657 .closure = .{
658 .cancel_tid = .none,
659 .start = GroupClosure.start,
660 .is_concurrent = false,
661 },
662 .t = t,
663 .group = group,
664 .node = undefined,
665 .func = start,
666 .context_alignment = context_alignment,
667 .context_len = context.len,
668 };692 };
669 @memcpy(gc.contextPointer()[0..context.len], context);
670693
671 t.mutex.lock();694 t.mutex.lock();
672695
...@@ -678,7 +701,7 @@ fn groupAsync(...@@ -678,7 +701,7 @@ fn groupAsync(
678701
679 t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {702 t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {
680 t.mutex.unlock();703 t.mutex.unlock();
681 gc.free(gpa);704 gc.deinit(gpa);
682 return start(group, context.ptr);705 return start(group, context.ptr);
683 };706 };
684707
...@@ -688,7 +711,7 @@ fn groupAsync(...@@ -688,7 +711,7 @@ fn groupAsync(
688 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {711 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
689 assert(t.run_queue.popFirst() == &gc.closure.node);712 assert(t.run_queue.popFirst() == &gc.closure.node);
690 t.mutex.unlock();713 t.mutex.unlock();
691 gc.free(gpa);714 gc.deinit(gpa);
692 return start(group, context.ptr);715 return start(group, context.ptr);
693 };716 };
694 t.threads.appendAssumeCapacity(thread);717 t.threads.appendAssumeCapacity(thread);
...@@ -730,7 +753,7 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {...@@ -730,7 +753,7 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
730 while (true) {753 while (true) {
731 const gc: *GroupClosure = @fieldParentPtr("node", node);754 const gc: *GroupClosure = @fieldParentPtr("node", node);
732 const node_next = node.next;755 const node_next = node.next;
733 gc.free(gpa);756 gc.deinit(gpa);
734 node = node_next orelse break;757 node = node_next orelse break;
735 }758 }
736}759}
...@@ -761,7 +784,7 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void...@@ -761,7 +784,7 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void
761 while (true) {784 while (true) {
762 const gc: *GroupClosure = @fieldParentPtr("node", node);785 const gc: *GroupClosure = @fieldParentPtr("node", node);
763 const node_next = node.next;786 const node_next = node.next;
764 gc.free(gpa);787 gc.deinit(gpa);
765 node = node_next orelse break;788 node = node_next orelse break;
766 }789 }
767 }790 }
...@@ -776,7 +799,7 @@ fn await(...@@ -776,7 +799,7 @@ fn await(
776 _ = result_alignment;799 _ = result_alignment;
777 const t: *Threaded = @ptrCast(@alignCast(userdata));800 const t: *Threaded = @ptrCast(@alignCast(userdata));
778 const closure: *AsyncClosure = @ptrCast(@alignCast(any_future));801 const closure: *AsyncClosure = @ptrCast(@alignCast(any_future));
779 closure.waitAndFree(t.allocator, result);802 closure.waitAndDeinit(t.allocator, result);
780}803}
781804
782fn cancel(805fn cancel(
...@@ -789,7 +812,7 @@ fn cancel(...@@ -789,7 +812,7 @@ fn cancel(
789 const t: *Threaded = @ptrCast(@alignCast(userdata));812 const t: *Threaded = @ptrCast(@alignCast(userdata));
790 const ac: *AsyncClosure = @ptrCast(@alignCast(any_future));813 const ac: *AsyncClosure = @ptrCast(@alignCast(any_future));
791 ac.closure.requestCancel();814 ac.closure.requestCancel();
792 ac.waitAndFree(t.allocator, result);815 ac.waitAndDeinit(t.allocator, result);
793}816}
794817
795fn cancelRequested(userdata: ?*anyopaque) bool {818fn cancelRequested(userdata: ?*anyopaque) bool {
lib/std/Io/Threaded/test.zig+59
...@@ -56,3 +56,62 @@ test "concurrent vs concurrent prevents deadlock via oversubscription" {...@@ -56,3 +56,62 @@ test "concurrent vs concurrent prevents deadlock via oversubscription" {
56 getter.await(io);56 getter.await(io);
57 putter.await(io);57 putter.await(io);
58}58}
59
60const ByteArray256 = struct { x: [32]u8 align(32) };
61const ByteArray512 = struct { x: [64]u8 align(64) };
62
63fn concatByteArrays(a: ByteArray256, b: ByteArray256) ByteArray512 {
64 return .{ .x = a.x ++ b.x };
65}
66
67test "async/concurrent context and result alignment" {
68 var buffer: [2048]u8 align(@alignOf(ByteArray512)) = undefined;
69 var fba: std.heap.FixedBufferAllocator = .init(&buffer);
70
71 var threaded: std.Io.Threaded = .init(fba.allocator());
72 defer threaded.deinit();
73 const io = threaded.io();
74
75 const a: ByteArray256 = .{ .x = @splat(2) };
76 const b: ByteArray256 = .{ .x = @splat(3) };
77 const expected: ByteArray512 = .{ .x = @as([32]u8, @splat(2)) ++ @as([32]u8, @splat(3)) };
78
79 {
80 var future = io.async(concatByteArrays, .{ a, b });
81 const result = future.await(io);
82 try std.testing.expectEqualSlices(u8, &expected.x, &result.x);
83 }
84 {
85 var future = io.concurrent(concatByteArrays, .{ a, b }) catch |err| switch (err) {
86 error.ConcurrencyUnavailable => {
87 try testing.expect(builtin.single_threaded);
88 return;
89 },
90 };
91 const result = future.await(io);
92 try std.testing.expectEqualSlices(u8, &expected.x, &result.x);
93 }
94}
95
96fn concatByteArraysResultPtr(a: ByteArray256, b: ByteArray256, result: *ByteArray512) void {
97 result.* = .{ .x = a.x ++ b.x };
98}
99
100test "Group.async context alignment" {
101 var buffer: [2048]u8 align(@alignOf(ByteArray512)) = undefined;
102 var fba: std.heap.FixedBufferAllocator = .init(&buffer);
103
104 var threaded: std.Io.Threaded = .init(fba.allocator());
105 defer threaded.deinit();
106 const io = threaded.io();
107
108 const a: ByteArray256 = .{ .x = @splat(2) };
109 const b: ByteArray256 = .{ .x = @splat(3) };
110 const expected: ByteArray512 = .{ .x = @as([32]u8, @splat(2)) ++ @as([32]u8, @splat(3)) };
111
112 var group: std.Io.Group = .init;
113 var result: ByteArray512 = undefined;
114 group.async(io, concatByteArraysResultPtr, .{ a, b, &result });
115 group.wait(io);
116 try std.testing.expectEqualSlices(u8, &expected.x, &result.x);
117}