Skip to content

2.7.2: security audit fixes, data integrity, UI/UX and refactor - #53

Merged
1window2 merged 110 commits into
mainfrom
v2.7.2
Nov 6, 2026
Merged

1window2 merged 110 commits into
mainfrom
v2.7.2

Conversation

@1window2

@1window2 1window2 commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Summary

2.7.2 is a security and integrity release built from a full source audit of 2.7.1 (security-audit skill: 22 coverage units, 37 validated findings, adversarial critic rounds), plus whole-code reviews for UI/UX, database gaps and duplicates, and input normalization. A consistency and refactor pass followed, then a three-lens adversarial review of the complete 2.7.1→2.7.2 diff. That review confirmed 15 more findings, and all of them are fixed here.

See CHANGELOG.md for the full user-facing list. Highlights:

Security

  • Client signer pinning (TOFU). Each supported EXILIUM client's signer is pinned on first use, and traffic from a client whose signer changed is refused. Home and Settings show the trust state.
  • Decode amplification budget. At most 32,768 decoded elements per payload and 65,536 across held pre-identity payloads.
  • Restore rebuild. A .gf2backup is rebuilt into the app's own DDL with row-content validation. Live databases that still carry DDL from a restore made before 2.7.2 are rebuilt on first access.
  • Native zdtun fixes. FIN ordering, closing flows whose socket cannot be opened, purge priority at the socket cap, and DNS byte order. Each has a host C regression test (app/src/test/cpp/run-zdtun-host-test.sh).
  • The webhook token is no longer written to failures or logcat. A refused or unopenable export target is never deleted.

Data integrity

  • A database found damaged is quarantined, not silently recreated, and the UI offers a rebuild.
  • Schema v14 sets aside rosters captured with the phone clock set ahead. Activity-dated membership boundaries become their own evidence source. The upgrade repairs stale active flags, duplicate and shadow periods, and members stored without a real UID.
  • CSV import: the undo seal is now a content digest. Re-imported rosters are recognized, and interrupted imports from the older schema still roll back.
  • Weekly history: per-zone rebuild on restore, override domain checks, no stale revisions after a member delete.

UI/UX and input

  • Drafts survive recreation and stay bound to their Platoon.
  • Accessibility: announcements, tooltips, large fonts.
  • Validated UID, Platoon ID and date inputs. CSV formula-prefix escaping is reversible.
  • Standby tap and lock safety, plus clearer notices.

Refactor

One helper each for the game ID range, SQLite identifier quoting and the app-table filter, roster file naming, export failure messages, the screen-lock check and the latest-roster read. The legacy Platoon now has the same commit-or-revert preference writes as a scoped Platoon.

Behaviour changes to note

  • On upgrade, scheduled or standby runs record "client not trusted" until Home has been opened once to pin the installed clients.
  • A CSV undo checkpoint sealed by 2.7.1 is refused once.
  • The Updates correlation window now applies at 48h as intended, which can move a correlated join that older versions placed outside it.
  • At the socket cap, a new connection is reset immediately.

Known, deliberately not changed

  • The 60 s native TCP idle timeout (sec-native-forward-path-m1) needs real gap measurements before it is changed.
  • Standby screen-on behaviour (sec-components-2, sec-components-m1) depends on whether the game keeps the screen on. The standby text was clarified instead.

Test plan

  • ./gradlew :protocol:test :app:testDebugUnitTest :app:lintDebug: protocol 67 tests and app unit tests green, 0 lint errors
  • :app:connectedDebugAndroidTest on emulator (Android 14 AVD): 171/171
  • app/src/test/cpp/run-zdtun-host-test.sh: passed
  • On-device check on the Note10+ (standby rehearsal, first-run signer pinning)

Parse failures no longer quote the pasted URL, background failures log only
the exception type, the PTB, Canary and discordapp.com hosts are stored as
discord.com, and a Keystore or storage failure no longer claims the address
is invalid.

Findings: sec-egress-secrets-1, q-uiux-11, q-uiux-m1
Both sides are compared after NFC, whitespace collapsing and removal of
control, format and bidi characters and unpaired surrogates; suggestions are
off for the field, and a name with nothing typeable is confirmed by its ID.

Findings: q-input-9, sec-protocol-3
A refused socket only set CONN_STATUS_SOCKET_ERROR, so zdtun_conn_close
returned early and on_connection_close never ran: the flow state leaked,
Java never saw onFlowClosed, and the client got no RST. Close through
zdtun_conn_close instead, with the RST acknowledging the SYN.

Fixes sec-native-forward-path-1.
The forced purge destroyed the oldest connections whatever their
protocol, so a burst of other in-VPN sockets could RST the game's quiet
capture connection. Rank connections instead: socketless ones first and
uncounted, then UDP/ICMP, then TCP, and last the flows gf2capture still
inspects (new optional on_purge_priority callback).

Fixes sec-native-forward-path-2.
Fixes sec-native-forward-path-3.
A FIN+ACK half-closed the server socket at once, dropped the FIN
segment's own payload and ACKed bytes still queued, which later failed
with EPIPE and reset the connection. Queue the payload and defer the
shutdown and its ACK until the queue drains; if the server closes first,
ACK the pending FIN so the client can finish. A FIN retransmitted after
it was ACKed is ACKed again without being counted twice.

Fixes sec-native-forward-path-4.
zdtun.c is compiled into a host test with in-memory fake sockets;
run app/src/test/cpp/run-zdtun-host-test.sh on Linux or macOS. Also
list the local zdtun modifications in THIRD_PARTY_NOTICES.md.

Covers sec-native-forward-path-1, -2, -3 and -4.
…rations

The legacy global region seeds each client's capture region at most once,
so clearing it after the last Platoon is deleted sticks. A settings replay
keeps an automatic reset region automatic, never restores another
publisher's region onto a Platoon, and the picker offers only that
publisher's servers. A settings migration that cannot be written no longer
crashes every cold start.

Findings: input-settings-prefs-3, input-settings-prefs-4,
input-settings-prefs-m2, input-settings-prefs-6
A field is prefilled only with an exact value or an unsaved draft, and the
captured notation (≥90, ?) becomes its hint, so typing the shown number
confirms it instead of being dropped as unchanged.

Fixes q-uiux-2.
…s visible

Login, patrol, score and attempt warnings are now underlined and announced as
below cutline, not only colored. The Gunsmoke score and attempts line may wrap
to a second line at large font sizes instead of being clipped, the UID line
ellipsizes, and the evidence sheet speaks values the way the cell does.

Fixes ui-a11y-fontscale-1, ui-a11y-fontscale-3, ui-a11y-fontscale-9.
The deletion now shows a failure message and reloads the list when the
repository returns false or throws.

Fixes q-uiux-13.
…orage

A drag that ends where it started no longer turns the weekly ranking into a
saved join-date order, the screen says when the tables are still ranked, and
the reset wording no longer promises join-date order. Saving keeps the stored
places of members who are not active. Reset reports a failed clear, and a
failed commit of the member order or the cutlines puts the previous values
back in memory so later reads match what was saved.

Fixes input-settings-prefs-1, input-settings-prefs-2, input-settings-prefs-5,
input-settings-prefs-7.
Each exported row grows to its longest cell, up to six lines, and a cell
that still does not fit ends with an ellipsis. The Korean evidence summary
drops the line break that Canvas.drawText cannot draw.

Fixes ui-a11y-fontscale-10, ui-status-render-5, ui-status-render-4.
Information rows such as Version are no longer exposed as buttons, a
renamed navigation or clear button updates its tooltip, re-applied top-bar
actions keep their buttons so screen-reader focus survives Select all, a
changed top-bar title and every loading, empty and failure state are
announced, roster details wrap instead of hiding last login, and boundary
date and time fields stack at large font sizes instead of clipping digits.

Findings: q-uiux-12, ui-a11y-fontscale-2, ui-a11y-fontscale-5,
ui-a11y-fontscale-6, ui-a11y-fontscale-7, ui-a11y-fontscale-8
Every delta measured from a baseline just before 05:00 now goes through
the prefix/tail split, narrowed by login times and Daily Patrol events at
both captures, and a capture after Sunday with an earlier login makes the
Standard Sunday 0 like every other day.

Findings: q-weekly-engine-1, q-weekly-engine-m1
Membership date and time pickers, the Platoon member list and member details
show a server-time caption whenever the game reset zone's offset differs
from the device's, so a boundary is not entered hours off.

Findings: q-time-boundaries-4
An uncapped capture shortly before the Gunsmoke Sunday's 05:00 reset can
miss Saturday's last login or Daily Patrol. The anchor is now closed only
at the reset itself, with the capped preceding-week counter, or after
Saturday's first Daily Patrol.

Finding: q-weekly-engine-2
Flow closes now bypass the chunk budget, so a closed flow always releases
its parser, metadata and pre-identity payloads. A flow whose open was
refused is tainted instead of later failing client verification.

Before identity, only roster, activity and updates payloads can reject a
flow; other payloads are held for enabled history under their own budget
or discarded, so a flow of no Platoon no longer warns about an overflow.

Payloads keep the time they arrived through admission and replay, so a
roster confirmed after the reset is still dated before it.

Findings: data-capture-handoffs-2, data-capture-handoffs-3,
data-capture-handoffs-4, data-capture-handoffs-m3, q-data-3
A scope whose interrupted import or undo could not be rolled back would
later be restored over whatever capture ingested meanwhile. Capture start
now fails with the history import notice instead, and retries recovery on
the next start.

Findings: data-lifecycle-races-m1, data-sqlite-corruption-poison-3
Traffic counters no longer replace a warning or error and appear only
while running. During teardown, drained work cannot replace the stop
reason unless it is more severe, and a second stop without a reason keeps
the first one. A revoked VPN now says so, and standby records it as
missing VPN consent.

A partial one-time capture names its missing items with the checklist
labels, and one completed at the grace deadline ends as complete. The
checklist shows only while capturing or beside its outcome; its rows are
built once and updated in place, and each new warning or error is
announced once.

Findings: ui-status-render-1, ui-status-render-2, ui-status-render-3,
ui-status-render-m1, ui-status-render-6, ui-status-render-7,
ui-status-render-8, ui-a11y-fontscale-4
A document the picker created is deleted when its export fails, is refused,
or can no longer be written; a refused provider says which locations work.
Toasts, the restore refresh and PNG cache cleanup now run once for every
finished task even if the screen was recreated meanwhile. A refused VPN
consent on Home explains why capture did not start.

Findings: ui-process-death-results-1, ui-process-death-results-2,
ui-process-death-results-3
Decoding charges each submessage and repeated scalar to a per-payload
budget, and the parser applies the same budget to a merged continuation
sequence, discarding and quarantining it like the byte limit. Pending
continuations are concatenated once on emission instead of on every
frame. Pre-identity and admission buffers also weigh payloads by their
decoded elements, and payloads whose history is off are no longer held
before identification.

Findings: sec-capture-1, sec-capture-m2, sec-protocol-1
A name, announcement or declaration longer than its limit no longer
ends in an unpaired high surrogate when the cut lands inside a pair.

Findings: sec-protocol-m2
Manual merit is capped at a Standard day's 90 or a generous Gunsmoke
bound and score at one billion, so no weekly sum can overflow. A Login or
Patrol 'no' that a fact or positive score disproves is refused in the
editor, and a stored one no longer constrains the Gunsmoke solver.

Findings: q-weekly-engine-3, q-weekly-engine-4
The formatter writes into a bounded buffer and gives up as soon as the
text would pass the entry limit, so a crafted payload can no longer
allocate its whole oversized text before the 2 MiB check rejects it.

Findings: sec-capture-m1
AtomicFiles and the roster CSV writer now sync the temporary file's data
before the rename, as the import store already does, so a power loss
cannot leave a zero-length or truncated file under its final name.

Findings: data-sqlite-corruption-poison-m2
Each entry ID now carries a sequence one above the highest already in the
directory, and rotation deletes the lowest sequences first, never the
entry just saved. Entries from earlier versions, whose counter was
process-local, count as recorded first. A clock moved backwards therefore
no longer deletes each new capture as soon as it is written.

Findings: data-doc-contracts-8
The CSV tokenizer no longer normalizes line endings across the whole
document: an unquoted CR, LF or CRLF ends a record, while a carriage
return inside a quoted name is kept, so retained rosters read back the
same name live capture stored. The history envelope is split without
touching the body.

The packet table's row and cell limits are derived from the roster
limits, so a valid 227 to 256 member roster renders and uploads again.

Findings: sec-protocol-m1, sec-protocol-m3
@1window2
1window2 merged commit 6579f07 into main Nov 6, 2026
5 checks passed
@1window2
1window2 deleted the v2.7.2 branch November 6, 2026 23:58
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