diff --git a/docs/api/bytes.html b/docs/api/bytes.html index 6147d4451..348efda53 100644 --- a/docs/api/bytes.html +++ b/docs/api/bytes.html @@ -324,7 +324,7 @@

from_ptr fn

string_from_ptr fn

string_from_ptr(src, length) -> string

src: ptr, length: int

-

One-call ptr -> string crossing: a fresh refcounted binary string copying `length` bytes from a raw pointer `src`, without an intermediate bytes. The direct answer to "I have a ptr + len, I want a string" (issue #1782).

+

One-call ptr -> string crossing: a fresh refcounted binary string copying `length` bytes from a raw pointer `src`, without an intermediate bytes. The direct answer to "I have a ptr + len, I want a string" (issue #1782). `src` must be a RAW data pointer: `bytes.data(b)` or a C buffer. A bytes handle is also a `ptr` but is refused (panic): for a handle use `bytes.to_string(b, n)`.

view fn

diff --git a/docs/api/http.html b/docs/api/http.html index 1ac2b40ae..b8b90d0ce 100644 --- a/docs/api/http.html +++ b/docs/api/http.html @@ -451,6 +451,7 @@

server_port extern

server_start_raw extern

server_start_raw(server) -> int

server: ptr

+

A start of a server that is already running is refused (-1, the reason in http_server_error).

server_set_host fn

@@ -467,6 +468,7 @@

server_start_background_raw extern<

server_stop extern

server_stop(server)

server: ptr

+

Stop waits until the server's run is over, whichever start began it and on whatever thread, so a free may follow at once. On one of the server's own threads (a handler, the monitor) it only signals, and a free there is fatal: free it from the thread that started it.

server_free extern

@@ -795,7 +797,7 @@

server_start fn

server_start_background fn

-
server_start_background(server) -> string
+
server_start_background(server)

server: ptr

Start a server on a detached native thread. Useful for integration tests and embedding hosts that need multiple local servers in one process without tying up the Aether actor scheduler.

diff --git a/docs/http-server.md b/docs/http-server.md index 57b6caae2..9130815ad 100644 --- a/docs/http-server.md +++ b/docs/http-server.md @@ -913,6 +913,19 @@ any connection; a keep-alive connection waiting for its client's next request, or parked, is closed at once rather than after its idle timeout. +A stop waits for the server's run to be over, whichever start began it +and whichever thread it runs on (the caller's for `server_start`, an +actor's, or the server's own for `server_start_background`): when +`http_server_stop` returns, the accept loop, its workers and the +parking lot are done, so the server can be freed at once, or started +again. Called on one of the server's own threads (a handler, the +monitor), a stop cannot wait for a run that waits for that thread, so +it only signals, and the run ends once the call returns; +`http_server_free` there is a fatal error, since the run still uses +the server: free it from the thread that started it, whose own stop +waits. A start of a server that is already running is refused, with +the reason in `http_server_error`. + --- ## Embedded / background servers @@ -920,10 +933,7 @@ timeout. `http_server_start_background_raw(server)` runs the accept loop on a thread of its own and returns immediately, so a host can keep serving while doing other work (and stop it later with `http_server_stop`). -Stop waits for that thread: when it returns, the accept loop, its -workers and the parking lot are done, so the server can be freed at -once, and another started in the same process. This is the *embedded* -mode and behaves like a library, not a CLI: +This is the *embedded* mode and behaves like a library, not a CLI: - **No banner.** It has no monitor unless the program gives it one, so it prints nothing: an embedded server is controlled by diff --git a/new_changelogs/20261109T185326Z-fixed-http-server-stop-waits-foreground.md b/new_changelogs/20261109T185326Z-fixed-http-server-stop-waits-foreground.md new file mode 100644 index 000000000..7f7703538 --- /dev/null +++ b/new_changelogs/20261109T185326Z-fixed-http-server-stop-waits-foreground.md @@ -0,0 +1,14 @@ +- **`http_server_stop` waits for a foreground server too, so a free after it + is safe.** Stop waited only for a server started with + `http_server_start_background_raw`. A foreground `http_server_start_raw` + run on another thread (an actor's) was signalled and left running, and the + `http_server_free` that followed released it under its accept loop and a + pool worker still serving a keep-alive connection: a segfault in + `conn_recv_request_start` / `http_resolve_route` on macOS and Linux (found + by servirtium-vcr). Now a stop waits until the run is over, whichever start + began it. On one of the server's own threads (a handler, the monitor, an + HTTP/2 dispatch worker, the proxy driver's middleware) it only signals, + since the run waits for that thread: a background server stopped from a + handler used to hang there. `http_server_free` on such a thread while the + server runs is a fatal error rather than a use after free, and a start of + a running server is refused with the reason in `http_server_error`. diff --git a/new_changelogs/20261109T200443Z-fixed-macos-deployment-target-follows-os.md b/new_changelogs/20261109T200443Z-fixed-macos-deployment-target-follows-os.md new file mode 100644 index 000000000..961b54dcc --- /dev/null +++ b/new_changelogs/20261109T200443Z-fixed-macos-deployment-target-follows-os.md @@ -0,0 +1 @@ +- **`ae build` on macOS no longer prints an `ld: warning` for every Homebrew dylib it links.** With `MACOSX_DEPLOYMENT_TARGET` unset, the toolchain targeted the installed SDK's macOS, which can trail the running OS; Homebrew builds its dylibs for the running OS, so each link of a program using nghttp2 (or any newer-built dylib) warned "building for macOS-27.0, but linking with dylib ... built for newer version 27.2", and the warning landed in the output of `ae run`. A native `ae build` now defaults the deployment target to the running OS; a `MACOSX_DEPLOYMENT_TARGET` you set still wins, and cross builds are unchanged. `ae cflags --libs` adds the same `-mmacosx-version-min=` for a build that drives its own compiler. diff --git a/new_changelogs/20261109T200545Z-fixed-bytes-string-from-ptr-handle.md b/new_changelogs/20261109T200545Z-fixed-bytes-string-from-ptr-handle.md new file mode 100644 index 000000000..52459e8b8 --- /dev/null +++ b/new_changelogs/20261109T200545Z-fixed-bytes-string-from-ptr-handle.md @@ -0,0 +1 @@ +- **`bytes.string_from_ptr` refuses a bytes handle instead of copying its header** (`std/bytes/aether_bytes.c`, `std/bytes/module.ae`). `string_from_ptr(src, n)` wants a raw data pointer (`bytes.data(b)` or a C buffer), but a bytes handle is also a `ptr`, so `bytes.string_from_ptr(b, 4)` compiled and silently returned the `AetherBytes` struct header as text (it produced garbage keys in servirtium-vcr's Azure Key Vault test). `AetherBytes` now carries an 8-byte tag (cleared on free) and `string_from_ptr` panics on a live handle with a message naming `bytes.to_string(b, n)` and `bytes.data(b)`; the check reads one byte of a raw buffer unless the tag matches, so short C buffers are never over-read. The module comment, `std/bytes/README.md` and `docs/api` say `src` must be a raw data pointer. Test: `tests/regression/test_bytes_string_from_ptr_handle.ae` (the handle is refused; `to_string` and `string_from_ptr(bytes.data(b), n)` stay binary-safe over NUL and 0xff). diff --git a/std/bytes/README.md b/std/bytes/README.md index d80dedcf5..1c3e2dff5 100644 --- a/std/bytes/README.md +++ b/std/bytes/README.md @@ -42,7 +42,7 @@ There is deliberately no `ptr as string` cast (they are different types). ```aether,fragment // ptr -> string (I have a raw buffer + length, I want a binary string) -s = bytes.string_from_ptr(p, n) +s = bytes.string_from_ptr(p, n) // p: bytes.data(b) or a C buffer, NOT a bytes handle // string -> ptr (I have a string, I want a raw pointer to walk) n = string.length(s) @@ -52,6 +52,12 @@ p = bytes.data(b) // ... use p ... (keep `b` alive as long as you use `p`) ``` +`src` must be a raw data pointer. A bytes handle is also a `ptr`, so the +compiler cannot tell the two apart, but `string_from_ptr` recognises a handle +and panics rather than copy its header as text. To turn a handle into a +string use `bytes.to_string(b, n)`; to use the handle's payload as a raw +pointer, `bytes.string_from_ptr(bytes.data(b), n)`. + The longhand still works and is useful when you build a buffer incrementally: ```aether,fragment diff --git a/std/bytes/aether_bytes.c b/std/bytes/aether_bytes.c index f65f465b8..d0fdb1733 100644 --- a/std/bytes/aether_bytes.c +++ b/std/bytes/aether_bytes.c @@ -2,6 +2,7 @@ #include "../string/aether_string.h" #include "../../runtime/aether_resource_caps.h" #include "../../runtime/utils/aether_wipe.h" +#include "../../runtime/actors/aether_panic.h" #include #include @@ -16,11 +17,33 @@ * AetherBytes would need length == 0xAE57C0DE (~2.9GB) to collide, and the * header plausibility checks inside is_aether_string reject that too. */ struct AetherBytes { + /* Handle tag, so a bytes HANDLE handed to aether_bytes_string_from_ptr + * (which wants the raw data pointer) can be told from a data pointer. + * Cleared on free. */ + unsigned char tag[8]; size_t length; size_t capacity; char* data; }; +static const unsigned char BYTES_TAG[8] = {0xAE, 'B', 'Y', 'T', 'E', 'S', 0x01, 0xFF}; + +/* Is `p` a live AetherBytes handle? Compares byte by byte with a short + * circuit, so a raw buffer whose first byte differs (the near-certain case) is + * read for exactly one byte and a short allocation is never over-read. A full + * tag match is then checked for a plausible header. */ +static int is_aether_bytes(const void* p) { + if (!p) return 0; + const unsigned char* q = (const unsigned char*)p; + for (int i = 0; i < 8; i++) { + if (q[i] != BYTES_TAG[i]) return 0; + } + const AetherBytes* b = (const AetherBytes*)p; + if (b->capacity < b->length) return 0; + if (b->capacity > 0 && !b->data) return 0; + return 1; +} + /* Grow the underlying buffer so it has at least `min_capacity` bytes. * Doubles each time to keep amortised-O(1) append cost; falls back to * `min_capacity` itself once doubling stops being enough. Returns 1 @@ -59,6 +82,7 @@ AetherBytes* aether_bytes_new(int initial_capacity) { if (initial_capacity < 0) return NULL; AetherBytes* b = (AetherBytes*)aether_caps_malloc(sizeof(AetherBytes)); if (!b) return NULL; + memcpy(b->tag, BYTES_TAG, sizeof b->tag); b->length = 0; b->capacity = 0; b->data = NULL; @@ -92,6 +116,13 @@ AetherBytes* aether_bytes_from_ptr(void* src, int length) { * I want a length-aware binary string". */ void* aether_bytes_string_from_ptr(void* src, int length) { if (length < 0) return NULL; + /* `src` is the raw DATA pointer (bytes.data(b), or a C buffer). A bytes + * handle is also a `ptr`, so the type system cannot tell; copying it would + * yield the struct header as text. Refuse it loudly (the std style for a + * misuse that has no failure value to return: aether_panic). */ + if (is_aether_bytes(src)) + aether_panic("bytes.string_from_ptr: `src` is a bytes handle, not a raw data pointer; " + "use bytes.to_string(b, n), or pass bytes.data(b)"); if (length == 0 || !src) return (void*)string_new_with_length("", 0); return (void*)string_new_with_length((const char*)src, (size_t)length); } @@ -470,6 +501,7 @@ void aether_bytes_free(AetherBytes* b) { * so the resource accounting balances the aether_caps_malloc / * aether_caps_realloc above. */ aether_caps_free(b->data, b->capacity); + memset(b->tag, 0, sizeof b->tag); aether_caps_free(b, sizeof(AetherBytes)); } diff --git a/std/bytes/module.ae b/std/bytes/module.ae index e6aa8d579..24bfce92e 100644 --- a/std/bytes/module.ae +++ b/std/bytes/module.ae @@ -160,6 +160,9 @@ from_ptr(src: ptr, length: int) -> ptr { // One-call ptr -> string crossing: a fresh refcounted binary string copying // `length` bytes from a raw pointer `src`, without an intermediate bytes. // The direct answer to "I have a ptr + len, I want a string" (issue #1782). +// `src` must be a RAW data pointer: `bytes.data(b)` or a C buffer. A bytes +// handle is also a `ptr` but is refused (panic): for a handle use +// `bytes.to_string(b, n)`. string_from_ptr(src: ptr, length: int) -> string { return aether_bytes_string_from_ptr(src, length) } diff --git a/std/http/module.ae b/std/http/module.ae index 858f86db1..a0be569aa 100644 --- a/std/http/module.ae +++ b/std/http/module.ae @@ -274,8 +274,14 @@ extern http_server_port(server: ptr) -> int // loopback binds. A host name binds the IPv4 address it resolves to, // and one that does not resolve fails the bind (#2639). extern http_server_set_host(server: ptr, host: string) +// A start of a server that is already running is refused (-1, the reason in +// http_server_error). extern http_server_start_raw(server: ptr) -> int extern http_server_start_background_raw(server: ptr) -> int +// Stop waits until the server's run is over, whichever start began it and on +// whatever thread, so a free may follow at once. On one of the server's own +// threads (a handler, the monitor) it only signals, and a free there is fatal: +// free it from the thread that started it. extern http_server_stop(server: ptr) extern http_server_free(server: @frees ptr) // TLS termination (#260 Tier 0). Loads the cert+key (PEM-encoded); @@ -872,7 +878,7 @@ server_start(server: ptr) -> { server_start_background(server: ptr) -> { rc = http_server_start_background_raw(server) if rc < 0 { - return "server background start failed" + return server_error_or(server, "server background start failed") } return "" } diff --git a/std/http/server/h2/aether_h2.c b/std/http/server/h2/aether_h2.c index e425209b0..203181130 100644 --- a/std/http/server/h2/aether_h2.c +++ b/std/http/server/h2/aether_h2.c @@ -604,8 +604,12 @@ static void h2_task_run(void* arg) { /* Route table + middleware are read-only after startup, so running the * handler here (off the connection thread) is safe across any number of * sessions; nghttp2_session is NEVER touched here. Each task owns its - * own (req, res). */ + * own (req, res). The worker is one of the server's own threads for the + * handler: its connection thread waits for this task, so a stop from the + * handler must not wait for the run. */ + http_server_thread_enter(sess->server); http_server_dispatch_for_h2(sess->server, task->req, task->res); + http_server_thread_leave(sess->server); /* Push to the session's ready queue + wake its connection thread. The * session's in_flight counter (bumped before submit) keeps `sess` alive diff --git a/std/net/aether_http_evloop.c b/std/net/aether_http_evloop.c index 50434157f..168fb1e85 100644 --- a/std/net/aether_http_evloop.c +++ b/std/net/aether_http_evloop.c @@ -1991,6 +1991,10 @@ static void* ev_driver_main(void* arg) { EvDriver* d = (EvDriver*)arg; AetherIoEvent events[64]; + /* One of the server's own threads while the run lasts: the middleware + * ahead of the proxy runs here, and one that stops the server must not + * wait for the run, which joins this thread. */ + http_server_thread_enter(d->loop->server); aether_io_poller_add(&d->poller, d->wake_r, NULL, AETHER_IO_READ); while (!atomic_load(&d->loop->stopping)) { @@ -2041,6 +2045,7 @@ static void* ev_driver_main(void* arg) { EvConn* c = d->by_fd[fd]; if (c && c->client_fd == fd) ev_conn_close(d, c); } + http_server_thread_leave(d->loop->server); return NULL; } diff --git a/std/net/aether_http_pool.c b/std/net/aether_http_pool.c index 29c62a3f7..6d2e2e690 100644 --- a/std/net/aether_http_pool.c +++ b/std/net/aether_http_pool.c @@ -130,6 +130,10 @@ int http_pool_default_workers(void) { static void* http_pool_worker(void* arg) { HttpConnectionPool* pool = (HttpConnectionPool*)arg; + /* One of the server's own threads while the run lasts: a handler here + * that stops the server must not wait for the run, which joins this + * thread. */ + http_server_thread_enter(pool->server); while (1) { pthread_mutex_lock(&pool->lock); while (pool->count == 0 && !pool->shutdown) { @@ -151,6 +155,7 @@ static void* http_pool_worker(void* arg) { else http_server_drain_connection(pool->server, item.fd); atomic_fetch_sub(&http_pool_busy_workers, 1); } + http_server_thread_leave(pool->server); return NULL; } diff --git a/std/net/aether_http_server.c b/std/net/aether_http_server.c index 3619ffda3..8241c774f 100644 --- a/std/net/aether_http_server.c +++ b/std/net/aether_http_server.c @@ -149,6 +149,8 @@ const char* http_server_set_keepalive_raw(HttpServer* s, int e, int m, int64_t i (void)s; (void)e; (void)m; (void)i; return "keep-alive unavailable: networking not built in"; } void http_server_drain_connection(HttpServer* s, int fd) { (void)s; (void)fd; } +void http_server_thread_enter(HttpServer* s) { (void)s; } +void http_server_thread_leave(HttpServer* s) { (void)s; } const char* http_server_shutdown_graceful_raw(HttpServer* s, int64_t t) { (void)s; (void)t; return ""; } const char* http_server_set_health_probes_raw(HttpServer* s, const char* lp, const char* rp, HttpReadyCheck rc, void* ud) { @@ -1486,6 +1488,12 @@ HttpServer* http_server_create(int port) { pthread_mutex_init(&server->live_lock, NULL); pthread_mutex_init(&server->tls_lock, NULL); pthread_mutex_init(&server->tls_config_lock, NULL); + pthread_mutex_init(&server->run_lock, NULL); + pthread_cond_init(&server->run_done, NULL); + server->serving = 0; + server->server_tids = NULL; + server->server_tid_count = 0; + server->server_tid_cap = 0; server->live_fds = NULL; server->live_count = 0; server->live_cap = 0; @@ -7521,22 +7529,94 @@ static void* accept_thread_fn(void* arg) { static int http_server_run(HttpServer* server); static int http_server_serve(HttpServer* server); -int http_server_start_raw(HttpServer* server) { +/* The thread is one of the server's own for the run (see server_tids). A + * thread that could not say so would have a stop on it wait for itself, so + * running out of memory here is fatal rather than quietly skipped. */ +void http_server_thread_enter(HttpServer* server) { + pthread_mutex_lock(&server->run_lock); + if (server->server_tid_count == server->server_tid_cap) { + int cap = server->server_tid_cap ? server->server_tid_cap * 2 : 16; + aether_tid_t* grown = realloc(server->server_tids, (size_t)cap * sizeof(aether_tid_t)); + if (!grown) { + aether_fatal_flush(); /* the program's lines first (#3435, aether_fatal.h) */ + fprintf(stderr, "aether: out of memory recording an HTTP server thread\n"); /* silent-ok: last line before abort */ + abort(); + } + server->server_tids = grown; + server->server_tid_cap = cap; + } + server->server_tids[server->server_tid_count++] = aether_tid_self(); + pthread_mutex_unlock(&server->run_lock); +} + +void http_server_thread_leave(HttpServer* server) { + aether_tid_t self = aether_tid_self(); + pthread_mutex_lock(&server->run_lock); + for (int i = 0; i < server->server_tid_count; i++) { + if (aether_tid_equal(server->server_tids[i], self)) { + server->server_tids[i] = server->server_tids[--server->server_tid_count]; + break; + } + } + pthread_mutex_unlock(&server->run_lock); +} + +/* Whether the calling thread is one of the server's own; run_lock held. */ +static int server_thread_is_own_locked(HttpServer* server) { + aether_tid_t self = aether_tid_self(); + for (int i = 0; i < server->server_tid_count; i++) { + if (aether_tid_equal(server->server_tids[i], self)) return 1; + } + return 0; +} + +/* Begin a run, from either start: refused while one is on, so a second start + * cannot run a server twice over the same socket, pool and lists. */ +static int server_run_begin(HttpServer* server) { + pthread_mutex_lock(&server->run_lock); + if (server->serving) { + pthread_mutex_unlock(&server->run_lock); + return server_fail(server, "the server is already running"); + } + server->serving = 1; server->error[0] = '\0'; server->is_running = 1; atomic_store(&server->stopping, 0); + pthread_mutex_unlock(&server->run_lock); + return 0; +} + +/* The run is over: a stop waiting for it may return, and the server may be + * freed from here on, so the calling thread touches it no more after this. */ +static void server_run_end(HttpServer* server) { + pthread_mutex_lock(&server->run_lock); + server->serving = 0; + pthread_cond_broadcast(&server->run_done); + pthread_mutex_unlock(&server->run_lock); +} + +int http_server_start_raw(HttpServer* server) { + if (!server) return -1; + if (server_run_begin(server) < 0) return -1; return http_server_run(server); } -/* The server's body, with is_running already set by whoever started it: - * http_server_start_raw on the calling thread, or +/* The server's body, on the thread that runs it, with the run already begun + * by whoever started it: http_server_start_raw on the calling thread, or * http_server_start_background_raw before it spawns the thread. Never here, * on the server's own thread: a stop issued between the spawn and this * thread's first line was then overwritten by its `is_running = 1`, the loop - * ran forever, and http_server_stop, which joins the thread (#2672), hung. */ + * ran forever, and http_server_stop, which waits for the run (#2672), hung. + * A stop that came first has set stopping, and wins. */ static int http_server_run(HttpServer* server) { - int rc = http_server_serve(server); - monitor_stopped(server); + int rc = 0; + http_server_thread_enter(server); + if (!atomic_load(&server->stopping)) { + rc = http_server_serve(server); + monitor_stopped(server); + } + http_server_thread_leave(server); + server_run_end(server); return rc; } @@ -7813,10 +7893,22 @@ static int http_server_serve(HttpServer* server) { return 0; } +#if AETHER_HAS_THREADS +/* Join the background thread of a run that is over, if one is left: taken + * under run_lock, so of two callers one joins it, and joined outside it, + * since the thread ends its run under it. */ +static void server_join_background(HttpServer* server) { + pthread_mutex_lock(&server->run_lock); + int joinable = server->background_joinable; + pthread_t thread = server->background_thread; + server->background_joinable = 0; + pthread_mutex_unlock(&server->run_lock); + if (joinable) pthread_join(thread, NULL); +} +#endif + static void* http_server_background_main(void* arg) { HttpServer* server = (HttpServer*)arg; - server->background_tid = aether_tid_self(); - server->background_tid_set = 1; #if !defined(_WIN32) /* Embedded/background server: block async signals on this thread and * every thread it spawns (the accept thread + pool workers inherit @@ -7839,9 +7931,7 @@ static void* http_server_background_main(void* arg) { sigdelset(&block, SIGILL); pthread_sigmask(SIG_BLOCK, &block, NULL); #endif - /* is_running was set before this thread was spawned; a stop that came - * first has already cleared it and set stopping, and must win. */ - if (!atomic_load(&server->stopping)) http_server_run(server); + http_server_run(server); return NULL; } @@ -7856,21 +7946,26 @@ int http_server_start_background_raw(HttpServer* server) { (void)http_server_background_main; return -1; #else - /* Mark embedded mode before the thread starts so http_server_start_raw - * suppresses the interactive "Press Ctrl+C to stop" banner. */ - server->background = 1; - server->background_tid_set = 0; /* Running from here, not from the thread's first line: a stop issued as * soon as this returns must find a running server to stop, and nothing * the thread does later may undo it (see http_server_run). */ - server->is_running = 1; - atomic_store(&server->stopping, 0); - if (pthread_create(&server->background_thread, NULL, http_server_background_main, server) != 0) { + if (server_run_begin(server) < 0) return -1; + /* A thread left from an earlier run whose stop came from one of its own + * threads (a handler), which could not join it; that run is over. */ + server_join_background(server); + /* Embedded mode, set before the thread starts: no interactive banner. */ + server->background = 1; + pthread_mutex_lock(&server->run_lock); + int created = pthread_create(&server->background_thread, NULL, + http_server_background_main, server) == 0; + server->background_joinable = created; /* joined by http_server_stop */ + pthread_mutex_unlock(&server->run_lock); + if (!created) { server->background = 0; server->is_running = 0; - return -1; + server_run_end(server); + return server_fail(server, "cannot start the server thread"); } - server->background_joinable = 1; /* joined by http_server_stop */ return 0; #endif } @@ -7937,17 +8032,23 @@ void http_server_stop(HttpServer* server) { } pthread_mutex_unlock(&server->live_lock); -#if AETHER_HAS_THREADS - /* A background server is stopped when its thread is done: the closed - * socket ends its accept loop (at most its one-second poll on POSIX), - * and the loop shuts its pool down before it returns. Not from the - * thread itself, as when a handler stops the server it runs in. */ - if (server->background_joinable && - !(server->background_tid_set && - aether_tid_equal(aether_tid_self(), server->background_tid))) { - server->background_joinable = 0; - pthread_join(server->background_thread, NULL); + /* Wait for the run to be over, whichever start began it and on whatever + * thread: the closed socket and the wake pipe end its accept loop, and + * the loop joins its workers, which finish the request each is in, before + * it returns. Not on one of the run's own threads (a handler, the + * monitor), which the run waits for in turn: there the stop only + * signals, and the run ends once this call has returned. */ + pthread_mutex_lock(&server->run_lock); + int own = server_thread_is_own_locked(server); + if (!own) { + while (server->serving) pthread_cond_wait(&server->run_done, &server->run_lock); } + pthread_mutex_unlock(&server->run_lock); + +#if AETHER_HAS_THREADS + /* A background run's thread, once its run is over: it does nothing + * after ending it, so this is quick. */ + if (!own) server_join_background(server); #endif } @@ -7955,6 +8056,18 @@ void http_server_free(HttpServer* server) { if (!server) return; http_server_stop(server); + /* Still running only when this is one of the run's own threads, where + * the stop could not wait: the run is still using the server. */ + pthread_mutex_lock(&server->run_lock); + int running = server->serving; + pthread_mutex_unlock(&server->run_lock); + if (running) { + aether_fatal_flush(); /* the program's lines first (#3435, aether_fatal.h) */ + fprintf(stderr, "aether: http_server_free called on one of the server's own threads " /* silent-ok: last line before abort */ + "(a handler or its monitor) while it runs; stop it there and free it " + "from the thread that started it\n"); + abort(); + } http_server_monitor_null_raw(server); /* gives back the closures' environments */ @@ -8037,6 +8150,9 @@ void http_server_free(HttpServer* server) { #endif free(server->live_fds); + free(server->server_tids); + pthread_cond_destroy(&server->run_done); + pthread_mutex_destroy(&server->run_lock); pthread_mutex_destroy(&server->live_lock); pthread_mutex_destroy(&server->tls_lock); pthread_mutex_destroy(&server->tls_config_lock); diff --git a/std/net/aether_http_server.h b/std/net/aether_http_server.h index 06c01953b..606a6b476 100644 --- a/std/net/aether_http_server.h +++ b/std/net/aether_http_server.h @@ -349,16 +349,30 @@ typedef struct HttpServer { // intercept a process-directed signal meant for the host application. // std-http-server-background-sigurg-poisons-harness.md int background; - // The background server's thread, which http_server_stop joins so the - // accept loop and its workers are done before stop returns: left - // detached, they were still in Winsock calls when the program went on - // to exit (an access violation on Windows). background_tid is set by - // the thread itself, so a stop called from a handler does not join - // the thread it runs on. + // The background server's thread, which http_server_stop joins once the + // run is over (see `serving`): left detached, it was still in Winsock + // calls when the program went on to exit (an access violation on + // Windows). pthread_t background_thread; int background_joinable; - int background_tid_set; - aether_tid_t background_tid; + // The run, whichever start began it. `serving` is set by the start, + // under run_lock, and cleared once the run is over: its accept loop has + // returned and its workers, event loop and parking lot are joined and + // freed; run_done is broadcast then. http_server_stop waits for it, so + // when stop returns nothing of the server is running on any thread and + // http_server_free may free it. A foreground start on another thread (an + // actor's) used to be signalled but not waited for, and the free that + // followed released the server under a worker still serving a keep-alive + // connection. server_tids are the threads the run owns (the one running + // it, its pool workers, the event loop's drivers): a stop called on one + // of them, from a handler or the monitor, cannot wait for a run that + // waits for it, so it only signals, and a free there is refused. + pthread_mutex_t run_lock; + pthread_cond_t run_done; + int serving; + aether_tid_t* server_tids; + int server_tid_count; + int server_tid_cap; // Multi-accept: one accept thread per core with SO_REUSEPORT (opt-in) int multi_accept; // 0 = single accept (default), 1 = SO_REUSEPORT multi-accept int accept_thread_count; @@ -434,10 +448,26 @@ int http_server_has_monitor(HttpServer* server); // that does not resolve fails the bind (#2639). No-op if `host` is NULL // or empty. void http_server_set_host(HttpServer* server, const char* host); +// Start the server on the calling thread (returns when it stops) or on a +// thread of its own (returns at once). Either refuses, -1 with the reason in +// http_server_error, a server that is already running. int http_server_start_raw(HttpServer* server); int http_server_start_background_raw(HttpServer* server); +// Stop the server and wait until its run is over, whichever start began it +// and whichever thread it runs on: when stop returns, its accept loop and +// every worker are done, and the server may be freed or started again. +// Called on one of the server's own threads (a handler, the monitor), it +// only signals: the run ends once that call returns. void http_server_stop(HttpServer* server); +// Stop (as above) and free the server. Fatal on one of the server's own +// threads while it runs, since the run still uses it: stop it there and +// free it from the thread that started it. void http_server_free(HttpServer* server); +// The run's threads say they are the server's, for the duration of the +// run, so a stop on them does not wait for itself. Internal to std.net: +// the pool workers and the event loop's drivers call them. +void http_server_thread_enter(HttpServer* server); +void http_server_thread_leave(HttpServer* server); // Enable HTTP/1.1 keep-alive on this server (#260 Tier 0). When // `enabled` is non-zero, the connection handler reads multiple diff --git a/tests/ae_sweep_prune.txt b/tests/ae_sweep_prune.txt index fd4186d66..3ca208bff 100644 --- a/tests/ae_sweep_prune.txt +++ b/tests/ae_sweep_prune.txt @@ -175,6 +175,7 @@ tests/integration/http_serve_and_dial/ tests/integration/http_server_actor_dispatch/ tests/integration/http_server_background_quiet/ tests/integration/http_server_monitor/ +tests/integration/http_server_stop_waits/ tests/integration/http_server_multi_accept/ tests/integration/http_server_connect_tunnel/ tests/integration/http_server_h2_middleware/ diff --git a/tests/integration/http_server_monitor/test_http_server_monitor.sh b/tests/integration/http_server_monitor/test_http_server_monitor.sh index bad27809a..0d3f2dc88 100755 --- a/tests/integration/http_server_monitor/test_http_server_monitor.sh +++ b/tests/integration/http_server_monitor/test_http_server_monitor.sh @@ -11,7 +11,16 @@ AE="$ROOT/build/ae" fail() { echo " [FAIL] http_server_monitor: $1"; exit 1; } -run() { MONITOR_MODE="$1" AETHER_HOME="$ROOT" "$AE" run "$SCRIPT_DIR/$2" 2>&1; } +# Build, then run: the build's output (a linker warning about a Homebrew dylib, +# say) is not the program's, and only the program's is under test. A build +# that fails shows its own output. +BUILD_DIR="$(mktemp -d)"; trap 'rm -rf "$BUILD_DIR"' EXIT +run() { + _exe="$BUILD_DIR/$(basename "$2" .ae)" + AETHER_HOME="$ROOT" "$AE" build "$SCRIPT_DIR/$2" -o "$_exe" >"$BUILD_DIR/build.log" 2>&1 \ + || { cat "$BUILD_DIR/build.log"; return 1; } + MONITOR_MODE="$1" "$_exe" 2>&1 +} OUT="$(run - monitor.ae)" || { echo "$OUT"; fail "the monitor program failed"; } [ "$OUT" = "MONITOR-OK" ] || { echo "$OUT"; fail "a background server or a failed bind printed something"; } @@ -34,7 +43,7 @@ FG-DONE" case "$(uname -s 2>/dev/null)" in MINGW*|MSYS*|CYGWIN*|Windows_NT) ;; *) - TMPDIR="$(mktemp -d)"; trap 'rm -rf "$TMPDIR"' EXIT + TMPDIR="$BUILD_DIR" AETHER_HOME="$ROOT" "$AE" build "$SCRIPT_DIR/actor_mode.ae" -o "$TMPDIR/actor_mode" \ --extra "$SCRIPT_DIR/actor_shim.c" >"$TMPDIR/build.log" 2>&1 \ || { cat "$TMPDIR/build.log"; fail "the actor-mode program did not build"; } diff --git a/tests/integration/http_server_stop_waits/foreground_actor.ae b/tests/integration/http_server_stop_waits/foreground_actor.ae new file mode 100644 index 000000000..d6f973dd1 --- /dev/null +++ b/tests/integration/http_server_stop_waits/foreground_actor.ae @@ -0,0 +1,120 @@ +// http_server_stop_waits: a foreground server (http.server_start, so +// http_server_start_raw) run by an actor, so on another thread, with a +// keep-alive connection held open across the stop, then freed at once, round +// after round. +// +// http_server_stop used to wait only for a server started in the +// background. For a foreground one on another thread it signalled and +// returned, and the free that followed released the server while its accept +// loop and a pool worker, woken from waiting on the held connection, were +// still in it: a segfault in the worker (conn_recv_request_start, +// http_resolve_route) on macOS and on Linux. Now stop waits for the run, +// whichever start began it, so the server's monitor has reported stopped by +// the time stop returns, which each round checks, and the free is safe. + +import std.http +import std.os +import std.fs +import std.string +import std.tcp + +extern http_server_stop(server: ptr) +extern http_server_free(server: ptr) +extern http_server_port(server: ptr) -> int +extern sleep(ms: int) +extern exit(code: int) + +message Run { srv: ptr } +actor Runner { + state runs = 0 + receive { + Run(srv) -> { + runs = runs + 1 + http.server_start(srv) + } + } +} + +handler(req: ptr, res: ptr, ud: ptr) { + http.response_set_status(res, 200) + http.response_set_body(res, "ok") +} + +// One request on a connection the caller keeps: the server holds it open +// for the next request, so a pool worker waits on it. "" when the answer +// came back, else what went wrong. +ask(sock: ptr) -> string { + tcp.write(sock, "GET /ok HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n") + wire = "" + waited = 0 + while string.index_of(wire, "\r\n\r\nok") < 0 && waited < 50 { + ready, _perr = tcp.poll(sock, 100) + if ready { + chunk, rerr = tcp.read(sock, 4096) + if rerr != "" || string.length(chunk) == 0 { + return "connection closed before the answer" + } + wire = string.concat(wire, chunk) + } + waited = waited + 1 + } + if string.index_of(wire, "HTTP/1.1 200") != 0 { + return "no 200 answer" + } + return "" +} + +main() { + dir = os.getenv("STOP_WAITS_DIR") + if dir == "" { + dir = "." + } + rounds = 10 + i = 0 + while i < rounds { + marker = "${dir}/stopped-${i}" + fs.delete(marker) + s = http.server_create(0) + http.server_set_host(s, "127.0.0.1") + http.server_get(s, "/ok", handler, null) + http.monitor(s) { + listening() | host: string, port: int | {} + stopped() | host: string, port: int | { + fs.write(marker, "stopped") + } + } + if http.server_bind(s, "127.0.0.1", 0) != "" { + println("FAIL round ${i}: bind") + exit(1) + } + port = http_server_port(s) + runner = spawn(Runner()) + runner ! Run { srv: s } + + // The actor may not have started the server yet: the socket is + // bound, so the connect is queued and answered once it has. + sock, cerr = tcp.connect("127.0.0.1", port) + if cerr != "" { + println("FAIL round ${i}: connect: ${cerr}") + exit(1) + } + why = ask(sock) + if why != "" { + println("FAIL round ${i}: ${why}") + exit(1) + } + + // The connection stays open across the stop and the free. + http_server_stop(s) + if fs.exists(marker) == 0 { + println("FAIL round ${i}: http_server_stop returned before the server stopped") + exit(1) + } + http_server_free(s) + sleep(50) + tcp.close(sock) + fs.delete(marker) + i = i + 1 + } + println("FOREGROUND-OK ${rounds}") +} diff --git a/tests/integration/http_server_stop_waits/from_handler.ae b/tests/integration/http_server_stop_waits/from_handler.ae new file mode 100644 index 000000000..e0629c811 --- /dev/null +++ b/tests/integration/http_server_stop_waits/from_handler.ae @@ -0,0 +1,97 @@ +// http_server_stop_waits: a stop from a handler, which runs on one of the +// server's own pool workers, and the start that is refused while a server +// runs. +// +// A stop waits for the server's run to be over, but the run joins its +// workers, so on one of them it only signals; the thread that started the +// server, or any other, then stops (waiting) and frees it. A background +// server stopped from a handler used to hang: the stop joined the server +// thread, which was joining the worker the stop ran on. + +import std.http +import std.string + +extern http_server_port(server: ptr) -> int +extern http_server_free(server: ptr) +extern exit(code: int) + +message Run { srv: ptr } +actor Runner { + state runs = 0 + receive { + Run(srv) -> { + runs = runs + 1 + http.server_start(srv) + } + } +} + +stop_handler(req: ptr, res: ptr, ud: ptr) { + http.response_set_status(res, 200) + http.response_set_body(res, "stopping") + http.server_stop(ud) +} + +// A server with /stop, on 127.0.0.1 and a port the kernel picks, bound. +make_server() -> ptr { + s = http.server_create(0) + http.server_set_host(s, "127.0.0.1") + http.null_monitor(s) + http.server_get(s, "/stop", stop_handler, s) + if http.server_bind(s, "127.0.0.1", 0) != "" { + println("FAIL: bind") + exit(1) + } + return s +} + +ask_stop(s: ptr, what: string) { + port = http_server_port(s) + body, err = http.get("http://127.0.0.1:${port}/stop") + if err != "" || body != "stopping" { + println("FAIL ${what}: /stop answered '${body}' (${err})") + exit(1) + } +} + +main() { + i = 0 + while i < 5 { + // Foreground, on an actor. + s = make_server() + runner = spawn(Runner()) + runner ! Run { srv: s } + ask_stop(s, "foreground") + http_server_free(s) + + // Background. + b = make_server() + if http.server_start_background(b) != "" { + println("FAIL: start_background") + exit(1) + } + ask_stop(b, "background") + http_server_free(b) + i = i + 1 + } + + // A start while the server runs is refused, whichever start it is. + r = make_server() + if http.server_start_background(r) != "" { + println("FAIL: start_background") + exit(1) + } + again = http.server_start_background(r) + if string.index_of(again, "already running") < 0 { + println("FAIL: a second background start was not refused: '${again}'") + exit(1) + } + fg = http.server_start(r) + if string.index_of(fg, "already running") < 0 { + println("FAIL: a foreground start of a running server was not refused: '${fg}'") + exit(1) + } + ask_stop(r, "refused-start server") + http_server_free(r) + println("FROM-HANDLER-OK") +} diff --git a/tests/integration/http_server_stop_waits/test_http_server_stop_waits.sh b/tests/integration/http_server_stop_waits/test_http_server_stop_waits.sh new file mode 100755 index 000000000..450cc5b12 --- /dev/null +++ b/tests/integration/http_server_stop_waits/test_http_server_stop_waits.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# std.http: http_server_stop waits for the server's run, whichever start +# began it, so http_server_free after it is safe. +# +# foreground_actor.ae: http_server_start_raw run by an actor, a keep-alive +# connection held open across the stop, stop and free from main. Stop used +# to wait only for a background server, and the free released a foreground +# one under its pool worker: a segfault on macOS and Linux (found by +# servirtium-vcr). Each round also checks the server's monitor reported +# stopped before stop returned. +# +# from_handler.ae: a stop from a handler (one of the server's own threads) +# only signals, and the free from main then waits; a background server +# stopped that way used to hang. A start of a running server is refused. +# +# A watchdog kills each program after LIMIT seconds so a hang fails rather +# than wedging the sweep. +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +NAME=http_server_stop_waits +EXE="${EXE_EXT:-}" +if [ -z "$EXE" ] && [ ! -x "$ROOT/build/ae" ] && [ -x "$ROOT/build/ae.exe" ]; then + EXE=".exe" +fi +AE="$ROOT/build/ae$EXE" +[ -x "$AE" ] || { echo " [SKIP] $NAME: build/ae not built"; exit 0; } +LIMIT="${STOP_WAITS_LIMIT:-60}" +. "$ROOT/tests/lib/server_jobs.sh" + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP" || true' EXIT + +run_case() { + prog="$1" + want="$2" + if ! "$AE" build "$SCRIPT_DIR/$prog.ae" -o "$TMP/$prog$EXE" > "$TMP/$prog.build.log" 2>&1; then + echo " [FAIL] $NAME: $prog.ae did not build" + tail -8 "$TMP/$prog.build.log" | sed 's/^/ /' + return 1 + fi + STOP_WAITS_DIR="$TMP" "$TMP/$prog$EXE" > "$TMP/$prog.log" 2>&1 & + pid=$! + waited=0 + while server_alive "$pid"; do + if [ "$waited" -ge "$LIMIT" ]; then + kill_server "$pid" + echo " [FAIL] $NAME: $prog did not finish in ${LIMIT}s (a stop hung)" + tail -5 "$TMP/$prog.log" | sed 's/^/ /' + return 1 + fi + sleep 1 + waited=$((waited + 1)) + done + wait "$pid" + rc=$? + if [ "$rc" -ne 0 ] || ! grep -q "$want" "$TMP/$prog.log"; then + echo " [FAIL] $NAME: $prog exited $rc" + tail -5 "$TMP/$prog.log" | sed 's/^/ /' + return 1 + fi + return 0 +} + +run_case foreground_actor "FOREGROUND-OK 10" || exit 1 +run_case from_handler "FROM-HANDLER-OK" || exit 1 +echo " [PASS] $NAME: a foreground server stopped and freed from another thread, a stop from a handler, a refused second start" +exit 0 diff --git a/tests/regression/test_bytes_string_from_ptr_handle.ae b/tests/regression/test_bytes_string_from_ptr_handle.ae new file mode 100644 index 000000000..a3ace575b --- /dev/null +++ b/tests/regression/test_bytes_string_from_ptr_handle.ae @@ -0,0 +1,50 @@ +// Regression: bytes.string_from_ptr wants a raw data pointer, but a bytes +// handle is also a `ptr`, so passing the handle silently copied the +// AetherBytes struct header as text (servirtium-vcr's Azure Key Vault test +// got garbage keys). A handle is now refused with a panic; the correct +// forms (to_string on the handle, string_from_ptr on bytes.data) still work, +// binary-safe (NUL and 0xff bytes). + +import std.bytes +import std.string + +extern exit(code: int) + +fn check(cond: bool, msg: string) { + if !cond { + println("FAIL: ${msg}") + exit(1) + } +} + +main() { + b = bytes.new(8) + _ = bytes.set(b, 0, 65) + _ = bytes.set(b, 1, 0) + _ = bytes.set(b, 2, 255) + _ = bytes.set(b, 3, 66) + _ = bytes.set_length(b, 4) + + s1 = bytes.to_string(b, 4) + check(string.length(s1) == 4, "to_string length") + check(string.char_at(s1, 0) == 65 && string.char_at(s1, 1) == 0, "to_string bytes 0,1") + check(string.char_at(s1, 2) == 255 && string.char_at(s1, 3) == 66, "to_string bytes 2,3") + + s2 = bytes.string_from_ptr(bytes.data(b), 4) + check(string.length(s2) == 4, "string_from_ptr(data) length") + check(string.char_at(s2, 0) == 65 && string.char_at(s2, 1) == 0, "string_from_ptr(data) bytes 0,1") + check(string.char_at(s2, 2) == 255 && string.char_at(s2, 3) == 66, "string_from_ptr(data) bytes 2,3") + + // The trap: the handle itself is refused, not copied as header bytes. + refused = 0 + try { + bad = bytes.string_from_ptr(b, 4) + println("FAIL: handle accepted, length ${string.length(bad)}") + } catch e { + if string.contains(e, "bytes.to_string") == 1 { refused = 1 } + } + check(refused == 1, "string_from_ptr(handle) must panic naming bytes.to_string") + + bytes.free(b) + println("PASS: string_from_ptr refuses a bytes handle; data/raw forms work") +} diff --git a/tools/ae.c b/tools/ae.c index bd635d4db..35b1f0d36 100644 --- a/tools/ae.c +++ b/tools/ae.c @@ -57,6 +57,7 @@ extern char** environ; #ifdef __APPLE__ #include +#include #endif #if defined(__FreeBSD__) || defined(__NetBSD__) #include @@ -5060,6 +5061,34 @@ static const char* ae_vendored_src_defs(void) { return defs; } +#ifdef __APPLE__ +/* A program built here runs here. With MACOSX_DEPLOYMENT_TARGET unset, Apple's + * toolchain targets the SDK's macOS, which a Command Line Tools or Xcode + * install often leaves behind the running OS; Homebrew builds its dylibs for + * the running OS, and ld then warns on every link ("building for macOS-27.0, + * but linking with dylib ... built for newer version 27.2"). The warning goes + * to the build's stderr, so it lands in the output of `ae run`. Default the + * target to the running OS's major.minor; a value the caller set wins, so a + * build that deliberately targets an older macOS keeps doing so. Native + * builds only: the zig cross path names its own target. */ +static bool running_macos_target(char* out, size_t size) { + char ver[64]; + size_t len = sizeof(ver); + if (sysctlbyname("kern.osproductversion", ver, &len, NULL, 0) != 0) return false; + int major = 0, minor = 0; + if (sscanf(ver, "%d.%d", &major, &minor) < 1 || major < 11) return false; + snprintf(out, size, "%d.%d", major, minor); + return true; +} + +static void default_macos_deployment_target(void) { + if (getenv("MACOSX_DEPLOYMENT_TARGET")) return; + char target[32]; + if (running_macos_target(target, sizeof(target))) + setenv("MACOSX_DEPLOYMENT_TARGET", target, 0); +} +#endif + void build_gcc_cmd(char* cmd, size_t size, const char* c_file, const char* out_file, bool optimize, const char* extra_files) { @@ -5365,6 +5394,9 @@ void build_gcc_cmd(char* cmd, size_t size, return; } } +#ifdef __APPLE__ + default_macos_deployment_target(); +#endif /* contrib.sqlite with no archive here: compile it (see * vendored_sqlite_link). */ ae_link = vendored_sqlite_link(cc, ae_link, user_cflags); @@ -13137,6 +13169,18 @@ static int cmd_cflags(int argc, char** argv) { if (wrote_anything) fputc(' ', stdout); fputs("-pthread -lm", stdout); wrote_anything = 1; +#ifdef __APPLE__ + /* The same default `ae build` applies (default_macos_deployment_target), + * for the caller's own compiler, which `ae` cannot set an environment + * for: without it ld warns when a Homebrew dylib named below was built + * for a newer macOS than the SDK's. Omitted when the caller set + * MACOSX_DEPLOYMENT_TARGET, which the compiler reads itself. */ + if (!getenv("MACOSX_DEPLOYMENT_TARGET")) { + char target[32]; + if (running_macos_target(target, sizeof(target))) + printf(" -mmacosx-version-min=%s", target); + } +#endif /* Windows system libraries. libaether references Winsock, the Crypto * API and dbghelp's symboliser regardless of which std modules the