|
| 1 | +# Changelog |
| 2 | + |
| 3 | +All notable changes to this project are documented here. |
| 4 | + |
| 5 | +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), |
| 6 | +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | + |
| 8 | +## [2.5.0] - 2026-05-09 |
| 9 | + |
| 10 | +### Security |
| 11 | + |
| 12 | +- **CVE-PENDING (GHSA-xxxx-xxxx-xxxx)** — Missing authentication on |
| 13 | + the WebRTC source-ingest endpoint. `POST /webrtc/source-offer` |
| 14 | + accepted any inbound SDP offer with no source-password check; |
| 15 | + any internet user able to reach the server could hijack any |
| 16 | + mount's broadcast and replace the legitimate publisher's audio. |
| 17 | + The icecast SOURCE / RTMP / SRT ingest paths already required |
| 18 | + the per-mount source password — this one didn't. Affected |
| 19 | + versions: **>= 0.8.95, <= 2.4.1** (introduced 2026-02-21 in |
| 20 | + `e2b60d6`). Fixed in this release. CWE-306. CVSS 3.1: **7.4 |
| 21 | + High** (`AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L`). The same fix |
| 22 | + also adds source-password rate-limit hookup so wrong-password |
| 23 | + attempts contribute to the IP-level brute-force lockout. |
| 24 | +- Hardened additional auth gaps surfaced during the audit that |
| 25 | + shipped this release: |
| 26 | + - `POST /admin/golive/chunk` now requires CSRF + per-mount |
| 27 | + access. Previously, any authenticated user could broadcast |
| 28 | + raw audio bytes to any mount. |
| 29 | + - AutoDJ create / update / delete (both `/api/v2/autodjs` and |
| 30 | + the legacy `/admin/autodj/*` form handlers) now require |
| 31 | + `superadmin`. Previously a `dj`-role user could register an |
| 32 | + AutoDJ with arbitrary `song_command` / `on_play_command` |
| 33 | + shell strings that the server executes via `sh -c` — |
| 34 | + privilege escalation to the tinyice service user. |
| 35 | + - `POST /api/pending-users/approve` and `/deny` now check CSRF. |
| 36 | + Without it, an attacker page that a logged-in admin visited |
| 37 | + could submit a form-encoded POST with a JSON body that the |
| 38 | + handler would still decode and use to promote an attacker- |
| 39 | + controlled pending user to `superadmin`. |
| 40 | + - `/admin/clear-auth-lockout` and `/admin/clear-scan-lockout` |
| 41 | + now require `admin` or `superadmin`. A `dj` could |
| 42 | + previously clear an attacker's brute-force lockout, undoing |
| 43 | + the rate-limiter. |
| 44 | + - `handleLogin` now runs bcrypt against a dummy hash for |
| 45 | + unknown users to close the account-enumeration timing |
| 46 | + oracle (~250 ms vs <1 ms). |
| 47 | + - `checkAuthLimit` now bypasses the lockout when the IP is |
| 48 | + whitelisted, so an operator's own IP can recover after a |
| 49 | + misconfiguration without restarting the service. |
| 50 | + - MPD `password` comparison now uses |
| 51 | + `crypto/subtle.ConstantTimeCompare`. The MPD |
| 52 | + `command_list_*` accumulator is now bounded at 10 000 lines |
| 53 | + so an authenticated client can't OOM the process by opening |
| 54 | + a list and never closing it. |
| 55 | + |
| 56 | +### Fixed |
| 57 | + |
| 58 | +- **Production hang root cause:** `FindNextPageBoundary` infinite |
| 59 | + loop at the circular-buffer wrap. When the search window was |
| 60 | + clamped to `≤ 3` bytes, the iterator advanced by `n - 3 ≤ 0` |
| 61 | + and never moved forward, holding `cb.mu.RLock` until the |
| 62 | + process restarted. Every concurrent `Buffer.Write` then queued |
| 63 | + on the buffer's write lock, which queued every `Stream.Snapshot`, |
| 64 | + which queued every `GetStream`, freezing the relay. r4dio's |
| 65 | + pprof showed one stuck listener and 100+ goroutines blocked |
| 66 | + behind it. Fix: never advance by less than 1 byte; skip the |
| 67 | + search when `n < magicLen`. |
| 68 | +- `Stream.Broadcast` could panic with `send on closed channel` |
| 69 | + when a listener's `Unsubscribe` raced with the listener-signal |
| 70 | + fan-out (production journal showed 4 occurrences in 7 days, |
| 71 | + all from the icecast SOURCE goroutine). Two-phase fix: |
| 72 | + recover() in the signal loop as a defense, and remove the |
| 73 | + `close(ch)` from `Unsubscribe` — every caller is a self-exit |
| 74 | + defer that doesn't need the wake-up signal. Eliminates the |
| 75 | + race entirely. |
| 76 | +- `Stream.SetCurrentSong` was holding `s.mu.Lock` across five |
| 77 | + GORM/SQLite queries inside `History.Add`. Every ICY |
| 78 | + metadata update froze every broadcast / subscribe / snapshot |
| 79 | + / listener handler on the stream for the duration. Capture |
| 80 | + the diff under the lock, release, then call `History.Add` |
| 81 | + outside. |
| 82 | +- Icecast SOURCE hijacked TCP connections now have a 60s idle |
| 83 | + read deadline; a silent encoder (NAT idle drop, frozen |
| 84 | + process) used to pin a goroutine + FD + mounted Stream |
| 85 | + forever, eventually exhausting FDs. |
| 86 | +- Same idle-read deadline now also applies to the icecast |
| 87 | + pull-relay body, RTMP per-conn reads, and SRT publish reads. |
| 88 | +- WebRTC `OnConnectionStateChange` is now registered ONCE per |
| 89 | + PeerConnection (in HandleWHEPOffer / HandleOffer) rather than |
| 90 | + by both `streamToTrack` and `streamVideoToTrack` — pion's |
| 91 | + API replaces the prior handler, so the loser was leaking a |
| 92 | + goroutine + Stream subscription per WHEP listener disconnect. |
| 93 | +- WebRTC source-ingest now drains the previous publisher's pump |
| 94 | + goroutine (up to 3s) before letting the successor start |
| 95 | + writing — without this, two pumps briefly ran in parallel and |
| 96 | + produced torn Ogg pages on listener tabs. |
| 97 | +- `pc.Close()` is now called explicitly in the WebRTC terminal |
| 98 | + state-change handler to release pion's UDP sockets / DTLS |
| 99 | + state. |
| 100 | +- `Track.ResolveCodec` no longer panics on a nil receiver. The |
| 101 | + function's nil-check branch dereferenced `t.Codec` after |
| 102 | + asserting `t == nil`. |
| 103 | +- `SavePlaylist` no longer takes `s.mu.RLock` recursively via |
| 104 | + `GetSongTitle`. Recursive RLock is undefined behaviour in |
| 105 | + Go's writer-preferring `RWMutex` and deadlocks if a writer |
| 106 | + queues between the outer and inner acquisitions. |
| 107 | +- `Pipeline.Stats` now uses `atomic.LoadInt64` for `BytesIn` / |
| 108 | + `BytesOut` (matching the atomic writes from `Broadcast`) and |
| 109 | + `Stream.GetLastDataReceived` / `GetOggHead` synchronise |
| 110 | + reads of those fields against their locked writes (avoids |
| 111 | + torn `time.Time` and torn slice headers). |
| 112 | +- TS demuxer now resyncs byte-by-byte when the sync byte is |
| 113 | + missing instead of jumping by 188 — silent data loss on |
| 114 | + misaligned SRT inputs is gone. |
| 115 | +- HLS `RegisterHLS` is now race-safe; two concurrent first |
| 116 | + listeners no longer each spawn their own `segmentLoop`. |
| 117 | +- `decoder_hub.contextDeadline` no longer leaks a 30 s goroutine |
| 118 | + per pump cycle (replaced with `time.After`). |
| 119 | +- `OnTrackStart` callback runs in a goroutine outside any HTTP |
| 120 | + handler — recover() now contains panics in user-supplied |
| 121 | + webhook subscribers so they can't crash the process. |
| 122 | +- `Streamer.Stop` now cancels the streamer-lifetime context, so |
| 123 | + `on_play_command` child shells get SIGKILL via |
| 124 | + `exec.CommandContext` instead of lingering up to their |
| 125 | + per-command timeout (default 10s) past the operator's Stop. |
| 126 | +- YP directory `POST` now has a 15 s context timeout. Previously |
| 127 | + used `http.PostForm` against the default client with no |
| 128 | + timeout, so a hung directory server pinned the |
| 129 | + `directoryReportingTask` goroutine forever. |
| 130 | + |
| 131 | +### Added |
| 132 | + |
| 133 | +- `feat(transcoder): per-output visibility` — TranscoderConfig has |
| 134 | + a new `visibility` field with `""` (follow input, default), |
| 135 | + `"public"` (listed), or `"unlisted"` (hidden, still |
| 136 | + streamable). Surfaces in the admin add-transcoder form and |
| 137 | + the v2 API. |
| 138 | +- `feat(metrics): /debug/pprof on the metrics server` — the |
| 139 | + metrics-server bootstrap function existed but was never |
| 140 | + called from `Server.Start()`. Now it is, and it also |
| 141 | + registers `net/http/pprof` so a stuck production instance |
| 142 | + can be triaged with `curl |
| 143 | + http://HOST:8081/debug/pprof/goroutine?debug=2` etc. |
| 144 | + Mutex- and block-profile sampling is enabled at rate 1. |
| 145 | + |
| 146 | +### Changed |
| 147 | + |
| 148 | +- `relay.Broadcast` now releases the stream's write lock before |
| 149 | + fanning out listener signal channels (committed earlier in |
| 150 | + this release line as `daf5368`). The previous full-lock fan- |
| 151 | + out was the dominant lock-contention vector under load. |
| 152 | + |
| 153 | +[2.5.0]: https://github.com/DatanoiseTV/tinyice/releases/tag/v2.5.0 |
0 commit comments