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 {...@@ -1778,9 +1778,10 @@ pub const Condition = struct {
17781778
1779 epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before `state` laod1779 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 might1781 // We were woken normally, so try to consume a pending signal. A signal takes
1782 // cause a signal to get stuck in the state with no corresponding waiter.1782 // priority over an expired deadline, so this is checked before the deadline
1783 {1783 // below. On error we safely remove ourselves as a waiter and propagate the error.
1784 if (result) |_| {
1784 var prev_state = cond.state.load(.monotonic);1785 var prev_state = cond.state.load(.monotonic);
1785 while (prev_state.signals > 0) {1786 while (prev_state.signals > 0) {
1786 prev_state = cond.state.cmpxchgWeak(prev_state, .{1787 prev_state = cond.state.cmpxchgWeak(prev_state, .{
...@@ -1791,22 +1792,19 @@ pub const Condition = struct {...@@ -1791,22 +1792,19 @@ pub const Condition = struct {
1791 return;1792 return;
1792 };1793 };
1793 }1794 }
1795 } else |err| {
1796 cond.deregister(io);
1797 return err;
1794 }1798 }
17951799
1796 // There are no more signals available; this was a spurious wakeup or an error. If it1800 // There are no signals available and no error; if a timeout was specified and
1797 // was an error, we will remove ourselves as a waiter and return that error. If a1801 // the deadline has passed, remove ourselves as a waiter and return
1798 // timeout was specified and the deadline has passed, we remove ourselves as a waiter1802 // `error.Timeout`. Otherwise, this was a spurious wakeup: loop back to the
1799 // and return `error.Timeout`. Otherwise, we'll loop back to the futex wait.1803 // 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 };
1805 switch (deadline) {1804 switch (deadline) {
1806 .none => {},1805 .none => {},
1807 .deadline => |d| if (d.untilNow(io).raw.nanoseconds >= 0) {1806 .deadline => |d| if (d.untilNow(io).raw.nanoseconds >= 0) {
1808 const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);1807 cond.deregister(io);
1809 assert(prev_state.waiters > 0); // underflow caused by illegal state
1810 return error.Timeout;1808 return error.Timeout;
1811 },1809 },
1812 .duration => unreachable,1810 .duration => unreachable,
...@@ -1853,6 +1851,24 @@ pub const Condition = struct {...@@ -1853,6 +1851,24 @@ pub const Condition = struct {
1853 }1851 }
1854 }1852 }
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
1856 pub fn signal(cond: *Condition, io: Io) void {1872 pub fn signal(cond: *Condition, io: Io) void {
1857 var prev_state = cond.state.load(.monotonic);1873 var prev_state = cond.state.load(.monotonic);
1858 while (prev_state.waiters > prev_state.signals) {1874 while (prev_state.waiters > prev_state.signals) {