Skip to content

Batch: http_server_stop waits for any server; string_from_ptr refuses a bytes handle; macOS deployment target follows the OS - #3642

Merged
paul-hammant merged 3 commits into
mainfrom
fix/batch-2026-11-09
Nov 9, 2026
Merged

paul-hammant merged 3 commits into
mainfrom
fix/batch-2026-11-09

Conversation

@paul-hammant

Copy link
Copy Markdown
Collaborator

Three fixes, one commit each, batched to save Actions minutes.

1. std.http: http_server_stop waits for the server, however it was started (88d650814)

  • Bug: stop only joined a server started with http_server_start_background_raw. For a foreground http_server_start_raw on another thread (e.g. an actor), stop returned at once, and the http_server_free that followed freed the server under the still-running accept loop and a pool worker serving a keep-alive connection. ASan: heap-use-after-free in conn_serve / http_server_serve. Found by servirtium-vcr.
  • Second bug found on the way: a background server stopped from one of its own handlers hung or used freed memory. The handler's stop joined the background thread, which was itself joining the worker running the handler.
  • Contract: stop always signals and then waits until the run is over (accept loop returned; pool, event loop and parking lot joined), whichever start began it, so free after stop is always safe.
    • A stop from one of the run's own threads (a handler, the monitor, a proxy event-loop thread, an h2 dispatch worker) only signals, so it can't deadlock.
    • Starting a server that is already running is refused.
    • Free on a run thread while running aborts with a clear message.
    • The state is per server (run lock and condvar, serving flag, the run's thread ids). background_tid and background_tid_set are removed.
  • Not covered (pre-existing): in actor-dispatch mode, actor handlers in flight are not waited for.
  • Tests: tests/integration/http_server_stop_waits. foreground_actor holds a keep-alive connection across stop and free, 10 rounds; from_handler stops from a handler in foreground and background modes, and checks the double-start refusal.
    • On main: crash, fail or hang 3/3 on Linux and macOS.
    • With the fix: pass 3/3, ASan clean.

2. std.bytes: string_from_ptr refuses a bytes handle (e7b6ab557)

  • Bug: string_from_ptr(src, n) wants a raw data pointer, but a bytes handle is also a ptr. Passing one silently copied the AetherBytes header: string_from_ptr(b, 4) gave 04000000 where to_string(b, 4) gives 4100ff42.
  • Fix: AetherBytes now carries a tag, checked the way is_aether_string checks strings (byte by byte, so a raw C buffer is never over-read). A live handle panics with a message pointing to bytes.to_string(b, n) or bytes.data(b).
  • Why not a typed handle: it would need compiler work or retyping about 271 files and every cryptography buffer argument.
  • Docs and the API page say src must be a raw data pointer. Regression test included.

3. ae build on macOS: the deployment target defaults to the running OS (4cfa58624)

  • Bug: with MACOSX_DEPLOYMENT_TARGET unset, clang targets the selected SDK's macOS (27.0 from an Xcode beta here). Homebrew builds its dylibs for the running OS (27.2), so every link against nghttp2 printed an ld "built for newer version" warning into ae run output. That failed http_server_monitor and two examples, and make ci on macOS stopped at step 5.
  • Fix: ae build and ae cflags default the target to the running OS when the variable is unset; a value the caller sets still wins. http_server_monitor now builds first and compares only the program's output.
  • Release builds of libaether itself are unchanged, so release artifacts don't start requiring a newer macOS.

Verified:

  • make ci on cachyos (gcc 16, -Werror) with all three together: 714 C unit tests, 2203 .ae tests passed, 0 failed.
  • macOS (Apple clang): full make ci passes all 11 steps for the first time.

🤖 Generated with Claude Code

paul-hammant and others added 3 commits November 9, 2026 19:30
http_server_stop joined only 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 the server under its accept loop
and a pool worker still serving a keep-alive connection: a use after free
in conn_serve / http_server_serve (ASan), a segfault on macOS and Linux
(found by servirtium-vcr).

The server now tracks its run per instance: a start begins it under
run_lock (and refuses a server that is already running), the run ends it
once its accept loop has returned and its pool, event loop and parking lot
are joined, and broadcasts run_done. http_server_stop waits for that,
whichever start began the run and on whatever thread, then joins a
background thread. The run's own threads (the one running it, the pool
workers, the proxy driver's threads, an HTTP/2 dispatch worker for its
handler) register for the run, so a stop on one of them, from a handler
or the monitor, only signals rather than waiting for a run that waits for
it; a background server stopped from a handler used to hang there.
http_server_free on such a thread while the server runs is fatal instead
of a use after free. server_start_background now reports the C side's
reason when it fails.

tests/integration/http_server_stop_waits: a foreground server on an actor
with a keep-alive connection held across stop and free, checking the
stopped monitor fired before stop returned; stops from a handler,
foreground and background; a refused second start. Fail on main (stop
returns early; ASan use after free; macOS segfault 3/3), pass with this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its header

bytes.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 passing the handle silently
copied the AetherBytes struct header as text (bit servirtium-vcr Azure Key
Vault test). AetherBytes now carries an 8-byte tag (cleared on free) and
string_from_ptr panics on a live handle, naming bytes.to_string / bytes.data.
The tag check short-circuits byte by byte, so raw buffers are never over-read.
Docs (module comment, README, docs/api) say src must be a raw data pointer.
Regression test covers the refused handle and the binary-safe correct forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…test builds before running

With MACOSX_DEPLOYMENT_TARGET unset, clang/ld target the SDK's macOS. When
the SDK trails the running OS (a Command Line Tools or Xcode install that
is a minor behind), linking a Homebrew dylib built for the running OS
makes ld warn on every link:

  ld: warning: building for macOS-27.0, but linking with dylib
  '.../libnghttp2.14.dylib' which was built for newer version 27.2

The warning goes to the build's stderr and so into `ae run` output, which
broke tests/integration/http_server_monitor (exact match on MONITOR-OK) and
stopped `make ci` at step 5/11 on the Mac.

build_gcc_cmd (the native POSIX path) now sets MACOSX_DEPLOYMENT_TARGET to
the running OS's major.minor via kern.osproductversion when the caller has
not set it. A caller's value wins, so a deliberate older target is kept;
the zig cross path is untouched; release.yml pins no deployment target.

The monitor test now builds each program, then runs it, so it compares the
program's output and not the linker's; a failed build shows its own log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paul-hammant
paul-hammant merged commit 6f45d14 into main Nov 9, 2026
56 of 57 checks passed
@paul-hammant
paul-hammant deleted the fix/batch-2026-11-09 branch November 9, 2026 22:47
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.

1 participant