Implement reference counting of I/O descriptions#822
Merged
Conversation
Collaborator
Author
|
r? @TartanLlama |
This was referenced Jul 15, 2026
TartanLlama
approved these changes
Jul 17, 2026
This commit changes the `descriptor_table.h` internals, headers, etc, to reference-count the entries that are inserted. The goal of this work is to lay the foundation for WebAssembly#807. At this time there's no synchronization around the descriptor table for WASIp2/WASIp3 because there need not be, but with coop threads this will no longer be optional. For example it'll be possible for a thread to call `close` while another thread is blocked in `read` on that, and the result shouldn't be UB such as a trap. At the very least code needs to execute at the start of I/O operations to flag that certain descriptors are in-use, and code needs to run at the end of I/O operations to indicate that the descriptor is no longer in-use. Today that's not possible because descriptors are borrowed raw from the table with no modifications. Reference-counting descriptors is intended to match the underlying implementation on Unix-style operating systems which is intended to enable more natural implementations of eventual semantics for concurrent I/O operations. After this commit concurrent I/O ops are still not safe (e.g. may trap, cause UB, etc), and work will be done in subsequent commits to fix this for I/O subsystems. This commit starts out by adding a reference counter to the start of all I/O descriptions that are allocated. This is managed by `descriptor_table_*` functions with incs/decs and a deallocation when the count reaches 0. The table itself holds a strong reference count to all items, and then whenever something is looked up within the table it additionally gets a strong reference count returned. This is radically different from today's APIs where the `descriptor_table_entry_t*` points directly within the global table. The problem, then, is that all previous callers of `descriptor_table_get_ref` need to get updated to `descriptor_table_get` plus a deallocation routine. Personally I do not have faith in myself manually placing deallocation routines. What I've opted to do here is to raise the Clang baseline for WASIp2/WASIp3 targets to Clang 22. With Clang 22 the `-fdefer-ts` flag is now available and the `defer` keyword can be used, massively simplifying the decrement of the reference count path (and resource cleanup in general). I've not touched WASIp1 here mostly because I just don't want to ruffle feathers. If someone else wants to use `defer` for WASIp1 I think that's fine, but I'm not going to be the one to introduce it. The end result of this commit is that all I/O descriptions have a reference count and nothing borrows directly from the global I/O table itself. All I/O operations have a local strongly-owned copy of the description that they're working on. This now simplifies the problem of concurrent I/O to "figure it out within this object" as opposed to "is your object even valid?". I plan to do further work after this PR to audit various subsystems for modifications to I/O descriptions, add locks where appropriate, etc. Notably the descriptor table itself won't need locks in coop threading mode, just the I/O objects themselves will need internal synchronization/assertions/etc. One note to call out is that testing this behavior is kind of hard. The consequence of a leak (forgotten dec ref) is that nothing happens. Everything generally proceeds as expected, so there's no easy way to write a test that everything's actually cleaned up. I've opted to add a debug-mode-only assertion to libc itself on program exit (hooked via `crt1-command.c`) that closes all descriptors and then asserts that the only live allocations are the stdio descriptors. This is intended to at least work with wasi-libc's test suite for detecting leaks (and caught one during development). A final note is that this additionally opens up the door to implement `dup` on WASIp2/WASIp3 targets. I think this'll be a useful tidbit for POSIX compat (see WebAssembly#802) and will have a mostly trivial implementation after this PR lands.
alexcrichton
force-pushed
the
rc-io-entries
branch
from
July 17, 2026 14:25
873a5e4 to
c0ab937
Compare
alexcrichton
enabled auto-merge (squash)
July 17, 2026 14:25
alexcrichton
added a commit
to alexcrichton/wasi-libc
that referenced
this pull request
Jul 17, 2026
This commit builds on the work of WebAssembly#822 to implement `dup`-style functions. Notably `dup`, `dup2`, `dup3`, and `F_DUPFD{,_CLOEXEC}` are now implemented for wasip2/wasip3 targets. The wasip1 target is left unmodified here. The goal of this commit is to unblock more tests being run in Python upstream where a number rely on these functions. With the reference counting in WebAssembly#822 the functions are relatively easy to implement, although this required refactoring the descriptor table itself to support these operations. Previously the descriptor table allocated in a LIFO manner but this has been replaced with a POSIX-compliant lowest-value-returned instead. Closes WebAssembly#802
alexcrichton
added a commit
to alexcrichton/wasi-libc
that referenced
this pull request
Jul 17, 2026
This commit builds on the work of WebAssembly#822 to implement `dup`-style functions. Notably `dup`, `dup2`, `dup3`, and `F_DUPFD{,_CLOEXEC}` are now implemented for wasip2/wasip3 targets. The wasip1 target is left unmodified here. The goal of this commit is to unblock more tests being run in Python upstream where a number rely on these functions. With the reference counting in WebAssembly#822 the functions are relatively easy to implement, although this required refactoring the descriptor table itself to support these operations. Previously the descriptor table allocated in a LIFO manner but this has been replaced with a POSIX-compliant lowest-value-returned instead. Closes WebAssembly#802
alexcrichton
added a commit
to alexcrichton/wasi-libc
that referenced
this pull request
Jul 17, 2026
This commit builds on the work of WebAssembly#822 to implement `dup`-style functions. Notably `dup`, `dup2`, `dup3`, and `F_DUPFD{,_CLOEXEC}` are now implemented for wasip2/wasip3 targets. The wasip1 target is left unmodified here. The goal of this commit is to unblock more tests being run in Python upstream where a number rely on these functions. With the reference counting in WebAssembly#822 the functions are relatively easy to implement, although this required refactoring the descriptor table itself to support these operations. Previously the descriptor table allocated in a LIFO manner but this has been replaced with a POSIX-compliant lowest-value-returned instead. Closes WebAssembly#802
alexcrichton
added a commit
to alexcrichton/wasi-libc
that referenced
this pull request
Jul 17, 2026
This commit builds on the work of WebAssembly#822 to implement `dup`-style functions. Notably `dup`, `dup2`, `dup3`, and `F_DUPFD{,_CLOEXEC}` are now implemented for wasip2/wasip3 targets. The wasip1 target is left unmodified here. The goal of this commit is to unblock more tests being run in Python upstream where a number rely on these functions. With the reference counting in WebAssembly#822 the functions are relatively easy to implement, although this required refactoring the descriptor table itself to support these operations. Previously the descriptor table allocated in a LIFO manner but this has been replaced with a POSIX-compliant lowest-value-returned instead. Closes WebAssembly#802
alexcrichton
added a commit
that referenced
this pull request
Jul 17, 2026
This commit builds on the work of #822 to implement `dup`-style functions. Notably `dup`, `dup2`, `dup3`, and `F_DUPFD{,_CLOEXEC}` are now implemented for wasip2/wasip3 targets. The wasip1 target is left unmodified here. The goal of this commit is to unblock more tests being run in Python upstream where a number rely on these functions. With the reference counting in #822 the functions are relatively easy to implement, although this required refactoring the descriptor table itself to support these operations. Previously the descriptor table allocated in a LIFO manner but this has been replaced with a POSIX-compliant lowest-value-returned instead. Closes #802 > ~~**Note**: this is built on #822~~
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This commit changes the
descriptor_table.hinternals, headers, etc, to reference-count the entries that are inserted. The goal of this work is to lay the foundation for #807. At this time there's no synchronization around the descriptor table for WASIp2/WASIp3 because there need not be, but with coop threads this will no longer be optional. For example it'll be possible for a thread to callclosewhile another thread is blocked inreadon that, and the result shouldn't be UB such as a trap.At the very least code needs to execute at the start of I/O operations to flag that certain descriptors are in-use, and code needs to run at the end of I/O operations to indicate that the descriptor is no longer in-use. Today that's not possible because descriptors are borrowed raw from the table with no modifications. Reference-counting descriptors is intended to match the underlying implementation on Unix-style operating systems which is intended to enable more natural implementations of eventual semantics for concurrent I/O operations. After this commit concurrent I/O ops are still not safe (e.g. may trap, cause UB, etc), and work will be done in subsequent commits to fix this for I/O subsystems.
This commit starts out by adding a reference counter to the start of all I/O descriptions that are allocated. This is managed by
descriptor_table_*functions with incs/decs and a deallocation when the count reaches 0. The table itself holds a strong reference count to all items, and then whenever something is looked up within the table it additionally gets a strong reference count returned. This is radically different from today's APIs where thedescriptor_table_entry_t*points directly within the global table. The problem, then, is that all previous callers ofdescriptor_table_get_refneed to get updated todescriptor_table_getplus a deallocation routine.Personally I do not have faith in myself manually placing deallocation routines. What I've opted to do here is to raise the Clang baseline for WASIp2/WASIp3 targets to Clang 22. With Clang 22 the
-fdefer-tsflag is now available and thedeferkeyword can be used, massively simplifying the decrement of the reference count path (and resource cleanup in general). I've not touched WASIp1 here mostly because I just don't want to ruffle feathers. If someone else wants to usedeferfor WASIp1 I think that's fine, but I'm not going to be the one to introduce it.The end result of this commit is that all I/O descriptions have a reference count and nothing borrows directly from the global I/O table itself. All I/O operations have a local strongly-owned copy of the description that they're working on. This now simplifies the problem of concurrent I/O to "figure it out within this object" as opposed to "is your object even valid?". I plan to do further work after this PR to audit various subsystems for modifications to I/O descriptions, add locks where appropriate, etc. Notably the descriptor table itself won't need locks in coop threading mode, just the I/O objects themselves will need internal synchronization/assertions/etc.
One note to call out is that testing this behavior is kind of hard. The consequence of a leak (forgotten dec ref) is that nothing happens. Everything generally proceeds as expected, so there's no easy way to write a test that everything's actually cleaned up. I've opted to add a debug-mode-only assertion to libc itself on program exit (hooked via
crt1-command.c) that closes all descriptors and then asserts that the only live allocations are the stdio descriptors. This is intended to at least work with wasi-libc's test suite for detecting leaks (and caught one during development).A final note is that this additionally opens up the door to implement
dupon WASIp2/WASIp3 targets. I think this'll be a useful tidbit for POSIX compat (see #802) and will have a mostly trivial implementation after this PR lands.