Skip to content

Fix server destroy lifecycle race - #712

Open
ViktorJannicke wants to merge 2 commits into
webui-dev:mainfrom
Runic-Artifex:codex/server-destroy-lifecycle
Open

Fix server destroy lifecycle race#712
ViktorJannicke wants to merge 2 commits into
webui-dev:mainfrom
Runic-Artifex:codex/server-destroy-lifecycle

Conversation

@ViktorJannicke

@ViktorJannicke ViktorJannicke commented Aug 8, 2026

Copy link
Copy Markdown

Summary

Fix crashes, use-after-free races, and shutdown deadlocks when a WebUI window is destroyed while its server, callbacks, WebSocket work, WebView work, or show operations are still active.

This introduces coordinated two-phase destruction: new work is rejected immediately, while final memory reclamation waits until every owner of the window and its CivetWeb connections has retired.

Motivation

We recently started using WebUI in a project and, while integrating it more deeply, began exercising window and server lifecycle paths beyond the basic examples. This exposed reproducible crashes when destroying a window from a callback and during repeated server teardown.

Investigating those crashes required following ownership across WebUI’s window state, server threads, callbacks, and the embedded CivetWeb connection storage. The resulting diff is larger than the visible fix might suggest because the affected lifetime rules were spread across several teardown paths. This change consolidates those rules so destruction consistently stops new work, waits for active work to retire, and only then releases the underlying state.

Problem

webui_destroy() could free window state while it was still referenced by:

  • The server thread during startup or shutdown
  • User callbacks, including file-handler callbacks
  • Detached WebSocket and asynchronous-response work
  • WebView worker threads
  • Concurrent webui_show*() or webui_start_server() calls
  • Folder-monitor threads

Destroying a window from one of its own callbacks was especially unsafe. Waiting synchronously would deadlock because CivetWeb was waiting for that callback to return, while freeing immediately caused use-after-free crashes.

CivetWeb also needed a way to stop accepting new work and interrupt blocked connection I/O without immediately joining workers and freeing connection storage still referenced by WebUI tasks.

Changes

Window lifecycle

  • Split destruction into request and finalization phases.
  • Track the complete server-thread lifetime, including startup and cleanup.
  • Track active callbacks, detached tasks, WebView work, and public show calls.
  • Stop admitting new work as soon as destruction begins.
  • Defer callback-triggered destruction until the callback and server retire.
  • Make concurrent webui_destroy() calls wait on the global window registry instead of touching potentially reclaimed per-window synchronization objects.
  • Prevent reusing a window ID until the previous generation has been fully finalized.
  • Correct the window ID upper-bound check to reject WEBUI_MAX_IDS.

Show and global cleanup coordination

  • Retain windows for the full duration of webui_show*() and webui_start_server().
  • Prevent new show calls once global cleanup begins.
  • Make webui_clean() wait for both server threads and show operations.
  • When webui_clean() is called from a WebUI callback, request application exit and defer process-wide reclamation until the callback has returned.
  • Cancel asynchronous waits when their window begins stopping.

webui_start_server() result lifetime

  • Store returned server URLs per window.
  • Starting another window no longer overwrites an earlier returned URL.
  • Returned URLs remain valid across the originating thread exiting and across window destruction, until the same window result is replaced or WebUI is cleaned.

Worker and monitor shutdown

  • Account for detached WebSocket and WebView work before reclaiming window state.
  • Replace forced folder-monitor cancellation with cooperative, bounded polling on Windows, Linux, and macOS.
  • Interrupt blocked CivetWeb connection I/O so retained tasks can finish during shutdown.
  • Use CivetWeb’s per-connection write serialization instead of a global WebUI send lock.

Vendored CivetWeb carry patch

Add a WebUI-private quiesce operation that:

  • Rejects newly accepted sockets without freeing the server context.
  • Prevents dynamic workers from being published after quiescing begins.
  • Closes queued sockets and interrupts active worker connections.
  • Keeps worker connection storage alive until WebUI tasks have retired.
  • Synchronizes socket scanning with worker close and descriptor invalidation.
  • Initializes idle worker sockets to INVALID_SOCKET.
  • Transfers alternative-queue socket ownership without leaving stale descriptor copies.

The extension is private to WebUI and is not added to CivetWeb’s public API.

Compatibility

  • No WebUI public function signatures changed.
  • No new public CivetWeb APIs were introduced.
  • Existing C and C++ consuming code remains source-compatible.
  • webui_start_server() now provides safer returned-string storage.
  • Destruction from callbacks and concurrent lifecycle operations are now handled safely.

Verification

Verified on Linux with:

  • Full CMake release build, including examples
  • GCC release and debug builds
  • Clang GNU99 syntax checks
  • Default and ALTERNATIVE_QUEUE CivetWeb configurations
  • AddressSanitizer and UndefinedBehaviorSanitizer
  • Repeated create/show/destroy lifecycle stress
  • Destruction from normal, file-handler, and asynchronous callbacks
  • Concurrent show/destroy, show/clean, and repeated destroy calls
  • Folder-monitor shutdown
  • Same-ID rejection while an older generation retires
  • webui_start_server() URL lifetime across destruction and thread exit
  • Repeated stop and file-descriptor-zero safety checks

@ViktorJannicke
ViktorJannicke marked this pull request as draft August 8, 2026 15:13
@ViktorJannicke
ViktorJannicke force-pushed the codex/server-destroy-lifecycle branch from e3c5327 to 92c918d Compare August 8, 2026 18:39
@ViktorJannicke
ViktorJannicke force-pushed the codex/server-destroy-lifecycle branch from 92c918d to bcf6d1f Compare August 8, 2026 18:53
@ViktorJannicke
ViktorJannicke marked this pull request as ready for review August 8, 2026 18:53
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