On Sway 1.12 + Qt 6.9 (Guix, NVIDIA) Qt-Wayland deadlocks before mapping:
WAYLAND_DEBUG shows the client completes xdg_toplevel setup but never sends
the initial wl_surface.commit, so the compositor never sends configure and
the surface never maps — the event loop runs, the app prints its startup
notice, and no window ever appears. This reproduces with a bare PySide6
QLabel, so it is a toolkit/compositor bug, not ours; no Qt env knob
(fractional-scale disable, scale pinning) unblocks it, while the same
window maps instantly on XWayland.
Fix: a map watchdog on Wayland platforms. An event filter LATCHES the first
Expose on the toplevel QWindow (sampling isExposed() at a deadline would
misfire: a healthy hidden window — other workspace, scratchpad, locker —
reads unexposed ~100 ms after frame callbacks stop). If no expose ever
arrived after 4 s, re-exec the same process with QT_QPA_PLATFORM=xcb.
Safety rails: a sentinel env var (VAPTVUPT_XCB_FALLBACK_DONE) makes a
second fallback impossible even if '-platform wayland' argv (which outranks
the env override) brings the child up on Wayland again; execve failure is
caught and degrades to the no-fallback message; frozen bundles reuse argv
as-is (PyInstaller sets argv[0] to the exe); DISPLAY-unset systems just get
an honest notice; VAPTVUPT_NO_XCB_FALLBACK=1 opts out.
Verified live: wayland launch relaunches at 4 s and the window appears in
the sway tree (title 'VaptVupt 5.0.0', visible, tiled) — first time the GUI
is actually on screen on this machine; latch flips true where expose events
exist (offscreen); sentinel path stays wayland with no exec; selftest OK.
e15329a's window-activation code crashed the GUI at startup on Wayland:
raise_()/activateWindow()/move() go through Qt-Wayland's xdg restack/
xdg-activation paths, which SEGSEGV in PySide6 6.9 as shipped on Guix
(faulthandler pinpointed win.raise_(); QT_QPA_PLATFORM=wayland reproduces,
--selftest — plain show() — never crashed). Gate the whole center/raise/
activate block behind platformName() == "xcb": on X11 it is the canonical
show-then-activate sequence and verified working under XWayland; on Wayland
the compositor maps and focuses the new toplevel itself (raise/activate were
no-ops there even before they crashed). Strict equality keeps wayland-egl
and any unknown platform on the safe plain-show path. Centering is computed
before show() again, as pre-e15329a, so X11 windows map already-centered.
Also make every sys.stderr access None-safe: PyInstaller --windowed (>=5.8,
i.e. the shipped Windows .exe) sets sys.stderr = None, so the unguarded
startup notice would have raised AttributeError right after show() and
killed the window; a dead pipe similarly raises on flush. Guard the notice
(+ try/except OSError), the import-error message, and the debug discovery
echo. Verified: launch stays alive on wayland, xcb, and offscreen-with-
fd2-closed; --selftest passes offscreen/wayland incl. VAPTVUPT_DEBUG=1
with stderr closed.
Center the main window on the active screen and raise()/activateWindow()
it on launch, so it cannot open off-screen or behind the focused frame on
a tiling compositor (Sway/i3/Hyprland) — the usual cause of reports that
'the GUI won't start / is stuck': it launched, but was not visible.
Add lightweight non-GUI flags handled before the QApplication is built:
--version print GUI + pinned-CLI version and Qt binding
--help usage
--selftest build the full UI, spin the event loop once, exit 0
so a successful launch can be verified with no display (headless/CI/remote).
Also emit a one-line 'window open' notice to stderr so a terminal launch
is not mistaken for a hung shell.
Version bumped to 5.0.0 across include/zupt.h, all packaging recipes, man
page, and docs.
Audit fixes (pre-5.0.0 review):
- src/zupt_format.c: overflow-safe bound in the solid-mode `test` path
(off+sz could wrap and drive an OOB read in zupt_xxh64 on a crafted archive;
the extract path was already hardened, the test path was not).
- gui: run_async now marshals the completion callback onto the GUI thread with
QueuedConnection (a bare functor connected DirectConnection and touched
widgets off the worker thread); Extract auto-detect note survives the log
clear via a new `info` param.
- .github/workflows/ci.yml: trigger on `master` (was main/develop, so CI never
ran); `make dist` tarball is vaptvupt-*.tar.gz not zupt-*; the ASAN PQ
round-trip uses native --pq (was --pq-sdk, which fails on the source-only
build and blocked the release job).
Documentation:
- New AUDIT.md (methodology, FIPS 203 conformance validation, findings, repro).
- CHANGELOG 5.0.0 entry covers the FIPS 203 conformance fix + BREAKING note and
the GUI/CLI/security/packaging work.
- README "What's new in 5.0.0", download tables (incl. Windows/macOS/BSD +
portable GUI), version-history row.
- SECURITY.md + THREAT_MODEL.md: ML-KEM-768 documented as FIPS 203, validated
byte-for-byte against OpenSSL 3.5.
- Accuracy fixes: man page (--kdf default is PBKDF2 on source-only; codec
2.60.4), rpm %description, debian control/copyright, homebrew header
(no vendored library on source-only builds).
make check 16/16 (FIPS 203 conformance 3/3, all distro-safe checks).
Fixes the "GUI functions don't work on GNU Guix" report and a batch of
adversarial-audit findings across the CLI, decoder, crypto and packaging.
GUI (gui/src/zupt_gui.py)
- Root cause of the broken GUI: every encryption path defaulted to the
libzuptsdk "SDK v2" modes, which are absent from the source-only build and
exit 1. Reworked Keys/Compress/Extract around the native modes with a
build-aware PQ-mode selector: Hybrid (--pq, default), Full PQ (--pq-only),
and SDK v2 only when the binary reports WITH_SDK support. Capability is
detected from `version` ("Build:"/"KDF:" lines) with a `help` fallback.
- Extract/Verify gain a PQ private-key input with auto-detect (reads the
archive via `info` to pick --pq vs --pq-only). Verify could not verify any
PQ archive before (password field only).
- run_async now holds a LIST of in-flight (thread, worker) refs; DiskTab's two
buttons previously shared one slot, so a second op GC'd the first QThread
mid-run. About tab corrected (codec 2.60.4, PBKDF2 default, --pq-only, URL).
CLI (src/zupt_main.c)
- compress -p <archive> <files> swallowed the archive name as the password and
truncated the first input file (silent data loss, exit 0). Added a
data-loss guard: refuse to overwrite an existing non-.zupt file as the output
archive unless -y/--force; plus a self-overwrite guard.
- compress <archive> <src> -p <pw> wrote an UNENCRYPTED archive (exit 0)
because options after the first positional were treated as files. Now errors
on a misplaced option (with a `--` escape for real dashed filenames).
- Removed duplicated dead --pq-box/--pq-sdk branches. version/banner/usage now
state the build's real KDF (PBKDF2 on source-only) and repo URL; examples
lead with native --pq / --pq-only.
Security (audit findings)
- vv_decoder.c: the two AVX2 fast-path token decoders read a 2-/3-byte match
offset after only checking 1 byte remained -> heap over-read on a crafted
archive. Added the `ip + off_bytes > ip_end` guard the tail path already had.
- zupt_crypto.c: wipe ML-KEM/X25519 secret-key buffers when hybrid decrypt
init fails on key read (matches the pq-only path).
- zupt_format.c: bound attacker-controlled encryption_header_off before the
(off_t)+7 arithmetic in the info enc_type reader (avoid signed-overflow UB).
Packaging (would fail source-only)
- debian/rules (staged into debian/zupt, installed vendored .so), aur/PKGBUILD,
nix/flake.nix, homebrew (vendored .so + nonexistent AUDIT.md, stale /zupt
URLs) now build source-only. opensuse spec %files ships the shell completions
make install writes (rpmbuild no longer fails on unpackaged files);
_service + debian/control point at the vaptvupt repo; KDF claim corrected.
Cross-platform GUI packaging (new)
- packaging/portable/: OS-agnostic GUI package (zupt_gui.py + .bat/.command/.sh
launchers + README) that runs on Windows/macOS/Linux/BSD with Python+PySide6.
- .github/workflows/cross-platform.yml: builds real native binaries on
windows-latest + macos-latest runners (CLI, PyInstaller GUI, Inno Setup
installer, .dmg) and the portable zip, attaching them to the release on tag.
- packaging/windows/vaptvupt-gui.iss: Inno Setup installer script.
Validation: make check 16/16 (all distro-safe checks), GUI imports + command
contracts verified against the fixed CLI. test_help_consistency updated to
assert the truthful (build-aware) default KDF.
Major release. Highlights:
- Codec: vendored VaptVupt codec moves to canonical 2.60.4 security
release. Fixes a high-severity OOB heap write in the AVX2 decode fast
path (reachable on a valid stream sized to exactly content_size, both
tail variants). Brings CBMC-formally-verified BCJ filters with
automatic ELF/PE/Mach-O detection. Compressed output stays
byte-identical (ratio gate Δ 0.00%); wire format unchanged at v1.6.
- New --pq-box sealed-box recipient mode (vendored libpqvaptvupt 0.6.0):
ML-KEM-768 + X25519 combined via HKDF-SHA256 with domain separation,
AES-256-CTR + HMAC-SHA256 EtM. Legacy --pq and --pq-sdk stay readable.
- F-16: discloses and fixes a pre-existing data-loss defect in the
<= 3.8.0 in-tree BCJ encoder. Full back-compat matrix decodes
byte-exact under 4.0.0; every readable pre-4.0 archive remains readable.
Repository hygiene:
- Sync full 4.0.0 source tree (codec, crypto, SDK, GUI, packaging, tests).
- Remove internal scratch files (PROMPT.md, FORMAL_AUDIT_PROMPT.md)
and superseded version-specific docs (INTEGRATION_PROTOCOL_2.60.4.md,
docs/FINDINGS-2.x.md) and a stray test binary.
- Refresh README download/install section to real 4.0.0 release assets;
bump version badge to 4.0.0.
- Add .gitignore for build outputs (keeps vendored prebuilt libraries).