Skip to content

Pr 4 audit - #6

Open
SetZero wants to merge 40 commits into
1.6.xfrom
pr-4-audit
Open

SetZero wants to merge 40 commits into
1.6.xfrom
pr-4-audit

Conversation

@SetZero

@SetZero SetZero commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Checks

Krzmbrzl and others added 30 commits June 28, 2026 11:30
In case the received message was larger than the buffer we provide, the
message is truncated to the buffer's size. However, due to passing the
MSG_TRUNC flag, the returned message size was the original message size.
Hence, in cases where the message got truncated to fit in our buffer,
the returned size was larger than our buffer. This lead to potentially
using the buffer as larger than it actually is.

(cherry picked from commit a30b291)
MsgRaw allocated HID report buffers that were never released.
Add the missing destructor to free the allocated memory.

Closes mumble-voip#3406

(cherry picked from commit 0ff6d59)
Qt's restoreState and restoreGeometry is very unreliable and might
even crash the applicaiton.

This commit changes the window load sequence to first clear the
geometry/state settings and writing them to disk, before trying
to actually load the geometry/state.

In case the application crashes, the window state/geometry in the
settings on disk will be empty and a subsequent application start
will not crash (at least not for the same reason).

In case the geometry/state restore is successful, the geometry/state
is written to disk again.

Fixes mumble-voip#7193

(cherry picked from commit c06b475)
…s on crash

When a crash is detected during the restore state/geometry process,
we latch a newly introduced setting to prevent future crashing.

See mumble-voip#7193

(cherry picked from commit 6877cf6)
…s around restoreState and restoreGeometry" to 1.6.x (mumble-voip#7243)
With this checkbox, users can now control the previously added latch
to prevent corrupt window state and window geometry from loading (and
saving).

See mumble-voip#7241

(cherry picked from commit 7983aea)
…tates

With this new CLI option, users can now directly ask Mumble to skip
the whole window state/geometry restoring and saving logic directly before
the application full starts up.

See mumble-voip#7241

(cherry picked from commit 771072b)
(cherry picked from commit 8d32456)
During the rebase of mumble-voip#6243 onto a9b1356  this change was missed
and the flag was not applied properly.

This commit adds back the flag to the loading function.

(cherry picked from commit 54c4564)
Add a new mumble-audit plugin crate to the plugin-host workspace,
implementing the tamper-evident server audit log described in
docs/audit-log.md (design phases 1-2).

The plugin owns its own bundled-SQLite database and records
server-authoritative moderation actions (bans, kicks, mutes,
ACL/channel/config changes, plugin administration, pchat
moderation) as a git-style SHA-256 hash chain, with:

  - chain construction and validation (chain.rs)
  - event model and ingest pipeline (model.rs, ingest.rs, event_log.rs)
  - SQLite-backed store (store.rs)
  - per-part collection toggles (toggles.rs)
  - write-time retention with a 30-day accountability floor (retention.rs)
  - a permission/authorization seam (authz.rs)
  - config and runtime wiring (config.rs, runtime.rs, lib.rs)

Register the crate as a workspace member and publish its release
artifacts (libmumble_audit.{so,dll,dylib}) from the plugin-host CI
workflow.
MumbleProtocol.h pins wire IDs 166-171, which generates dispatch
references to Server::msgFancyAudit*; without definitions the murmur
link fails (undefined reference). Accept-and-drop until phase 3 wires
the plugin-backed query/config surface.
…ngest

Implements the phase-3 seam the wire contract pinned (docs/audit-log.md
section 5), mirroring the LinkPreviewBridge pattern:

- AuditLogBridge translates FancyAuditQuery (166) into the plugin's
  audit.query / audit.verify plugin-messages and packs the replies into
  FancyAuditResponse (167) - structured filters, keyset pagination
  (has_more/next_before_id), chain verification, and an explicit error
  for the unavailable advanced-SQL mode. FancyAuditConfigUpdate (171)
  routes to audit.config.set; the audit.config snapshot becomes
  FancyAuditConfig (170) and is pushed post-ServerSync to fancy-0.4.2
  root-Write admins alongside the server-settings snapshot.
- Core moderation events now reach the plugin's ingest pipeline as
  audit.ingest plugin-messages stamped sender_session=0 (which the
  plugin requires, so clients cannot forge entries): channel.create,
  channel.remove, kick, ban and acl, with wall-clock-derived offsets so
  idempotency survives server restarts.
- mumble-audit plugin: handle audit.ingest / audit.config.get /
  audit.config.set; replies now travel over the host's
  send_request_response seam (instead of raw plugin messages) so the
  bridge can pack them into the Fancy wire messages; config snapshot
  renders the per-part toggle matrix as generic Setting rows with a
  monotonic revision and the chain height.
- Audit handlers moved before Messages.cpp's #undef block (MSG_SETUP /
  RATELIMIT / PERM_DENIED are gone after it).
- kAudit*-prefixed anonymous-namespace constants: CMAKE_UNITY_BUILD
  merges translation units, colliding with LinkPreviewBridge's
  kPluginName.
- Explicit qint64 for the QJsonValue channel_id insert (int64_t is
  ambiguous on LP64).
…version

The handshake advertises the protocol-crate version (0.3.x), which does
not track per-feature support - the 0.4.2 comparison could never pass,
so no client ever received the FancyAuditConfig snapshot and the Audit
tab's Configuration half stayed empty. Gate on has_value() only, like
sendFancyServerSettings.
SetZero added 10 commits July 20, 2026 18:00
The audit plugin already mapped mute/deafen/suppress/priority_speaker,
move, register/deregister, plugin.* and pchat.* onto parts
(toggles.rs Part::from_event_kind), but the core server only called
AuditLogBridge::emitEvent from four handlers: ban/kick, channel.create,
channel.remove and acl. Every other part was therefore switchable in the
admin UI while never producing a single entry - the consumer existed,
the producer did not.

Add the missing producers through the existing bridge (no ABI change, no
host hook: this is the same push API the four existing sites use):

  - mute / deafen / suppress / priority_speaker in msgUserState, via a
    local helper that snapshots the flags first and emits one event per
    attribute that actually changed, outside the voice-thread write lock
    since publishing does I/O
  - move, only when the channel really changes
  - register / deregister
  - channel.update, one event per edit with a `fields` list because a
    single ChannelState can rename, reparent and re-describe at once
  - plugin.install / enable / disable / uninstall
  - pchat.delete_messages, recording which deletion scope was used (ids,
    time range or sender) - a range delete carries no ids and would
    otherwise look like a no-op
  - config, recording the changed keys but never their values, since
    server settings carry passwords and tokens
CHANNEL_ATTRIBUTE_STRUCTURAL marks a channel that exists only to organise the
tree: entry is refused so it never holds users, and clients draw it as a heading
for the channels nested beneath it. It is persisted as a channel property so it
survives a restart, and the login landing-channel search now skips structural
channels rather than stalling when a remembered channel has become one. Making
the root structural is refused outright, since it would leave nowhere to log in
to.

ChannelState gains attribute_mask so a client can assign attributes without a
dedicated field per trait. For every attribute named in the mask the server
assigns the value implied by `attributes` - set when also present there, cleared
when not - and leaves unlisted attributes untouched, so a rename no longer
disturbs unrelated traits. A repeated field cannot distinguish "empty" from
"absent", which is exactly why clearing the last attribute needs the mask.
Omitting it keeps the original additive create-time behaviour.

ChannelAttribute is now partitioned explicitly: server-computed (CAN_ENTER,
ENTER_RESTRICTED, HIDDEN, TEMPORARY) is ignored as input, create-only (DETACHED)
is honoured on create, and settable (STRUCTURAL) is honoured on create and edit
and requires Write.
…stream

This server speaks the interleaved 100-999 message layout and always has, but
never said so. An absent `fancy_protocol` and an epoch-0 one mean different
things to a client — "has not been taught about epochs" versus "speaks epoch 0"
— and the two need different handling the moment a second epoch exists, which
it now does. `Server::sendVersion` states it rather than leaving it to the
default.

The field itself is separate from `fancy_version` deliberately. That one is a
product version: it answers which *features* exist, and cannot express "I
renumbered the wire". A client that trusts it for that sends types the peer can
route nowhere, which is exactly what happened when Starling kept the upstream
message types and moved every Fancy field into a new range.

Mumble.proto's Fancy fields move to 100+ to match, leaving 1-99 to upstream
with a margin, so a future upstream field cannot land on one of ours.
…living a session

Seven of the eight findings in SECURITY-AUDIT-pchat.md. The three High ones
chained, so the first two break the chain.

handlePchatKeyHolderReport had no authorization of any kind and took cert_hash
straight off the wire, so any authenticated client could write a holder row
naming anybody, for any channel, including ones it cannot see -- and that row
is what the client UI renders as "who can read this channel". It now takes the
hash from the session and refuses a report naming anyone else, exactly as
handlePchatMessage has always done, refuses a session with no certificate at
all (every identity here is a certificate hash), and requires Enter. The Enter
check sits after the takeover dispatch because takeover carries its own,
stronger, KeyOwner check.

The sole-holder challenge reset asked "does another holder exist" when it meant
"am I the holder". On a channel with no recorded holders -- the state right
after a takeover or a wipe -- that handed the reset, and with it the right to
redefine the reference HMAC, to whoever asked first.

Verified status is keyed on the session id, and the server puts the id straight
back in the reuse pool on disconnect. onUserDisconnected exists to clear it and
says so, but both it and its call site returned early whenever the certificate
hash was empty, so it never ran for anonymous clients -- on a server that
allows anonymous connections, most of them. Only the pending-key-request
cleanup actually needs a hash, so only that part is still conditional.

Six key-management operations were unlimited, which is the half of the message
set that writes holder rows and drives the challenge. They now have budgets,
and every bucket is keyed on the certificate hash rather than the session id,
so a limit is no longer cleared by reconnecting. IRateLimiter::reset was
implemented, documented "e.g. on disconnect", and called from nowhere; it is
called now, which is also what stops the buckets accumulating forever.

sendStoredSenderKeyDistributions hands out every stored sender key for a
channel, so it carries its own Enter check rather than trusting a caller that
reaches it from the auto-verify path.

Holder writes, rejected reports, challenge resets and takeovers now reach the
audit log instead of only qWarning, via a new IServerBridge::emitAuditEvent
taking a flat key/value list so pchat stays free of any JSON type.

Finding 3, the first-prover-wins challenge, is mitigated rather than closed:
reaching it now needs Enter and your own certificate, but the server still has
no ground truth to check a proof against. Closing it needs a stored key
commitment and therefore a client change.

9 regression tests; the image build runs ctest with --no-tests=error and no
|| true, so it cannot be produced if one fails.
Text and voice fan-out for persistent chat reached sessions that should
not have been in the recipient set. Fixed in Messages.cpp and Server.cpp
with the protocol and docs updated to match.
The same removal Starling made in its own copy: the epoch-1 envelope block
that duplicated definitions the canon owns. All three trees carry one
definition per message again.
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.

8 participants