authorgravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2026-02-04 20:44:45+01:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2026-02-04 20:44:45+01:00
logc3edf0ba641fcaf9ccc93e27ca3bf140d4b8e84c
tree43a268a863cc6c1076fe8e99becec9efb292ed94
parentfce7878a9149caa80433e6d650e0bd7f60d345fd
parent012be3efd779764de8cb910f3d07a25c13861c48
signaturebadge-check Signed by SSH key SHA256:7B/LJ7bpR1eX8aCXSr4mtd5M45VMPKcx9zY8e95b5QM

Merge branch 'netbsd-ci'


3 files changed, 90 insertions(+), 52 deletions(-)

.forgejo/workflows/ci.yaml+20-20
......@@ -197,26 +197,26 @@ jobs:
197197 run: sh ci/x86_64-linux-release.sh
198198 timeout-minutes: 360
199199
200 #x86_64-netbsd-debug:
201 # runs-on: [self-hosted, x86_64-netbsd]
202 # steps:
203 # - name: Checkout
204 # uses: https://codeberg.org/ziglang/checkout@19af6bac491e2534a4687a50ee84fa7f13258d28
205 # with:
206 # fetch-depth: 0
207 # - name: Build and Test
208 # run: sh ci/x86_64-netbsd-debug.sh
209 # timeout-minutes: 120
210 #x86_64-netbsd-release:
211 # runs-on: [self-hosted, x86_64-netbsd]
212 # steps:
213 # - name: Checkout
214 # uses: https://codeberg.org/ziglang/checkout@19af6bac491e2534a4687a50ee84fa7f13258d28
215 # with:
216 # fetch-depth: 0
217 # - name: Build and Test
218 # run: sh ci/x86_64-netbsd-release.sh
219 # timeout-minutes: 120
200 x86_64-netbsd-debug:
201 runs-on: [self-hosted, x86_64-netbsd]
202 steps:
203 - name: Checkout
204 uses: https://codeberg.org/ziglang/checkout@19af6bac491e2534a4687a50ee84fa7f13258d28
205 with:
206 fetch-depth: 0
207 - name: Build and Test
208 run: sh ci/x86_64-netbsd-debug.sh
209 timeout-minutes: 120
210 x86_64-netbsd-release:
211 runs-on: [self-hosted, x86_64-netbsd]
212 steps:
213 - name: Checkout
214 uses: https://codeberg.org/ziglang/checkout@19af6bac491e2534a4687a50ee84fa7f13258d28
215 with:
216 fetch-depth: 0
217 - name: Build and Test
218 run: sh ci/x86_64-netbsd-release.sh
219 timeout-minutes: 120
220220
221221 x86_64-openbsd-debug:
222222 runs-on: [self-hosted, x86_64-openbsd]
lib/std/Io/Threaded.zig+54-32
......@@ -628,6 +628,7 @@ const Thread = struct {
628628 cancel_protection: Io.CancelProtection,
629629 /// Always released when `Status.cancelation` is set to `.parked`.
630630 futex_waiter: if (use_parking_futex) ?*parking_futex.Waiter else ?noreturn,
631 unpark_flag: UnparkFlag,
631632
632633 csprng: Csprng,
633634
......@@ -1018,6 +1019,7 @@ const Thread = struct {
10181019 if (thread.futex_waiter) |futex_waiter| {
10191020 parking_futex.removeCanceledWaiter(futex_waiter);
10201021 }
1022 if (need_unpark_flag) setUnparkFlag(&thread.unpark_flag);
10211023 unpark(&.{thread.id}, null);
10221024 return false;
10231025 },
......@@ -1559,6 +1561,7 @@ fn worker(t: *Threaded) void {
15591561 }),
15601562 .cancel_protection = .unblocked,
15611563 .futex_waiter = undefined,
1564 .unpark_flag = unpark_flag_init,
15621565 .csprng = .{},
15631566 };
15641567 Thread.current = &thread;
......@@ -17007,6 +17010,7 @@ const parking_futex = struct {
1700717010 /// * Unparking the thread (*after* the above, so that the `Waiter` does not go out of scope
1700817011 /// while it is still in the `Bucket`).
1700917012 thread_status: *std.atomic.Value(Thread.Status),
17013 unpark_flag: if (need_unpark_flag) *UnparkFlag else void,
1701017014 };
1701117015
1701217016 fn bucketForAddress(address: usize) *Bucket {
......@@ -17045,9 +17049,11 @@ const parking_futex = struct {
1704517049 .address = @intFromPtr(ptr),
1704617050 .tid = self_tid,
1704717051 .thread_status = undefined, // populated in critical section
17052 .unpark_flag = undefined, // populated in critical section
1704817053 };
1704917054
1705017055 var status_buf: std.atomic.Value(Thread.Status) = undefined;
17056 var unpark_flag_buf: UnparkFlag = unpark_flag_init;
1705117057
1705217058 {
1705317059 bucket.mutex.lock();
......@@ -17062,7 +17068,7 @@ const parking_futex = struct {
1706217068
1706317069 // This is in the critical section to avoid marking the thread as parked until we're
1706417070 // certain that we're actually going to park.
17065 waiter.thread_status = status: {
17071 waiter.thread_status, waiter.unpark_flag = status: {
1706617072 cancelable: {
1706717073 if (uncancelable) break :cancelable;
1706817074 const thread = opt_thread orelse break :cancelable;
......@@ -17090,19 +17096,19 @@ const parking_futex = struct {
1709017096 .blocked_canceling => unreachable,
1709117097 }
1709217098 // We could now be unparked for a cancelation at any time!
17093 break :status &thread.status;
17099 break :status .{ &thread.status, if (need_unpark_flag) &thread.unpark_flag };
1709417100 }
1709517101 // This is an uncancelable wait, so just use `status_buf`. Note that the value of
1709617102 // `status_buf.awaitable` is irrelevant because this is only visible to futex code,
1709717103 // while only cancelation cares about `awaitable`.
1709817104 status_buf.raw = .{ .cancelation = .parked, .awaitable = .null };
17099 break :status &status_buf;
17105 break :status .{ &status_buf, if (need_unpark_flag) &unpark_flag_buf };
1710017106 };
1710117107
1710217108 bucket.waiters.append(&waiter.node);
1710317109 }
1710417110
17105 if (park(timeout, ptr, waiter.thread_status)) {
17111 if (park(timeout, ptr, waiter.unpark_flag)) {
1710617112 // We were unparked by either `wake` or cancelation, so our current status is either
1710717113 // `.none` or `.canceling`. In either case, they've already removed `waiter` from
1710817114 // `bucket`, so we have nothing more to do!
......@@ -17127,7 +17133,7 @@ const parking_futex = struct {
1712717133 // to unpark us. Whoever did that will remove us from `bucket`. Wait for
1712817134 // that (and drop the unpark request in doing so).
1712917135 // New status is `.none` or `.canceling` respectively.
17130 park(.none, ptr, waiter.thread_status) catch |e| switch (e) {
17136 park(.none, ptr, waiter.unpark_flag) catch |e| switch (e) {
1713117137 error.Timeout => unreachable,
1713217138 };
1713317139 },
......@@ -17201,6 +17207,7 @@ const parking_futex = struct {
1720117207 waking_head = node.next;
1720217208 const waiter: *Waiter = @fieldParentPtr("node", node);
1720317209 unpark_buf[unpark_len] = waiter.tid;
17210 if (need_unpark_flag) setUnparkFlag(waiter.unpark_flag);
1720417211 unpark_len += 1;
1720517212 if (unpark_len == unpark_buf.len) {
1720617213 unpark(&unpark_buf, ptr);
......@@ -17249,7 +17256,7 @@ const parking_sleep = struct {
1724917256 .blocked_canceling => unreachable,
1725017257 }
1725117258 }
17252 if (park(timeout, null, &thread.status)) {
17259 if (park(timeout, null, if (need_unpark_flag) &thread.unpark_flag)) {
1725317260 // The only reason this could possibly happen is cancelation.
1725417261 const old_status = thread.status.load(.monotonic);
1725517262 assert(old_status.cancelation == .canceling);
......@@ -17272,7 +17279,7 @@ const parking_sleep = struct {
1727217279 // us for a cancelation. Whoever did that will have called `unpark`, so
1727317280 // drop that unpark request by waiting for it.
1727417281 // Status is still `.canceling`.
17275 park(.none, null, &thread.status) catch |e| switch (e) {
17282 park(.none, null, if (need_unpark_flag) &thread.unpark_flag) catch |e| switch (e) {
1727617283 error.Timeout => unreachable,
1727717284 };
1727817285 return;
......@@ -17288,8 +17295,8 @@ const parking_sleep = struct {
1728817295 }
1728917296 }
1729017297 // Uncancelable sleep; we expect not to be manually unparked.
17291 var dummy_status: std.atomic.Value(Thread.Status) = .init(.{ .cancelation = .parked, .awaitable = .null });
17292 if (park(timeout, null, &dummy_status)) {
17298 var dummy_flag: UnparkFlag = unpark_flag_init;
17299 if (park(timeout, null, if (need_unpark_flag) &dummy_flag)) {
1729317300 unreachable; // unexpected unpark
1729417301 } else |err| switch (err) {
1729517302 error.Timeout => return,
......@@ -17322,7 +17329,7 @@ const ParkingMutex = struct {
1732217329 }
1732317330 };
1732417331 const Waiter = struct {
17325 status: std.atomic.Value(Thread.Status),
17332 unpark_flag: UnparkFlag,
1732617333 /// Never modified once the `Waiter` is in the linked list.
1732717334 next: ?*Waiter,
1732817335 /// Never modified once the `Waiter` is in the linked list.
......@@ -17345,7 +17352,7 @@ const ParkingMutex = struct {
1734517352 const self_tid = if (Thread.current) |t| t.id else std.Thread.getCurrentId();
1734617353 var waiter: Waiter = .{
1734717354 .next = old_waiter,
17348 .status = .init(.{ .cancelation = .parked, .awaitable = .null }),
17355 .unpark_flag = unpark_flag_init,
1734917356 .tid = self_tid,
1735017357 };
1735117358 if (m.state.cmpxchgWeak(
......@@ -17357,11 +17364,9 @@ const ParkingMutex = struct {
1735717364 continue :state new_state;
1735817365 }
1735917366 // We're now in the list of waiters---park until we're given the lock.
17360 park(.none, m, &waiter.status) catch |err| switch (err) {
17367 park(.none, m, if (need_unpark_flag) &waiter.unpark_flag) catch |err| switch (err) {
1736117368 error.Timeout => unreachable,
1736217369 };
17363 // We now hold the lock.
17364 assert(waiter.status.load(.monotonic).cancelation == .none);
1736517370 return;
1736617371 },
1736717372 }
......@@ -17383,7 +17388,7 @@ const ParkingMutex = struct {
1738317388 _ => |last_state| {
1738417389 // The logic here does not have ABA problems, and does some accesses non-atomically,
1738517390 // because `Waiter.next` is owned by the lock holder (that's us!) once the waiter is
17386 // in the linked list, up until we set `Waiter.status` to `.none`.
17391 // in the linked list, up until we unpark the waiter.
1738717392
1738817393 // Run through the waiter list to the end to ensure fairness. This is obviously not
1738917394 // ideal, but it shouldn't be a big deal in practice provided the critical section
......@@ -17412,8 +17417,8 @@ const ParkingMutex = struct {
1741217417 }
1741317418 }
1741417419 // Now we're ready to actually hand the lock over to them.
17415 const tid = waiter.tid; // load this before the store below potentially invalidates `waiter`
17416 waiter.status.store(.{ .cancelation = .none, .awaitable = .null }, .release); // release lock
17420 const tid = waiter.tid; // load before the unpark below potentially invalidates `waiter`
17421 if (need_unpark_flag) setUnparkFlag(&waiter.unpark_flag);
1741717422 unpark(&.{tid}, m);
1741817423 return;
1741917424 },
......@@ -17451,15 +17456,34 @@ fn timeoutToWindowsInterval(timeout: Io.Timeout) ?windows.LARGE_INTEGER {
1745117456 }
1745217457}
1745317458
17459/// The API on NetBSD and Illumos sucks and can unpark spuriously (well, it *can't*, but signals
17460/// cause an indistinguishable unblock, and libpthread really likes to leave unparks pending).
17461/// As such, on these targets only, we need to pass around a flag to track whether a thread is
17462/// "actually" being unparked.
17463const need_unpark_flag = switch (native_os) {
17464 .netbsd, .illumos => true,
17465 else => false,
17466};
17467const UnparkFlag = if (need_unpark_flag) std.atomic.Value(bool) else void;
17468const unpark_flag_init: UnparkFlag = if (need_unpark_flag) .init(false);
17469/// Must be called before `unpark`. After this function is called, the thread may be unparked at any
17470/// time, so the caller must not reference values on its stack.
17471fn setUnparkFlag(f: *UnparkFlag) void {
17472 f.store(true, .release);
17473}
17474
17475/// The type passed into `unpark` for the thread ID. You'd think this was just a `std.Thread.Id`,
17476/// but it seems that someone at Microsoft forgot how big their TIDs are supposed to be.
17477const UnparkTid = switch (native_os) {
17478 .windows => usize,
17479 else => std.Thread.Id,
17480};
17481
1745417482fn park(
1745517483 timeout: Io.Timeout,
1745617484 /// This value has no semantic effect, but may allow the OS to optimize the operation.
1745717485 addr_hint: ?*const anyopaque,
17458 /// The API on NetBSD and Illumos sucks and can unpark spuriously (well, it *can't*, but signals
17459 /// cause an indistinguishable unblock, and libpthread really likes to leave unparks pending).
17460 /// As such, on these targets only, this `status` is checked to determine if an unpark is real.
17461 /// no way to differentiate
17462 status: *std.atomic.Value(Thread.Status),
17486 unpark_flag: if (need_unpark_flag) *UnparkFlag else void,
1746317487) error{Timeout}!void {
1746417488 comptime assert(use_parking_futex or use_parking_sleep);
1746517489 switch (native_os) {
......@@ -17502,7 +17526,7 @@ fn park(
1750217526 };
1750317527 // It's okay to pass the same timeout in a loop. If it's a duration, the OS actually
1750417528 // writes the remaining time into the buffer when the syscall returns.
17505 while (status.load(.monotonic).cancelation == .parked) {
17529 while (!unpark_flag.swap(false, .acquire)) {
1750617530 switch (posix.errno(std.c._lwp_park(
1750717531 if (clock_real) .REALTIME else .MONOTONIC,
1750817532 .{ .ABSTIME = abstime },
......@@ -17523,12 +17547,6 @@ fn park(
1752317547 else => comptime unreachable,
1752417548 }
1752517549}
17526
17527const UnparkTid = switch (native_os) {
17528 // `NtAlertMultipleThreadByThreadId` is weird and wants 64-bit thread IDs?
17529 .windows => usize,
17530 else => std.Thread.Id,
17531};
1753217550/// `addr_hint` has no semantic effect, but may allow the OS to optimize this operation.
1753317551fn unpark(tids: []const UnparkTid, addr_hint: ?*const anyopaque) void {
1753417552 comptime assert(use_parking_futex or use_parking_sleep);
......@@ -17548,8 +17566,8 @@ fn unpark(tids: []const UnparkTid, addr_hint: ?*const anyopaque) void {
1754817566 switch (posix.errno(std.c._lwp_unpark_all(@ptrCast(tids.ptr), tids.len, addr_hint))) {
1754917567 .SUCCESS => return,
1755017568 // For errors, fall through to a loop over `tids`, though this is only expected to
17551 // be possible for ENOMEM (and even that is questionable).
17552 .SRCH => recoverableOsBugDetected(),
17569 // be possible for ENOMEM (even that is questionable) and ESRCH (see comment below).
17570 .SRCH => {},
1755317571 .FAULT => recoverableOsBugDetected(),
1755417572 .INVAL => recoverableOsBugDetected(),
1755517573 .NOMEM => {},
......@@ -17558,7 +17576,11 @@ fn unpark(tids: []const UnparkTid, addr_hint: ?*const anyopaque) void {
1755817576 for (tids) |tid| {
1755917577 switch (posix.errno(std.c._lwp_unpark(@bitCast(tid), addr_hint))) {
1756017578 .SUCCESS => {},
17561 .SRCH => recoverableOsBugDetected(),
17579 .SRCH => {
17580 // This can happen in a rare race: the thread might have been spuriously
17581 // unparked, so already observed the changing status, and from there have
17582 // exited. That's okay, because the thread has woken up like we wanted.
17583 },
1756217584 else => recoverableOsBugDetected(),
1756317585 }
1756417586 }
src/link/Lld.zig+16
......@@ -356,6 +356,16 @@ fn linkAsArchive(lld: *Lld, arena: Allocator) !void {
356356 if (bad) return error.UnableToWriteArchive;
357357}
358358
359fn addCommonArgs(argv: *std.array_list.Managed([]const u8), coff: bool) !void {
360 if (builtin.os.tag == .netbsd) {
361 // NetBSD 10.1's `malloc` appears to have some nasty bugs that occur
362 // when doing parallel linking in LLD, manifesting as input and/or
363 // output section memory randomly being unmapped. So just don't do
364 // parallel linking for now.
365 try argv.append(if (coff) "-threads:1" else "--threads=1");
366 }
367}
368
359369fn coffLink(lld: *Lld, arena: Allocator) !void {
360370 const comp = lld.base.comp;
361371 const gpa = comp.gpa;
......@@ -418,6 +428,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
418428 // it calls exit() and does not reset all global data between invocations.
419429 const linker_command = "lld-link";
420430 try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
431 try addCommonArgs(&argv, true);
421432
422433 if (target.isMinGW()) {
423434 try argv.append("-lldmingw");
......@@ -836,6 +847,8 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
836847 // it calls exit() and does not reset all global data between invocations.
837848 const linker_command = "ld.lld";
838849 try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
850 try addCommonArgs(&argv, false);
851
839852 if (is_obj) {
840853 try argv.append("-r");
841854 }
......@@ -1401,6 +1414,8 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
14011414 // it calls exit() and does not reset all global data between invocations.
14021415 const linker_command = "wasm-ld";
14031416 try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, linker_command });
1417 try addCommonArgs(&argv, false);
1418
14041419 try argv.append("--error-limit=0");
14051420
14061421 if (comp.config.lto != .none) {
......@@ -1724,6 +1739,7 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi
17241739 if (stderr.len > 0) log.warn("unexpected LLD stderr:\n{s}", .{stderr});
17251740}
17261741
1742const builtin = @import("builtin");
17271743const std = @import("std");
17281744const Io = std.Io;
17291745const Allocator = std.mem.Allocator;