Skip to content

Implement reference counting of I/O descriptions#822

Merged
alexcrichton merged 1 commit into
WebAssembly:mainfrom
alexcrichton:rc-io-entries
Jul 17, 2026
Merged

Implement reference counting of I/O descriptions#822
alexcrichton merged 1 commit into
WebAssembly:mainfrom
alexcrichton:rc-io-entries

Conversation

@alexcrichton

Copy link
Copy Markdown
Collaborator

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 #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 #802) and will have a mostly trivial implementation after this PR lands.

@alexcrichton

Copy link
Copy Markdown
Collaborator Author

r? @TartanLlama

Comment thread libc-bottom-half/headers/private/wasi/descriptor_table.h Outdated
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
alexcrichton enabled auto-merge (squash) July 17, 2026 14:25
@alexcrichton
alexcrichton merged commit d387152 into WebAssembly:main Jul 17, 2026
35 checks passed
@alexcrichton
alexcrichton deleted the rc-io-entries branch July 17, 2026 14:42
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~~
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants