authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-22 12:01:37+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-22 12:47:38+00:00
log2046e0f4be23028d18009a61a517d0712e2ca164
tree48e0c21716723769f40bc0ead859a1171c771b62
parent4772f1a9f418d91e3e157b4f45a8edb7ed7975f0
signaturelock-open Commit is signed but in an unrecognized format.

std.Io.Threaded: fix group closure leak

More tasks could be added to the group at any time before it completes, so it's not valid to look at the `token` passed in here. There's also a related bug in `Threaded`, which is that tasks spawned in a group after it is canceled will not observe that cancelation, but that is a more complex bug which needs some deeper design changes.

1 files changed, 22 insertions(+), 26 deletions(-)

lib/std/Io/Threaded.zig+22-26
...@@ -1183,10 +1183,12 @@ fn groupConcurrent(...@@ -1183,10 +1183,12 @@ fn groupConcurrent(
1183 t.cond.signal();1183 t.cond.signal();
1184}1184}
11851185
1186fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {1186fn groupWait(userdata: ?*anyopaque, group: *Io.Group, initial_token: *anyopaque) void {
1187 const t: *Threaded = @ptrCast(@alignCast(userdata));1187 const t: *Threaded = @ptrCast(@alignCast(userdata));
1188 const gpa = t.allocator;1188 const gpa = t.allocator;
11891189
1190 _ = initial_token; // we need to load `token` *after* the group finishes
1191
1190 if (builtin.single_threaded) unreachable; // we never set `group.token` to non-`null`1192 if (builtin.single_threaded) unreachable; // we never set `group.token` to non-`null`
11911193
1192 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);1194 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
...@@ -1195,42 +1197,40 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {...@@ -1195,42 +1197,40 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
1195 assert(prev_state & GroupClosure.sync_is_waiting == 0);1197 assert(prev_state & GroupClosure.sync_is_waiting == 0);
1196 if ((prev_state / GroupClosure.sync_one_pending) > 0) event.wait(ioBasic(t)) catch |err| switch (err) {1198 if ((prev_state / GroupClosure.sync_one_pending) > 0) event.wait(ioBasic(t)) catch |err| switch (err) {
1197 error.Canceled => {1199 error.Canceled => {
1198 var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));1200 var it: ?*std.SinglyLinkedList.Node = @ptrCast(@alignCast(group.token.load(.monotonic)));
1199 while (true) {1201 while (it) |node| : (it = node.next) {
1200 const gc: *GroupClosure = @fieldParentPtr("node", node);1202 const gc: *GroupClosure = @fieldParentPtr("node", node);
1201 gc.closure.requestCancel(t);1203 gc.closure.requestCancel(t);
1202 node = node.next orelse break;
1203 }1204 }
1204 event.waitUncancelable(ioBasic(t));1205 event.waitUncancelable(ioBasic(t));
1205 },1206 },
1206 };1207 };
12071208
1208 var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
1209 while (true) {
1210 const gc: *GroupClosure = @fieldParentPtr("node", node);
1211 const node_next = node.next;
1212 gc.deinit(gpa);
1213 node = node_next orelse break;
1214 }
1215
1216 // Since the group has now finished, it's illegal to add more tasks to it until we return. It's1209 // Since the group has now finished, it's illegal to add more tasks to it until we return. It's
1217 // also illegal for us to race with another `await` or `cancel`. Therefore, we must be the only1210 // also illegal for us to race with another `await` or `cancel`. Therefore, we must be the only
1218 // thread who can access `group` right now.1211 // thread who can access `group` right now.
1212 var it: ?*std.SinglyLinkedList.Node = @ptrCast(@alignCast(group.token.raw));
1219 group.token.raw = null;1213 group.token.raw = null;
1214 while (it) |node| {
1215 it = node.next; // update `it` now, because `deinit` will invalidate `node`
1216 const gc: *GroupClosure = @fieldParentPtr("node", node);
1217 gc.deinit(gpa);
1218 }
1220}1219}
12211220
1222fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {1221fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, initial_token: *anyopaque) void {
1223 const t: *Threaded = @ptrCast(@alignCast(userdata));1222 const t: *Threaded = @ptrCast(@alignCast(userdata));
1224 const gpa = t.allocator;1223 const gpa = t.allocator;
12251224
1225 _ = initial_token; // we need to load `token` *after* the group finishes
1226
1226 if (builtin.single_threaded) unreachable; // we never set `group.token` to non-`null`1227 if (builtin.single_threaded) unreachable; // we never set `group.token` to non-`null`
12271228
1228 {1229 {
1229 var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));1230 var it: ?*std.SinglyLinkedList.Node = @ptrCast(@alignCast(group.token.load(.monotonic)));
1230 while (true) {1231 while (it) |node| : (it = node.next) {
1231 const gc: *GroupClosure = @fieldParentPtr("node", node);1232 const gc: *GroupClosure = @fieldParentPtr("node", node);
1232 gc.closure.requestCancel(t);1233 gc.closure.requestCancel(t);
1233 node = node.next orelse break;
1234 }1234 }
1235 }1235 }
12361236
...@@ -1240,20 +1240,16 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void...@@ -1240,20 +1240,16 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void
1240 assert(prev_state & GroupClosure.sync_is_waiting == 0);1240 assert(prev_state & GroupClosure.sync_is_waiting == 0);
1241 if ((prev_state / GroupClosure.sync_one_pending) > 0) event.waitUncancelable(ioBasic(t));1241 if ((prev_state / GroupClosure.sync_one_pending) > 0) event.waitUncancelable(ioBasic(t));
12421242
1243 {
1244 var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
1245 while (true) {
1246 const gc: *GroupClosure = @fieldParentPtr("node", node);
1247 const node_next = node.next;
1248 gc.deinit(gpa);
1249 node = node_next orelse break;
1250 }
1251 }
1252
1253 // Since the group has now finished, it's illegal to add more tasks to it until we return. It's1243 // Since the group has now finished, it's illegal to add more tasks to it until we return. It's
1254 // also illegal for us to race with another `await` or `cancel`. Therefore, we must be the only1244 // also illegal for us to race with another `await` or `cancel`. Therefore, we must be the only
1255 // thread who can access `group` right now.1245 // thread who can access `group` right now.
1246 var it: ?*std.SinglyLinkedList.Node = @ptrCast(@alignCast(group.token.raw));
1256 group.token.raw = null;1247 group.token.raw = null;
1248 while (it) |node| {
1249 it = node.next; // update `it` now, because `deinit` will invalidate `node`
1250 const gc: *GroupClosure = @fieldParentPtr("node", node);
1251 gc.deinit(gpa);
1252 }
1257}1253}
12581254
1259fn recancel(userdata: ?*anyopaque) void {1255fn recancel(userdata: ?*anyopaque) void {