diff --git a/Makefile b/Makefile index bb20e46a8..c6752a794 100644 --- a/Makefile +++ b/Makefile @@ -79,10 +79,12 @@ ifeq ($(WASI_SNAPSHOT), preview1) # Omit source files not relevant to WASI Preview 1. As we introduce files # supporting `wasi-sockets` for `wasm32-wasi-preview2`, we'll add those files to # this list. -LIBC_BOTTOM_HALF_OMIT_SOURCES := $(LIBC_BOTTOM_HALF_SOURCES)/preview2.c +LIBC_BOTTOM_HALF_OMIT_SOURCES := \ + $(LIBC_BOTTOM_HALF_SOURCES)/preview2.c \ + $(LIBC_BOTTOM_HALF_SOURCES)/descriptor_table.c LIBC_BOTTOM_HALF_ALL_SOURCES := $(filter-out $(LIBC_BOTTOM_HALF_OMIT_SOURCES),$(LIBC_BOTTOM_HALF_ALL_SOURCES)) -# Omit preview2.h from include-all.c test. -INCLUDE_ALL_CLAUSES := -not -name preview2.h +# Omit preview2-specific headers from include-all.c test. +INCLUDE_ALL_CLAUSES := -not -name preview2.h -not -name descriptor_table.h endif # FIXME(https://reviews.llvm.org/D85567) - due to a bug in LLD the weak diff --git a/expected/wasm32-wasi-preview2/defined-symbols.txt b/expected/wasm32-wasi-preview2/defined-symbols.txt index da4d39107..9c4408c5b 100644 --- a/expected/wasm32-wasi-preview2/defined-symbols.txt +++ b/expected/wasm32-wasi-preview2/defined-symbols.txt @@ -509,6 +509,9 @@ ctanhl ctanl ctime ctime_r +descriptor_table_get_ref +descriptor_table_insert +descriptor_table_remove difftime dirfd dirname diff --git a/expected/wasm32-wasi-preview2/undefined-symbols.txt b/expected/wasm32-wasi-preview2/undefined-symbols.txt index 4a72951d3..b98dc7113 100644 --- a/expected/wasm32-wasi-preview2/undefined-symbols.txt +++ b/expected/wasm32-wasi-preview2/undefined-symbols.txt @@ -65,6 +65,8 @@ __subtf3 __trunctfdf2 __trunctfsf2 __unordtf2 +__wasi_preview1_adapter_close_badfd +__wasi_preview1_adapter_open_badfd __wasm_call_ctors __wasm_import_environment_get_arguments __wasm_import_environment_get_environment diff --git a/libc-bottom-half/headers/private/wasi/descriptor_table.h b/libc-bottom-half/headers/private/wasi/descriptor_table.h new file mode 100644 index 000000000..aeec5bddc --- /dev/null +++ b/libc-bottom-half/headers/private/wasi/descriptor_table.h @@ -0,0 +1,127 @@ +#ifndef DESCRIPTOR_TABLE_H +#define DESCRIPTOR_TABLE_H + +#include + +typedef struct { + int dummy; +} tcp_socket_state_unbound_t; +typedef struct { + int dummy; +} tcp_socket_state_bound_t; +typedef struct { + int dummy; +} tcp_socket_state_connecting_t; +typedef struct { + int dummy; +} tcp_socket_state_listening_t; + +typedef struct { + streams_own_input_stream_t input; + poll_own_pollable_t input_pollable; + streams_own_output_stream_t output; + poll_own_pollable_t output_pollable; +} tcp_socket_state_connected_t; + +typedef struct { + network_error_code_t error_code; +} tcp_socket_state_connect_failed_t; + +// This is a tagged union. When adding/removing/renaming cases, be sure to keep the tag and union definitions in sync. +typedef struct { + enum { + TCP_SOCKET_STATE_UNBOUND, + TCP_SOCKET_STATE_BOUND, + TCP_SOCKET_STATE_CONNECTING, + TCP_SOCKET_STATE_CONNECTED, + TCP_SOCKET_STATE_CONNECT_FAILED, + TCP_SOCKET_STATE_LISTENING, + } tag; + union { + tcp_socket_state_unbound_t unbound; + tcp_socket_state_bound_t bound; + tcp_socket_state_connecting_t connecting; + tcp_socket_state_connected_t connected; + tcp_socket_state_connect_failed_t connect_failed; + tcp_socket_state_listening_t listening; + }; +} tcp_socket_state_t; + +typedef struct { + tcp_own_tcp_socket_t socket; + poll_own_pollable_t socket_pollable; + bool blocking; + bool fake_nodelay; + bool fake_reuseaddr; + network_ip_address_family_t family; + tcp_socket_state_t state; +} tcp_socket_t; + +typedef struct { + udp_own_incoming_datagram_stream_t incoming; + poll_own_pollable_t incoming_pollable; + udp_own_outgoing_datagram_stream_t outgoing; + poll_own_pollable_t outgoing_pollable; +} udp_socket_streams_t; + +typedef struct { + int dummy; +} udp_socket_state_unbound_t; +typedef struct { + int dummy; +} udp_socket_state_bound_nostreams_t; + +typedef struct { + udp_socket_streams_t streams; // Streams have no remote_address +} udp_socket_state_bound_streaming_t; + +typedef struct { + udp_socket_streams_t streams; // Streams have a remote_address +} udp_socket_state_connected_t; + +// This is a tagged union. When adding/removing/renaming cases, be sure to keep the tag and union definitions in sync. +// The "bound" state is split up into two distinct tags: +// - "bound_nostreams": Bound, but no datagram streams set up (yet). That will be done the first time send or recv is called. +// - "bound_streaming": Bound with active streams. +typedef struct { + enum { + UDP_SOCKET_STATE_UNBOUND, + UDP_SOCKET_STATE_BOUND_NOSTREAMS, + UDP_SOCKET_STATE_BOUND_STREAMING, + UDP_SOCKET_STATE_CONNECTED, + } tag; + union { + udp_socket_state_unbound_t unbound; + udp_socket_state_bound_nostreams_t bound_nostreams; + udp_socket_state_bound_streaming_t bound_streaming; + udp_socket_state_connected_t connected; + }; +} udp_socket_state_t; + +typedef struct { + udp_own_udp_socket_t socket; + poll_own_pollable_t socket_pollable; + bool blocking; + network_ip_address_family_t family; + udp_socket_state_t state; +} udp_socket_t; + +// This is a tagged union. When adding/removing/renaming cases, be sure to keep the tag and union definitions in sync. +typedef struct { + enum { + DESCRIPTOR_TABLE_ENTRY_TCP_SOCKET, + DESCRIPTOR_TABLE_ENTRY_UDP_SOCKET, + } tag; + union { + tcp_socket_t tcp_socket; + udp_socket_t udp_socket; + }; +} descriptor_table_entry_t; + +bool descriptor_table_insert(descriptor_table_entry_t entry, int *fd); + +bool descriptor_table_get_ref(int fd, descriptor_table_entry_t **entry); + +bool descriptor_table_remove(int fd, descriptor_table_entry_t *entry); + +#endif diff --git a/libc-bottom-half/sources/descriptor_table.c b/libc-bottom-half/sources/descriptor_table.c new file mode 100644 index 000000000..166bb84df --- /dev/null +++ b/libc-bottom-half/sources/descriptor_table.c @@ -0,0 +1,257 @@ +/* + * This file provides a global hashtable for tracking `wasi-libc`-managed file + * descriptors. + * + * WASI Preview 2 has no notion of file descriptors and instead uses unforgeable + * resource handles (which are currently represented as integers at the ABI + * level, used as indices into per-component tables managed by the host). + * Moreover, there's not necessarily a one-to-one correspondence between POSIX + * file descriptors and resource handles (e.g. a TCP connection may require + * separate handles for reading, writing, and polling the same connection). We + * use this table to map each POSIX descriptor to a set of one or more handles. + * + * As of this writing, we still rely on the WASI Preview 1 adapter + * (https://github.com/bytecodealliance/wasmtime/tree/main/crates/wasi-preview1-component-adapter) + * to manage non-socket descriptors, so currently this table only tracks TCP and + * UDP sockets. We use the adapter's `adapter_open_badfd` and + * `adapter_close_badfd` functions to reserve and later close descriptors to + * avoid confusion (e.g. if an application tries to use Preview 1 host functions + * directly for socket operations rather than go through `wasi-libc`). + * Eventually, we'll switch `wasi-libc` over to Preview 2 entirely, at which + * point we'll no longer need the adapter. At that point, all file descriptors + * will be managed exclusively in this table. + */ + +#include + +#include + +__attribute__((__import_module__("wasi_snapshot_preview1"), + __import_name__("adapter_open_badfd"))) extern int32_t + __wasi_preview1_adapter_open_badfd(int32_t); + +static bool wasi_preview1_adapter_open_badfd(int *fd) +{ + return __wasi_preview1_adapter_open_badfd((int32_t)fd) == 0; +} + +__attribute__((__import_module__("wasi_snapshot_preview1"), + __import_name__("adapter_close_badfd"))) extern int32_t + __wasi_preview1_adapter_close_badfd(int32_t); + +static bool wasi_preview1_adapter_close_badfd(int fd) +{ + return __wasi_preview1_adapter_close_badfd(fd) == 0; +} + +/* + * This hash table is based on the one in musl/src/search/hsearch.c, but uses + * integer keys and supports a `remove` operation. Note that I've switched from + * quadratic to linear probing in order to make `remove` simple and efficient, + * with the tradeoff that clustering is more likely. See also + * https://en.wikipedia.org/wiki/Open_addressing. + */ + +#define MINSIZE 8 +#define MAXSIZE ((size_t)-1 / 2 + 1) + +typedef struct { + bool occupied; + int key; + descriptor_table_entry_t entry; +} descriptor_table_item_t; + +typedef struct { + descriptor_table_item_t *entries; + size_t mask; + size_t used; +} descriptor_table_t; + +static descriptor_table_t global_table = { .entries = NULL, + .mask = 0, + .used = 0 }; + +static size_t keyhash(int key) +{ + // TODO: use a hash function here + return key; +} + +static int resize(size_t nel, descriptor_table_t *table) +{ + size_t newsize; + size_t i; + descriptor_table_item_t *e, *newe; + descriptor_table_item_t *oldtab = table->entries; + descriptor_table_item_t *oldend = table->entries + table->mask + 1; + + if (nel > MAXSIZE) + nel = MAXSIZE; + for (newsize = MINSIZE; newsize < nel; newsize *= 2) + ; + table->entries = calloc(newsize, sizeof *table->entries); + if (!table->entries) { + table->entries = oldtab; + return 0; + } + table->mask = newsize - 1; + if (!oldtab) + return 1; + for (e = oldtab; e < oldend; e++) + if (e->occupied) { + for (i = keyhash(e->key);; ++i) { + newe = table->entries + (i & table->mask); + if (!newe->occupied) + break; + } + *newe = *e; + } + free(oldtab); + return 1; +} + +static descriptor_table_item_t *lookup(int key, size_t hash, + descriptor_table_t *table) +{ + size_t i; + descriptor_table_item_t *e; + + for (i = hash;; ++i) { + e = table->entries + (i & table->mask); + if (!e->occupied || e->key == key) + break; + } + return e; +} + +static bool insert(descriptor_table_entry_t entry, int fd, + descriptor_table_t *table) +{ + if (!table->entries) { + if (!resize(MINSIZE, table)) { + return false; + } + } + + size_t hash = keyhash(fd); + descriptor_table_item_t *e = lookup(fd, hash, table); + + e->entry = entry; + if (!e->occupied) { + e->key = fd; + e->occupied = true; + if (++table->used > table->mask - table->mask / 4) { + if (!resize(2 * table->used, table)) { + table->used--; + e->occupied = false; + return false; + } + } + } + return true; +} + +static bool get(int fd, descriptor_table_entry_t **entry, + descriptor_table_t *table) +{ + if (!table->entries) { + return false; + } + + size_t hash = keyhash(fd); + descriptor_table_item_t *e = lookup(fd, hash, table); + if (e->occupied) { + *entry = &e->entry; + return true; + } else { + return false; + } +} + +static bool remove(int fd, descriptor_table_entry_t *entry, + descriptor_table_t *table) +{ + if (!table->entries) { + return false; + } + + size_t hash = keyhash(fd); + size_t i; + descriptor_table_item_t *e; + for (i = hash;; ++i) { + e = table->entries + (i & table->mask); + if (!e->occupied || e->key == fd) + break; + } + + if (e->occupied) { + *entry = e->entry; + e->occupied = false; + + // Search for any occupied entries which would be lost (due to + // an interrupted linear probe) if we left this one unoccupied + // and move them as necessary. + i = i & table->mask; + size_t j = i; + while (true) { + j = (j + 1) & table->mask; + e = table->entries + j; + if (!e->occupied) + break; + size_t k = keyhash(e->key) & table->mask; + if (i <= j) { + if ((i < k) && (k <= j)) + continue; + } else if ((i < k) || (k <= j)) { + continue; + } + table->entries[i] = *e; + e->occupied = false; + i = j; + } + + // If the load factor has dropped below 25%, shrink the table to + // reduce memory footprint. + if (--table->used < table->mask / 4) { + resize(table->mask / 2, table); + } + + return true; + } else { + return false; + } +} + +bool descriptor_table_insert(descriptor_table_entry_t entry, int *fd) +{ + if (wasi_preview1_adapter_open_badfd(fd)) { + if (insert(entry, *fd, &global_table)) { + return true; + } else { + if (!wasi_preview1_adapter_close_badfd(*fd)) { + abort(); + } + *fd = -1; + return false; + } + } else { + return false; + } +} + +bool descriptor_table_get_ref(int fd, descriptor_table_entry_t **entry) +{ + return get(fd, entry, &global_table); +} + +bool descriptor_table_remove(int fd, descriptor_table_entry_t *entry) +{ + if (remove(fd, entry, &global_table)) { + if (!wasi_preview1_adapter_close_badfd(fd)) { + abort(); + } + return true; + } else { + return false; + } +}