Commit 0c91947
Update libmoq to 0.3.6; rework MoQ source teardown to fix use-after-free (#44)
* Format moq-source.cpp with clang-format
Bring the file into compliance with the repo's clang-format@19 config so
the format check passes on subsequent changes. Pure formatting, no behavior
change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Fix MoQ source teardown UAF; drop unnecessary reconnect sleep
moq_source_destroy freed ctx after a fixed os_sleep_ms(100) that was meant
to let in-flight async MoQ callbacks drain (they check shutting_down and exit
early). That is a timing guess, not synchronization: the decode callback runs
FFmpeg avcodec_send_packet/receive_frame, which can exceed 100ms on a large
keyframe or slow/contended hardware, so a callback could touch ctx after it
was freed. The code documented this limitation itself.
Replace the sleep with real quiescence:
- Add an in-flight callback counter (active_callbacks) and a condition
variable (callbacks_done) to moq_source.
- A callback_guard RAII type increments the counter on callback entry (unless
shutdown has begun) and broadcasts when it returns to zero. Each top-level
callback (on_session_status, on_catalog, on_video_frame) holds one for its
whole stack frame, so ctx cannot be freed while a callback that may touch it
is still running.
- destroy sets shutting_down, disconnects, then waits on callbacks_done until
the count is zero - all under the mutex so the handoff is atomic.
Also drop the os_sleep_ms(50) in the reconnect path. libmoq origins and
sessions are fully independent (each origin is a distinct random instance,
each session its own task): moq_origin_close removes the origin synchronously
and moq_session_close only signals the old session's task to wind down on the
libmoq runtime thread. The new origin/session share no client-side state with
the closed one, so the delay guarded nothing - it was pure latency.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Update libmoq to 0.3.6
Bumps MOQ_VERSION 0.2.14 -> 0.3.6 (latest released; 0.3.7 is tagged but has
no published release artifacts yet). The fetched API is signature-compatible
for everything the plugin uses, and the moq_video_config / moq_frame structs
are unchanged - the only breaking change is semantic: the session on_status
codes were redefined in 0.3.0.
Old (0.2.x): 0 = connected, non-zero = failure.
New (0.3.x): > 0 = (re)connected carrying the connection epoch (1 = first
connect, 2 = first reconnect, ...); 0 = closed cleanly (terminal); < 0 =
fatal / reconnect permanently gave up (terminal).
Update both session status callbacks accordingly:
- moq-source.cpp on_session_status: treat > 0 as connected (was treating the
new epoch-1 connect as a failure, which would have broken connecting
entirely against 0.3.x). Start consuming only on the first connect; on later
epochs libmoq re-subscribes our existing consumer automatically since the
origin outlives the connection, so recreating it would leak handles. Handle
0 (clean close) and < 0 (error) as the terminal cases.
- moq-output.cpp: same > 0 / <= 0 split for its informational connect log.
A nice side effect: 0.3.x sessions auto-reconnect internally with backoff, so
transient transport drops now recover without the plugin's manual reconnect
(which remains for settings changes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Rework source teardown around libmoq terminal callbacks
The previous teardown counted in-flight callback *executions* and waited for
that count to hit zero. That closes the window where a callback is mid-execution
when destroy runs, but not the window where libmoq delivers a callback *after*
the count reached zero: each subscription fires one terminal callback (status
<= 0) asynchronously after it is closed, which could land after destroy had
already freed ctx.
libmoq >= 0.3.0 documents a terminal-callback lifetime contract: user_data must
stay valid until each subscription's terminal callback. Rework teardown to honor
it directly via subscription reference counting:
- ctx holds one reference for the OBS-owned source plus one per outstanding
subscription handed `ctx` as user_data (session, catalog, video track). The
ref is pre-incremented before the libmoq registration call (undone if it
fails) and released by that subscription's terminal callback via the
subscription_ref RAII guard. Because libmoq runs all callbacks serially on one
runtime thread, the ref is held continuously from registration through the
terminal callback, so ctx is always valid inside any callback.
- destroy sets shutting_down, closes every subscription (which makes each
terminal fire promptly - libmoq's biased close path wins over pending
updates), drops the owner ref, and waits for the count to reach zero. A
generous bounded wait backstops a subscription that never terminates so a
broken teardown degrades to a logged warning instead of hanging OBS.
Along the way this fixes a latent handle-management bug: the catalog *snapshot*
id delivered to on_catalog was being stored and closed with
moq_consume_catalog_close (which expects a *subscription* id from a different
slab - an id-collision hazard). Now the real subscription handle from
moq_consume_catalog is stored and closed, snapshots are freed with
moq_consume_catalog_free on every path, and a catalog update closes the previous
video track instead of leaking it.
Also: a fatal session error now tears down all subscriptions (not just
session+origin) so their terminals fire promptly, and reconnect bails if
shutting down.
The refcount balance and deadlock-freedom were checked by two independent
adversarial reviews. Still wants runtime validation: connect/disconnect,
scene-switch teardown with a decode in flight, settings-change reconnect, and
repeated catalog updates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci: trust obsproject tap before installing clang-format
Homebrew now refuses to run executables from an untrusted third-party tap,
so the format check's `brew install obsproject/tools/clang-format@19` was
skipped ("Skipping obsproject/tools because it is not trusted") and the step
failed with a broken pipe before any file was checked. Explicitly tap and
`brew trust obsproject/tools` first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Format moq-output.h with clang-format
Bring the header into compliance with the repo's clang-format@19 config so the
format check passes once the file is touched. Pure formatting, no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Fix MoQ output teardown use-after-free; review fixes
Code review surfaced that the source-side teardown rework left the sibling
MoQOutput with the same class of bug the 0.3.x bump makes live:
- Use-after-free: ~MoQOutput -> Stop() calls moq_session_close, which only
signals; libmoq delivers the session's terminal status callback (code <= 0)
asynchronously on its runtime thread afterward and dereferences `self`
(server_url, connect_time_ms, connect_start). OBS deletes the object as soon
as ~MoQOutput returns, so the late callback touched freed memory. 0.3.x's
auto-reconnect also fires the callback repeatedly mid-stream, widening the
window. Fix by mirroring the source's pattern: count outstanding session
subscriptions (pre-incremented before moq_session_connect, released by the
terminal callback) and have the destructor wait for the count to reach zero,
with the same bounded backstop so a missing terminal warns instead of hanging.
- Data race: connect_time_ms is written by the callback (runtime thread) and
read by GetConnectTime() (OBS thread); make it std::atomic<int>.
Also (source): restore the blank-video-on-catalog-error behavior that the
terminal-callback refactor dropped, so an invalid/failed broadcast still clears
the preview (except during our own teardown).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 0fa282c commit 0c91947
5 files changed
Lines changed: 381 additions & 218 deletions
File tree
- .github/actions/run-clang-format
- src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
54 | 58 | | |
55 | 59 | | |
56 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
28 | 37 | | |
29 | 38 | | |
30 | 39 | | |
| |||
75 | 84 | | |
76 | 85 | | |
77 | 86 | | |
78 | | - | |
79 | | - | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
80 | 91 | | |
81 | 92 | | |
82 | | - | |
| 93 | + | |
83 | 94 | | |
84 | | - | |
85 | | - | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
86 | 98 | | |
87 | 99 | | |
88 | | - | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
89 | 107 | | |
90 | 108 | | |
91 | 109 | | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
92 | 118 | | |
93 | 119 | | |
94 | 120 | | |
95 | 121 | | |
96 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
97 | 127 | | |
98 | 128 | | |
99 | 129 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
| 6 | + | |
5 | 7 | | |
| 8 | + | |
6 | 9 | | |
7 | 10 | | |
8 | 11 | | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
49 | 57 | | |
50 | 58 | | |
51 | 59 | | |
0 commit comments