authorgravatar for lukasl@noreply.codeberg.orgLukáš Lalinský <lukasl@noreply.codeberg.org> 2026-08-11 10:43:13+02:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-08-11 10:43:13+02:00
log254c23b03875387798b8ce779223091c5f6ee444
tree52b1907ad6784686229db3a6f4693d406aab9ae2
parentcecc3e1b917b1c6c5793b6c98783b67239b17682

Fix `std.Io.Condition.wait` cancelation race / error swallowing (#35564)

When `std.Io.Condition` is signaled and almost immediately after that canceled, it will swallow the `error.Canceled` error and leave the task in an uncancelable state. Co-authored-by: Lukas Lalinsky <lukas@lalinsky.com> Reviewed-on: https://codeberg.org/ziglang/zig/pulls/35564 Reviewed-by: mlugg <mlugg@mlugg.co.uk>

1 files changed, 30 insertions(+), 14 deletions(-)

lib/std/Io.zig+30-14
......@@ -1778,9 +1778,10 @@ pub const Condition = struct {
17781778
17791779 epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before `state` laod
17801780
1781 // Even on error, try to consume a pending signal first. Otherwise a race might
1782 // cause a signal to get stuck in the state with no corresponding waiter.
1783 {
1781 // We were woken normally, so try to consume a pending signal. A signal takes
1782 // priority over an expired deadline, so this is checked before the deadline
1783 // below. On error we safely remove ourselves as a waiter and propagate the error.
1784 if (result) |_| {
17841785 var prev_state = cond.state.load(.monotonic);
17851786 while (prev_state.signals > 0) {
17861787 prev_state = cond.state.cmpxchgWeak(prev_state, .{
......@@ -1791,22 +1792,19 @@ pub const Condition = struct {
17911792 return;
17921793 };
17931794 }
1795 } else |err| {
1796 cond.deregister(io);
1797 return err;
17941798 }
17951799
1796 // There are no more signals available; this was a spurious wakeup or an error. If it
1797 // was an error, we will remove ourselves as a waiter and return that error. If a
1798 // timeout was specified and the deadline has passed, we remove ourselves as a waiter
1799 // and return `error.Timeout`. Otherwise, we'll loop back to the futex wait.
1800 result catch |err| {
1801 const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
1802 assert(prev_state.waiters > 0); // underflow caused by illegal state
1803 return err;
1804 };
1800 // There are no signals available and no error; if a timeout was specified and
1801 // the deadline has passed, remove ourselves as a waiter and return
1802 // `error.Timeout`. Otherwise, this was a spurious wakeup: loop back to the
1803 // futex wait.
18051804 switch (deadline) {
18061805 .none => {},
18071806 .deadline => |d| if (d.untilNow(io).raw.nanoseconds >= 0) {
1808 const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
1809 assert(prev_state.waiters > 0); // underflow caused by illegal state
1807 cond.deregister(io);
18101808 return error.Timeout;
18111809 },
18121810 .duration => unreachable,
......@@ -1853,6 +1851,24 @@ pub const Condition = struct {
18531851 }
18541852 }
18551853
1854 fn deregister(cond: *Condition, io: Io) void {
1855 var prev_state = cond.state.load(.monotonic);
1856 while (true) {
1857 const new_signals = @min(prev_state.signals, prev_state.waiters - 1);
1858 prev_state = cond.state.cmpxchgWeak(prev_state, .{
1859 .waiters = prev_state.waiters - 1,
1860 .signals = new_signals,
1861 }, .monotonic, .monotonic) orelse {
1862 if (prev_state.signals > 0 and prev_state.signals < prev_state.waiters) {
1863 // We kept a signal we are not consuming; wake a remaining waiter for it.
1864 _ = cond.epoch.fetchAdd(1, .release);
1865 io.futexWake(u32, &cond.epoch.raw, 1);
1866 }
1867 return;
1868 };
1869 }
1870 }
1871
18561872 pub fn signal(cond: *Condition, io: Io) void {
18571873 var prev_state = cond.state.load(.monotonic);
18581874 while (prev_state.waiters > prev_state.signals) {