Compare commits

...
Author SHA1 Message Date
Cristian Cezar Moisés
f8844f806a docs: record audited 5.2.8 release evidence
Some checks are pending
CI / Source-only, license, shell and secret policy (push) Waiting to run
CI / Build and full tests (clang) (push) Blocked by required conditions
CI / Build and full tests (gcc) (push) Blocked by required conditions
CI / Strict warnings (clang) (push) Blocked by required conditions
CI / Strict warnings (gcc) (push) Blocked by required conditions
CI / ASan, LSan and UBSan (push) Blocked by required conditions
CI / GCC static analyzer (push) Blocked by required conditions
CI / Reproducible audited source archive (push) Blocked by required conditions
CI / Debian/Ubuntu source-built package (push) Blocked by required conditions
CI / openSUSE Tumbleweed x86_64 RPM gate (push) Blocked by required conditions
CI / Fedora noarch GUI RPM and SRPM gate (push) Blocked by required conditions
CI / Linux x86_64 notice-bearing CLI tar.xz gate (push) Blocked by required conditions
CI / Source-only GUI portable ZIP gate (push) Blocked by required conditions
CI / Windows and macOS release gates (push) Blocked by required conditions
CI / Windows x86_64 package and smoke test (push) Blocked by required conditions
CI / macOS native DMG and installed-image test (push) Blocked by required conditions
2026-08-31 22:28:57 -03:00
Cristian Cezar Moisés
33eb90454d packaging: validate genuine source RPM metadata
Some checks failed
CI / Source-only, license, shell and secret policy (push) Has been cancelled
CI / Build and full tests (clang) (push) Has been cancelled
CI / Build and full tests (gcc) (push) Has been cancelled
CI / Strict warnings (clang) (push) Has been cancelled
CI / Strict warnings (gcc) (push) Has been cancelled
CI / ASan, LSan and UBSan (push) Has been cancelled
CI / GCC static analyzer (push) Has been cancelled
CI / Reproducible audited source archive (push) Has been cancelled
CI / Debian/Ubuntu source-built package (push) Has been cancelled
CI / openSUSE Tumbleweed x86_64 RPM gate (push) Has been cancelled
CI / Fedora noarch GUI RPM and SRPM gate (push) Has been cancelled
CI / Linux x86_64 notice-bearing CLI tar.xz gate (push) Has been cancelled
CI / Source-only GUI portable ZIP gate (push) Has been cancelled
CI / Windows and macOS release gates (push) Has been cancelled
CI / Windows x86_64 package and smoke test (push) Has been cancelled
CI / macOS native DMG and installed-image test (push) Has been cancelled
2026-08-31 22:12:31 -03:00
Cristian Cezar Moisés
ebb9ab3aa1 packaging: repin canonical 5.2.8 archive
Some checks failed
CI / Source-only, license, shell and secret policy (push) Has been cancelled
CI / Build and full tests (clang) (push) Has been cancelled
CI / Build and full tests (gcc) (push) Has been cancelled
CI / Strict warnings (clang) (push) Has been cancelled
CI / Strict warnings (gcc) (push) Has been cancelled
CI / ASan, LSan and UBSan (push) Has been cancelled
CI / GCC static analyzer (push) Has been cancelled
CI / Reproducible audited source archive (push) Has been cancelled
CI / Debian/Ubuntu source-built package (push) Has been cancelled
CI / openSUSE Tumbleweed x86_64 RPM gate (push) Has been cancelled
CI / Fedora noarch GUI RPM and SRPM gate (push) Has been cancelled
CI / Linux x86_64 notice-bearing CLI tar.xz gate (push) Has been cancelled
CI / Source-only GUI portable ZIP gate (push) Has been cancelled
CI / Windows and macOS release gates (push) Has been cancelled
CI / Windows x86_64 package and smoke test (push) Has been cancelled
CI / macOS native DMG and installed-image test (push) Has been cancelled
2026-08-31 21:37:11 -03:00
Cristian Cezar Moisés
f6bbb95eee docs: use canonical release epoch 2026-08-31 21:36:27 -03:00
Cristian Cezar Moisés
4741d34783 packaging: pin audited 5.2.8 source archive 2026-08-31 21:26:32 -03:00
Cristian Cezar Moisés
a6e330827e docs: record final security gate evidence 2026-08-31 21:25:16 -03:00
Cristian Cezar Moisés
6fd78e0515 test: make Windows Unicode gate byte-exact 2026-08-31 21:25:10 -03:00
Cristian Cezar Moisés
cb60cf0218 packaging: pin final 5.2.8 source archive 2026-08-31 20:56:23 -03:00
Cristian Cezar Moisés
af87e67a87 docs: finalize 5.2.8 checksum record 2026-08-31 20:55:32 -03:00
Cristian Cezar Moisés
7a8e5c5fbd packaging: refresh 5.2.8 source checksums
Some checks failed
CI / Source-only, license, shell and secret policy (push) Has been cancelled
CI / Build and full tests (clang) (push) Has been cancelled
CI / Build and full tests (gcc) (push) Has been cancelled
CI / Strict warnings (clang) (push) Has been cancelled
CI / Strict warnings (gcc) (push) Has been cancelled
CI / ASan, LSan and UBSan (push) Has been cancelled
CI / GCC static analyzer (push) Has been cancelled
CI / Reproducible audited source archive (push) Has been cancelled
CI / Debian/Ubuntu source-built package (push) Has been cancelled
CI / openSUSE Tumbleweed x86_64 RPM gate (push) Has been cancelled
CI / Fedora noarch GUI RPM and SRPM gate (push) Has been cancelled
CI / Linux x86_64 notice-bearing CLI tar.xz gate (push) Has been cancelled
CI / Source-only GUI portable ZIP gate (push) Has been cancelled
CI / Windows and macOS release gates (push) Has been cancelled
CI / Windows x86_64 package and smoke test (push) Has been cancelled
CI / macOS native DMG and installed-image test (push) Has been cancelled
2026-08-31 20:52:54 -03:00
Cristian Cezar Moisés
65bc5bd427 test: close follow-up security gate findings 2026-08-31 20:51:59 -03:00
Cristian Cezar Moisés
69fc26bfec packaging: pin 5.2.8 source archive checksums
Some checks failed
CI / Source-only, license, shell and secret policy (push) Has been cancelled
CI / Build and full tests (clang) (push) Has been cancelled
CI / Build and full tests (gcc) (push) Has been cancelled
CI / Strict warnings (clang) (push) Has been cancelled
CI / Strict warnings (gcc) (push) Has been cancelled
CI / ASan, LSan and UBSan (push) Has been cancelled
CI / GCC static analyzer (push) Has been cancelled
CI / Reproducible audited source archive (push) Has been cancelled
CI / Debian/Ubuntu source-built package (push) Has been cancelled
CI / openSUSE Tumbleweed x86_64 RPM gate (push) Has been cancelled
CI / Fedora noarch GUI RPM and SRPM gate (push) Has been cancelled
CI / Linux x86_64 notice-bearing CLI tar.xz gate (push) Has been cancelled
CI / Source-only GUI portable ZIP gate (push) Has been cancelled
CI / Windows and macOS release gates (push) Has been cancelled
CI / Windows x86_64 package and smoke test (push) Has been cancelled
CI / macOS native DMG and installed-image test (push) Has been cancelled
2026-08-31 20:33:52 -03:00
Cristian Cezar Moisés
7687cfa577 release: prepare ZUPT 5.2.8 2026-08-31 20:32:42 -03:00
Cristian Cezar Moisés
d8668e6d64 security: close filesystem race findings 2026-08-31 20:27:14 -03:00
Cristian Cezar Moisés
8cca841258 packaging: pin 5.2.7 source archive checksums [skip ci] 2026-08-31 19:07:55 -03:00
Cristian Cezar Moisés
cb794e100c release: prepare ZUPT 5.2.7 native test gates 2026-08-31 19:05:55 -03:00
46 changed files with 1703 additions and 404 deletions

View file

@ -74,6 +74,7 @@ jobs:
tests/test_block_type_confusion.sh \
tests/test_disk_device_capacity.sh \
tests/test_f09_preface.sh \
tests/test_key_files.sh \
tests/test_legacy_disk_5_2_1.sh \
tests/test_path_traversal.sh \
tests/test_pqbox.sh \
@ -121,6 +122,8 @@ jobs:
run: make CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 check
- name: Extended upstream tests
run: make CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 test-all
- name: In-tree SDK atomic key-save regression
run: make CC=${{ matrix.cc }} V=1 sdk-test
- name: Functional test of the built CLI
run: bash scripts/test-installed-zupt.sh "$PWD/zupt"
@ -466,6 +469,12 @@ jobs:
test "$(rpm -qp --qf '%{ARCH}' "$gui_rpm")" = noarch
rpm -qp --requires "$gui_rpm" | grep -Fx "zupt >= $version"
test "$(rpm -qp --qf '%{NAME}' "$gui_srpm")" = zupt-gui
test "$(rpm -qp --qf '%{VERSION}-%{RELEASE}' "$gui_srpm")" = "$version-1"
test "$(rpm -qp --qf '%{SOURCEPACKAGE}' "$gui_srpm")" = 1
test "$(rpm -qp --qf '%{SOURCERPM}' "$gui_srpm")" = '(none)'
test "$(rpm -qpl "$gui_srpm" | wc -l)" -eq 2
rpm -qpl "$gui_srpm" | grep -Fx "zupt-gui-$version.tar.gz"
rpm -qpl "$gui_srpm" | grep -Fx zupt-gui.spec
- name: Build the matching Fedora CLI RPM
run: |
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)

View file

@ -86,20 +86,33 @@ jobs:
trap 'chmod -R u+rwX "$test_root" 2>/dev/null || true; rm -rf -- "$test_root"' EXIT
output_dir="$test_root/saída-安全"
archive="$test_root/cópia-安全.zupt"
emoji_name=$'emoji-\xF0\x9F\x98\x80.bin'
mkdir -p "$test_root/input/subdir" "$output_dir"
printf 'Windows release smoke test\n' > "$test_root/input/café.txt"
printf 'UTF-8: café-安全\n' > "$test_root/input/subdir/ação-安全.txt"
printf 'emoji filename\n' > "$test_root/input/subdir/emoji-😀.bin"
printf 'emoji filename\n' > "$test_root/input/subdir/$emoji_name"
: > "$test_root/input/empty"
dd if=/dev/urandom of="$test_root/input/subdir/random.bin" bs=4096 count=4 2>/dev/null
(cd "$test_root" && "$exe" compress "$archive" input)
"$exe" test "$archive"
"$exe" list "$archive" > "$test_root/list.txt" 2>&1
grep -F 'café.txt' "$test_root/list.txt"
grep -F 'ação-安全.txt' "$test_root/list.txt"
grep -F 'emoji-😀.bin' "$test_root/list.txt"
"$exe" extract -o "$output_dir" "$archive"
diff -r "$test_root/input" "$output_dir/input"
python3 - "$test_root/list.txt" <<'PY'
import pathlib
import sys
listing = pathlib.Path(sys.argv[1]).read_bytes()
expected = {
"Latin-1": bytes.fromhex("636166c3a92e747874"),
"BMP": bytes.fromhex("61c3a7c3a36f2de5ae89e585a82e747874"),
"non-BMP": bytes.fromhex("656d6f6a692df09f98802e62696e"),
}
missing = [label for label, name in expected.items() if name not in listing]
if missing:
raise SystemExit("list output is missing exact UTF-8 names: " +
", ".join(missing))
PY
objdump -p "$exe" > "$test_root/imports.txt"
if grep -Eqi '(vendor[/\\]|libvuptsdk|libpqvaptvupt|libgcc_s|libstdc\+\+|libwinpthread|msys-2[.]0|cygwin1)[^[:space:]]*[.]dll' \
"$test_root/imports.txt"; then

View file

@ -11,7 +11,7 @@ on:
required: true
type: number
tag:
description: Existing annotated release tag, for example v5.2.6
description: Existing annotated release tag, for example v5.2.8
required: true
type: string
@ -389,11 +389,17 @@ jobs:
[[ $(rpm -qp --qf '%{VERSION}' "$rpm_file") == "$VERSION" ]]
[[ $(rpm -qp --qf '%{RELEASE}' "$rpm_file") == 0 ]]
[[ $(rpm -qp --qf '%{ARCH}' "$rpm_file") == x86_64 ]]
[[ $(rpm -qp --qf '%{SOURCEPACKAGE}' "$rpm_file") == '(none)' ]]
[[ $(rpm -qp --qf '%{SOURCERPM}' "$rpm_file") == "$srpm_name" ]]
srpm=$asset_dir/$srpm_name
[[ $(rpm -qp --qf '%{NAME}' "$srpm") == zupt ]]
[[ $(rpm -qp --qf '%{VERSION}' "$srpm") == "$VERSION" ]]
[[ $(rpm -qp --qf '%{RELEASE}' "$srpm") == 0 ]]
[[ $(rpm -qp --qf '%{ARCH}' "$srpm") == src ]]
[[ $(rpm -qp --qf '%{SOURCEPACKAGE}' "$srpm") == 1 ]]
[[ $(rpm -qp --qf '%{SOURCERPM}' "$srpm") == '(none)' ]]
[[ $(rpm -qpl "$srpm" | wc -l) -eq 2 ]]
rpm -qpl "$srpm" | grep -Fx "zupt-$VERSION.tar.gz"
rpm -qpl "$srpm" | grep -Fx zupt.spec
gui_deb=$asset_dir/$gui_deb_name
dpkg-deb --info "$gui_deb" >/dev/null
@ -406,12 +412,18 @@ jobs:
[[ $(rpm -qp --qf '%{VERSION}' "$gui_rpm") == "$VERSION" ]]
[[ $(rpm -qp --qf '%{RELEASE}' "$gui_rpm") == 1 ]]
[[ $(rpm -qp --qf '%{ARCH}' "$gui_rpm") == noarch ]]
[[ $(rpm -qp --qf '%{SOURCEPACKAGE}' "$gui_rpm") == '(none)' ]]
[[ $(rpm -qp --qf '%{SOURCERPM}' "$gui_rpm") == "$gui_srpm_name" ]]
rpm -qp --requires "$gui_rpm" | grep -Fx "zupt >= $VERSION"
gui_srpm=$asset_dir/$gui_srpm_name
[[ $(rpm -qp --qf '%{NAME}' "$gui_srpm") == zupt-gui ]]
[[ $(rpm -qp --qf '%{VERSION}' "$gui_srpm") == "$VERSION" ]]
[[ $(rpm -qp --qf '%{RELEASE}' "$gui_srpm") == 1 ]]
[[ $(rpm -qp --qf '%{ARCH}' "$gui_srpm") == src ]]
[[ $(rpm -qp --qf '%{SOURCEPACKAGE}' "$gui_srpm") == 1 ]]
[[ $(rpm -qp --qf '%{SOURCERPM}' "$gui_srpm") == '(none)' ]]
[[ $(rpm -qpl "$gui_srpm" | wc -l) -eq 2 ]]
rpm -qpl "$gui_srpm" | grep -Fx "zupt-gui-$VERSION.tar.gz"
rpm -qpl "$gui_srpm" | grep -Fx zupt-gui.spec
linux_tar=$asset_dir/$linux_tar_name
python3 - "$linux_tar" "zupt-$VERSION-linux-x86_64" <<'PY'

135
AUDIT.md
View file

@ -1,12 +1,12 @@
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
# ZUPT 5.2.6 audit guide and finding history
# ZUPT 5.2.8 audit guide and finding history
This document describes review surfaces and reproducible checks. It is an
upstream self-review, not an independent audit, certification, or guarantee.
`SECURITY.md` defines reporting policy and `THREAT_MODEL.md` defines the
security boundary.
## 5.2.6 scope
## 5.2.8 scope
The baseline scope is the source-only CLI and its bundled source codec:
@ -28,7 +28,7 @@ output.
## Source-only review
The 5.2.6 baseline retains the source-only boundary introduced in 5.2.2, which
The 5.2.8 baseline retains the source-only boundary introduced in 5.2.2, which
removed incomplete SDK/PQBOX header snapshots and local precompiled-library
expectations. Git and new upstream source
archives are intended to contain no compiled executable, object, shared/static
@ -42,10 +42,10 @@ scripts/check-source-only.sh
# committed Git tree or immutable tag
scripts/check-source-only.sh --tag HEAD
scripts/check-source-only.sh --tag v5.2.6
scripts/check-source-only.sh --tag v5.2.8
# generated source archive
scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz
scripts/check-source-only.sh --archive /path/to/zupt-5.2.8.tar.gz
```
The scanner checks extensions and magic bytes, nested archives, symlink targets,
@ -80,6 +80,7 @@ make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf 1)" \
WITH_SDK=0 WITH_PQBOX=0 V=1
make WITH_SDK=0 WITH_PQBOX=0 check
make WITH_SDK=0 WITH_PQBOX=0 test-all
make sdk-test
```
Relevant review layers include:
@ -92,6 +93,7 @@ Relevant review layers include:
| Archive behavior | quick/regression, traversal, argument-order, block-swap, nonce, and exact-size tests | Exercises current parser, integrity, and round-trip properties |
| Password sources | `tests/test_password_sources.sh` | Exercises password-file, inherited-descriptor and explicit-prompt rejection paths without logging password contents |
| Key files | native key regressions | Exercises no-replace private-file creation, POSIX mode `0600`/Windows current-user-only DACL, failed-partial behavior, checksum, and exact ZKEY/ZPQK version/flags/reserved/size/role validation |
| SDK key publication | `make sdk-test` | Exercises atomic descriptor/handle-backed key copies, POSIX private/public modes, and symlink/hardlink target preservation; this now runs in `release-check` and hosted GCC/Clang Linux CI |
| Terminal output | archive-comment regression | Requires displayed untrusted comments to contain no raw terminal-control sequence |
| Prompt cleanup | PTY signal regression | Requires handled POSIX interruption to restore the saved terminal state |
| Sanitizers | `make test-asan-run` | Builds and executes separate ASan/UBSan/LSan evidence where supported; not a substitute for normal tests |
@ -109,7 +111,7 @@ without evidence.
The following upstream self-audit results apply only to the 5.2.2 candidate at
commit `ff99770` on the recorded local Linux environments. The immutable 5.2.2
tag was not promoted after post-tag CI integration failures. These results are
not independent certification, a 5.2.6 result, or evidence that release assets
not independent certification, a 5.2.8 result, or evidence that release assets
were published.
| Gate | Result | Recorded evidence |
@ -141,7 +143,7 @@ A separate local openSUSE Tumbleweed reproduction resolved the explicit
produced exactly one `zupt-5.2.4.tar.gz`, which passed the source-only scanner.
This isolates a release/test harness defect; it is not evidence of a product,
archive-format, cryptographic, codec, or SDK ABI change. It also does not turn
the skipped native jobs into passes or transfer any result to 5.2.6.
the skipped native jobs into passes or transfer any result to 5.2.8.
## Prior 5.2.5 exact-tag native-gate evidence
@ -157,20 +159,113 @@ Darwin and NetBSD, guard every relevant scanner array, and make the Windows
fixture accept explicit hexadecimal bytes, verify the full requested path in
the archive, and reject each dangerous raw byte fragment anywhere in diagnostic
output. These changes do not alter the archive format, cryptography, bundled
codec, or SDK ABI. They are proposed corrections, not proof that any 5.2.6
native or hosted gate has passed.
codec, or SDK ABI.
A separate local compatibility run executed the corrected scanner with genuine
GNU Bash 3.2.57 in a clean clone. All four exercised modes completed: the
repository audit reported 609 files and one archive; `--tree` reported 204/0;
`--archive` reported 201/1; and `--root` plus `--tag v5.2.5` reported 810/2.
This is targeted scanner compatibility evidence only, not exact-v5.2.6 hosted
CI, package, native-platform, or promotion evidence.
This is targeted scanner compatibility evidence only, not exact-v5.2.6 or
v5.2.8 hosted CI, package, native-platform, or promotion evidence.
The exact 5.2.6 candidate must repeat the required suite. Native Windows and
macOS gates, hosted GitHub CI and release promotion, authenticated OBS
validation, and resolution of the openSUSE automatic `debugsource` rpmlint
`no-binary` finding remain pending until recorded otherwise.
## Prior 5.2.6 exact-tag native-gate evidence
The immutable `v5.2.6` candidate was not promoted. Exact-tag GitHub Actions run
`33442264243` completed 13 jobs successfully and failed two native jobs. On
macOS arm64, the strict SHA-NI regression build diagnosed x86-only helper
declarations as unused under `-Werror`. On Windows, argv transcoding of the safe
printable UTF-8 fixture caused the path regression to abort before its intended
archive and diagnostic assertions.
The 5.2.7 changes scope those helper declarations to supported x86 builds and
carry the safe UTF-8 fixture across the Windows argument boundary without
locale-dependent byte conversion. These are test/release integration changes,
not archive-format, cryptographic, codec, or SDK ABI changes.
## Prior 5.2.7 exact-tag native-gate evidence
The immutable `v5.2.7` candidate was not promoted. Exact-tag GitHub Actions run
`33445470664` concluded `cancelled` at `2026-08-31T23:11:19Z`, with 13
successful jobs, one failed macOS job, and one cancelled Windows job. The macOS
runner filesystem rejected creation of the
raw-C1 filename fixture with `EILSEQ`. The hosted Windows job stalled in `make
check`; a MinGW/Wine reproduction isolated the cause to
`test --password-prompt ... </dev/null` entering `_getch`, and the remaining
native job was cancelled.
The 5.2.8 fixture treats that creation refusal as an explicit `SKIP`; on a
filesystem that accepts the byte, the scanner must still reject the compiled
magic and render the path without emitting the raw C1 byte. No result from the
immutable tag transfers automatically to 5.2.8. The Windows prompt correction
rejects a redirected/non-console standard input before entering `_getch` and
adds a native EOF regression; it also requires fresh exact-candidate evidence.
## 5.2.8 CodeQL High path-race corrections
CodeQL High #5 identified SDK key-copy publication that reopened the output
path and then applied `chmod` to that mutable name. The copy now uses the core
atomic publisher, sets POSIX mode on the open temporary descriptor, checks
read/close/publication failures, and replaces only the destination directory
entry. `sdk-test` covers symlink and hardlink sentinels and expected key modes.
CodeQL High #6 identified the POSIX disk-restore `lstat`-then-`open` sequence.
Restore now opens once with `O_NOFOLLOW` and without truncation, classifies that
descriptor with `fstat`, and retains the same raw-device descriptor through
capacity checks and writes. Regular-file destinations retain the existing
atomic publisher.
CodeQL High #7 identified benchmark cleanup's `lstat`-then-recursive-path
sequence. POSIX cleanup now pins each component with `openat` and removes leaves
with `unlinkat`; Windows pins ancestors/children, opens reparse points without
following them, recurses only into a plain directory held against rename, and
after emptying a directory reopens it relative to the pinned parent, verifies
its volume and file index against the traversal handle, and marks only that
identity-checked handle for deletion. The live-workspace
regression injects a directory symlink and verifies that its external sentinel
survives. These are reviewed fixes and regression coverage, not independent
certification by themselves; the exact-tag evidence is recorded below.
The C/C++ default-branch analysis of commit `69fc26b` closed #5, #6, and #7,
then reported High #8, #9, and #10 solely in the newly added SDK regression:
its sentinel and mode checks used `stat`/`lstat` before later path operations.
Each individual content or metadata check now opens without following links
and uses `fstat` or reads through that already-open descriptor. A static guard
rejects a return to path-level `stat`/`lstat` in this test. The subsequent
C/C++ default-branch scan run `33452563116` completed successfully at commit
`7a8e5c5`; alerts #5 through #10 are fixed, and the authenticated
code-scanning API reported zero open alerts.
Manual pre-tag CI run `33452602634` at the same functional commit completed 14
of 15 jobs successfully. Linux compilers, analyzers, sanitizers, source policy,
reproducibility, DEB, RPM/SRPM, portable bundles, and the native macOS DMG gate
passed. On Windows, source audit, build, and `make check` passed; the subsequent
smoke failed only when the old MSYS `grep` tried to match a literal non-BMP
filename after ZUPT had compressed and verified all five inputs. Independent
MinGW/Wine reproduction confirmed that the redirected ZUPT listing contained
the exact UTF-8 bytes. The gate now creates the non-BMP name through byte
escapes, verifies Latin-1/BMP/non-BMP listing bytes without locale-sensitive
matching, and requires extraction plus a full tree diff. The path-confinement
regression independently constructs the BMP/non-BMP archive name from ASCII
hex and requires byte-exact listing and extraction.
Because that pre-tag run failed, it remains diagnostic evidence rather than
release approval. The immutable `v5.2.8` tag at
`ebb9ab3aa1d42c50030ca02883f6162dc4771fe1` repeated the complete suite in
manually dispatched run `33456209269`: all 15 jobs passed, including native
Windows/macOS, the pinned local OBS service chain, source reproducibility,
DEB/RPM/SRPM, installed-package, sanitizer, analyzer, and source-only gates.
The canonical source archive was reproduced at 798296 bytes with SHA-256
`378b9506211545b9594cf0d38ac8955d9b1cac34eb6b379ae0ec26b84edb65f7`.
Initial promotion run `33457344882` stopped before release creation because
its validator incorrectly assumed that an SRPM's `%{ARCH}` must be `src`.
Both artifacts were genuine source packages: `%{SOURCEPACKAGE}` was `1`,
`%{SOURCERPM}` was absent, each payload was exactly its Source0 plus spec, and
each binary RPM referenced the matching SRPM. Commit `33eb904` changed the
gate to those canonical metadata and payload checks. Corrected promotion run
`33457868306` then validated the same tag-bound artifacts and published exactly
13 assets. No asset was rebuilt to pass promotion. These are reproducible
project records, not independent certification.
## Cryptographic review boundary
@ -189,7 +284,7 @@ AES implementation has documented cache-timing risk on hostile shared hardware.
The following entries are retained as release history. Their regression tests
should be rerun, but the historical resolution does not itself constitute a
5.2.6 test result.
5.2.8 test result.
| First corrected | Severity | Finding | Resolution recorded at the time |
|---|---|---|---|
@ -209,6 +304,10 @@ should be rerun, but the historical resolution does not itself constitute a
| 5.2.2 | Medium | A signal during an interactive POSIX password prompt could leave terminal echo/state altered | Restore saved terminal settings on handled interruptions; cover the behavior with a PTY regression |
| 5.2.2 | Test reliability | `tests/regression.sh` used Bash syntax without making the interpreter contract explicit | Execute the suite explicitly with Bash and keep syntax/interpreter checks in release gates |
| 5.2.2 | Documentation/licensing | Current documentation incorrectly denied historical MIT grants visible in published Git history | Added a factual erratum: current source follows current SPDX notices, while earlier grants and immutable tags remain valid and unmodified |
| 5.2.8 | High | CodeQL #5: SDK key copies changed permissions through a re-resolved destination path | Publish through the core atomic output object and apply permissions to its open descriptor; run link-target/mode regressions through `sdk-test` |
| 5.2.8 | High | CodeQL #6: POSIX disk restore classified a pathname before reopening it destructively | Open without truncation or symlink following, classify with `fstat`, and retain the same device descriptor through write |
| 5.2.8 | High | CodeQL #7: benchmark cleanup classified entries before recursively resolving their path | Traverse pinned descriptors/handles, refuse link/reparse traversal, remove entries relative to pinned parents, and verify Windows directory identity before handle deletion |
| 5.2.8 | High (test-only) | CodeQL #8/#9/#10: the new SDK regression inspected paths before later reads or cleanup | Open each fixture without following links, inspect/read through `fstat` and the same descriptor, and reject path-level `stat`/`lstat` in the static gate |
See `CHANGELOG.md` for the complete per-release history and compatibility notes.
Old tags remain immutable and may contain artifacts or build assumptions removed
@ -230,12 +329,12 @@ include SHA-256 checksums. The gated GUI set adds the architecture-independent
DEB, noarch/source RPM, and source-only portable GUI ZIP. Package gates include
exact payload/dependency and installed off-screen integration checks; the
portable ZIP additionally receives source scans, an exact safe-member allowlist,
and an extracted launcher test. An AppImage is not promoted by the 5.2.6
and an extracted launcher test. An AppImage is not promoted by the 5.2.8
policy; AppDir and Flatpak bundles, GUI platform installers, and bare
Linux/Windows executables are also excluded. Windows ZIP and macOS DMG outputs
remain CLI-only.
No Wine result is retained as release evidence for 5.2.6. Cross-compilation
No Wine result is retained as release evidence for 5.2.8. Cross-compilation
does not establish native-Windows behavior. Extended-length/device namespace
paths, raw UNC output roots, and mapped/network-drive output are unsupported;
the native Windows workflow remains a publication gate for the ZIP containing

View file

@ -1,5 +1,92 @@
# ZUPT Changelog
## [5.2.8] — 2026-08-31 — Path-race hardening and native-fixture correction
Corrective successor to the immutable, unpromoted `v5.2.7` candidate.
Exact-tag GitHub Actions run `33445470664` reached a macOS failure because the
runner filesystem rejected creation of the raw-C1 filename fixture with
`EILSEQ`. The workflow concluded `cancelled` at `2026-08-31T23:11:19Z`, with
13 successful jobs, one failed macOS job, and one cancelled Windows job.
The hosted Windows job stalled in `make check`; a MinGW/Wine reproduction
isolated the cause to `test --password-prompt ... </dev/null` entering
`_getch` despite redirected standard input. The tag and its evidence remain
unchanged.
- Close CodeQL High #5 in SDK key saving by copying into the core atomic
publisher's already-open private object, applying POSIX key permissions with
`fchmod` on that descriptor, checking read/close/publication failures, and
replacing only the requested directory entry. Symlink and hardlink target
sentinels and private/public key modes are covered by the SDK regression.
- Close CodeQL High #6 in POSIX disk restore by opening the target once without
truncation or final-symlink following, classifying that descriptor with
`fstat`, and retaining the same device descriptor through capacity checks and
writes. Regular-file restores continue to use atomic publication.
- Close CodeQL High #7 in benchmark cleanup by resolving POSIX components with
`openat(..., O_NOFOLLOW)`, deleting relative to pinned descriptors with
`unlinkat`, and using pinned, reparse-point-aware handles for Windows
traversal. The regression injects a directory symlink into a live workspace
and verifies that cleanup does not visit its target.
- The first default-branch rescan closed #5, #6, and #7 and then identified
test-only path checks as High #8, #9, and #10 in the new SDK regression.
Replace every test-side `stat`/`lstat` sequence with one no-follow `open`
followed by `fstat` and descriptor reads, and retain that boundary in the
static regression gate. Default-branch run `33452563116` completed
successfully with #5 through #10 fixed and zero open code-scanning alerts.
- Treat inability to create the raw-C1 scanner filename as an explicit fixture
skip on filesystems that reject the byte; when creation succeeds, the unsafe
diagnostic-escaping assertions still run unchanged. Normalize Bash 3.2's
sign-extended `%d` character conversion to an unsigned octet so raw and
UTF-8 C1 diagnostics retain their canonical `\\xNN`/`\\uNNNN` form.
- Reject redirected or otherwise non-console Windows password prompts before
entering `_getch`, handle console EOF as an error, and cover the native
redirected-input path so it cannot hang a release gate.
- On Windows, validate private-key confinement as the protected,
current-user-only DACL that the implementation creates; do not treat MSYS's
synthetic `stat` mode as a POSIX `0600` result. POSIX continues to require
the real descriptor mode `0600` under multiple umasks.
- Replace the old MSYS `grep` non-BMP boundary exposed after 14 successful jobs
in pre-tag run `33452602634` with byte-exact Python checks for Latin-1, BMP,
and non-BMP UTF-8 listing output. Construct the emoji fixture from ASCII byte
escapes and require list, extract, and full-tree equality; the independent
path regression also creates its BMP/non-BMP name from ASCII hex.
- Run `sdk-test` from both `release-check` and the hosted GCC/Clang Linux job so
the atomic key-save regression cannot silently fall outside release gates.
- Carry the v5.2.7 archive format, cryptography, bundled codec release, and SDK
ABI forward unchanged. These are implementation and test-integration
corrections, not a wire-format or API change.
- Realign current code, package, workflow, artifact, and documentation
references to 5.2.8, and pin the AUR/Homebrew SHA-256 and Guix content hash
to the final reproducible source archive before tagging.
- Exact-tag run `33456209269` passed all 15 source, package, analyzer,
sanitizer, OBS, Windows, and macOS jobs at `ebb9ab3`. Promotion initially
stopped before publication because its validator treated `%{ARCH}` as the
SRPM discriminator; the genuine SRPMs instead correctly carry
`%{SOURCEPACKAGE}=1`. Commit `33eb904` corrected and strengthened that gate,
and run `33457868306` published the exact 13 tested assets without rebuilding
them. No v5.2.7 result transfers to this evidence.
## [5.2.7] — 2026-08-31 — Native test-harness portability corrections
Corrective successor to the immutable, unpromoted `v5.2.6` candidate. Exact-tag
GitHub Actions run `33442264243` completed 13 jobs successfully, but the native
Windows and macOS jobs failed, so no 5.2.6 assets were promoted. The tag and its
recorded evidence remain unchanged.
- Scope the SHA-NI regression helper declarations to the supported x86 build
path so macOS arm64 strict compilation does not diagnose unused declarations
under `-Werror`.
- Carry the safe printable UTF-8 path fixture through an explicit
byte-stable Windows argument representation so argv transcoding cannot abort
the regression before its intended archive and diagnostic assertions.
- Carry the 5.2.6 source-only and security baseline forward without changing
the archive format, cryptography, bundled codec release, or SDK ABI.
- Realign current source, package, workflow, artifact, and tag references to
5.2.7, then pin the final reproducible release-archive checksum and content
hash in the downstream recipes before tagging.
- Require fresh exact-`v5.2.7` source, checksum, hosted CI, native-platform,
package, OBS, and promotion evidence. No prior candidate result transfers
automatically, and this entry does not claim those gates passed.
## [5.2.6] — 2026-08-31 — Native release-gate portability corrections
Corrective successor to the immutable, unpromoted `v5.2.5` candidate. Exact-tag
@ -2411,7 +2498,7 @@ bump and packaging-syntax test expansion; archive format unchanged.
### `.github/workflows/ci.yml` — 8-job CI matrix
Replaces the prior 4-job CI with a comprehensive matrix that mirrors
the project's local-verification protocol from `PROMPT.md §6`:
the project's historical local-verification protocol:
| Job | What it does |
|---|---|
@ -3134,7 +3221,7 @@ size to run. It needs to be a sprint-protocol step, not a one-off.
### What changed
**`PROMPT.md` — Prompt v2.**
**Historical sprint instructions — version 2.**
- **NEW §3.5: The exhaustive byte-sweep mandate.** Every
format-touching change runs the full byte sweep before claiming
@ -3154,7 +3241,7 @@ size to run. It needs to be a sprint-protocol step, not a one-off.
with the regression-test names that catch each one. The table is
the canonical "things that have shipped and must never recur"
reference; keeping it current is part of every sprint.
- **Footer stamp**: Prompt v2, 2026-05-20.
- **Footer stamp**: historical instruction revision 2, 2026-05-20.
**`Makefile`.** The help-target banner version is now derived
from `include/zupt.h` via a `grep | awk` substitution:
@ -3165,7 +3252,7 @@ help:
include/zupt.h | awk -F'"' '{print $$2}') build targets:"
```
This closes a recurring bug noted in `PROMPT.md §6 step 9`
This closes a recurring bug noted in the historical sprint checklist
prior sprints (2.3.0, 2.3.1) left the banner stale even after the
sprint protocol said to bump it. Making it auto-derived removes
the drift opportunity entirely.
@ -3196,7 +3283,6 @@ the drift opportunity entirely.
### Files touched
```
PROMPT.md (§3.5 NEW, §6 step renumber, §10 kickoff, §11 rows, v2 stamp)
Makefile (help banner auto-derives version from header)
include/zupt.h (version 2.3.1 → 2.4.0)
CHANGELOG.md (this entry)
@ -3380,7 +3466,7 @@ the footer.
- **Encrypted modes**: AIT = `HMAC-SHA256(mac_key, hdr[0..63] || footer[0..23])`
- **Plaintext modes**: AIT = `XXH64(...)` in the first 8 bytes, zeros in
the rest. Best-effort (`OPAQUE`-class per PROMPT.md §5).
the rest. Best-effort (`OPAQUE` structural-integrity class).
The MAC input deliberately excludes `footer[24..31]` = `"ZEND" || u32 version`.
Both are structurally validated by the read path (`locate_footer_v15`
@ -3570,7 +3656,7 @@ so flipping it (e.g. from `0x02 INDEX` to `0x03 ENC_HEADER`) did
not cause auth failure; the downstream parser was tolerant.
Severity: low. Fix at `src/zupt_format.c` adds the structural
check immediately after `read_block`. This makes the byte
`OPAQUE`-class per PROMPT.md §5 (tamper detected by parser, not by
`OPAQUE` structural-integrity class (tamper detected by parser, not by
MAC).
**F-02b — RECLASSIFIED.** The 2.2.4 hypothesis that the archive
@ -3607,7 +3693,7 @@ other v2.3.0 changes than as a standalone patch.
### Verification
- `make` — clean on plain GCC and Clang.
- `make` with strict GCC flags (full set from PROMPT.md §6) — clean.
- `make` with the historical strict GCC warning set — clean.
- `make` with strict Clang flags — clean.
- `make test`**61/61 passing**.
- `make test-vectors`**14/14 passing**.
@ -3643,8 +3729,8 @@ SECURITY.md (integrity statement reaffirmed)
Patch release. No format changes, no feature changes, no on-disk
compatibility impact. Five findings closed against the v2.2.3 baseline
under the methodology in the new top-level `PROMPT.md` (continuous
improvement prompt) and tracked in `docs/FINDINGS-2.x.md` (durable
under the methodology in the then-current audit instructions and tracked in
`docs/FINDINGS-2.x.md` (durable
numbered ledger that survives between work sessions).
### Findings closed
@ -3716,9 +3802,9 @@ preserved.
### New process artefacts
- **`PROMPT.md`** — top-level "god-tier" continuous-improvement prompt.
Designed to be pasted verbatim into a fresh chat alongside the latest
source tarball. Encodes the methodology that produced this release:
- **Historical continuous-improvement instructions** — removed from the
current source tree after their durable material was consolidated into the
audit and security documents. They encoded the methodology that produced this release:
three-line workflow (survey → fix-with-test → ship), explicit
authentication-coverage invariant (every archive byte covered by
per-block HMAC OR a separate index MAC OR a footer MAC — no third
@ -3766,7 +3852,7 @@ preserved.
### Files touched
```
PROMPT.md (new, top-level)
[historical sprint-instruction file] (removed from the current tree)
docs/FINDINGS-2.x.md (new)
tests/test_audit_flake.sh (new)
src/zupt_main.c (F-01)
@ -3878,20 +3964,20 @@ Two `make test` runs back-to-back, both clean. Cumulative test count:
### Documentation cleanup
Four design/audit-prompt documents that were sprint-internal scratch
Four design/audit-instruction documents that were sprint-internal scratch
have been removed from the source tree (consolidated into the
remaining permanent docs):
| Removed | Where the content lives now |
|---|---|
| `AUDIT_PROMPT.md` | superseded by `FORMAL_AUDIT_PROMPT.md` |
| Two historical audit-instruction files | consolidated into the permanent audit and security documents |
| `ROOT_CAUSE_ANALYSIS.md` | reproducible-bug postmortems are now per-release entries in `CHANGELOG.md` |
| `COMPAT.md` | the table moved into `README.md` § "Architecture & platform support" |
| `DONATIONS.md` | one-liner moved into `README.md` § "Supporting Zupt" |
Surviving canonical docs: `README.md`, `CHANGELOG.md` (this file),
`SECURITY.md`, `INSTALL.md`, `LICENSE`, `THIRD-PARTY-NOTICES.md`,
`AUDIT.md`, `FORMAL_AUDIT_PROMPT.md`, `ROADMAP.md`.
`AUDIT.md` and the then-current roadmap.
## [2.2.2-final2] — 2026-05-01 — CLI help, man pages, deb copyright
@ -4069,7 +4155,7 @@ the github-old → github-new migration sprint) deleted as obsolete.
## [2.2.2] god-tier audit — bug #16 (block-swap attack) fix
Independent formal cryptographic audit (per FORMAL_AUDIT_PROMPT.md two-pass
Independent formal cryptographic audit (using the then-current two-pass
methodology) discovered a critical authenticated-encryption flaw in the
shipped 2.2.2 binary. Investigation, root-cause, fix, regression test, and
final verification documented below.
@ -4268,7 +4354,7 @@ Compile-tested with `-Wpedantic` under GCC. Win32 code paths verified via
- `AUDIT.md`: 2026-04-27 formal audit entry with cumulative test table
- `README.md`: Security section bumped with audit confirmation
- `doc/zupt.1`: SECURITY section mentions path-traversal protection
- `FORMAL_AUDIT_PROMPT.md`: methodology document at repo root for future audits
- the then-current formal-audit methodology document
## [2.2.2] — 2026-04-27

View file

@ -1,4 +1,4 @@
# Distributing ZUPT 5.2.6
# Distributing ZUPT 5.2.8
This document describes the packaging material maintained in the ZUPT
source repository. A recipe in `packaging/` is not evidence that a package has
@ -14,8 +14,8 @@ https://github.com/cristiancmoises/zupt
GitHub is the canonical source and release host. Packaging must never fetch
`zupt-web` or substitute an asset from another project.
The `v5.2.2`, `v5.2.3`, `v5.2.4`, and `v5.2.5` tags are immutable
non-promoted candidates.
The `v5.2.2`, `v5.2.3`, `v5.2.4`, `v5.2.5`, `v5.2.6`, and `v5.2.7` tags are
immutable non-promoted candidates.
The v5.2.3 source-policy test assumed LF for a Windows `.bat` file that Git
correctly checks out as CRLF. Exact-tag GitHub Actions run `33431386002` then
recorded 12 successful v5.2.4 jobs, one openSUSE service-harness failure caused
@ -24,11 +24,30 @@ Tumbleweed reproduction confirmed that `refs/tags/v5.2.4` is valid and that
entering the service directory completes the source-service chain. Corrective
working-directory integration was carried by v5.2.5, whose exact-tag GitHub
Actions run `33434986357` completed 13 jobs successfully but failed the native
Windows and macOS jobs. Corrective packages and release assets must use
`v5.2.6`; never move or overwrite an earlier tag or checksum, and never transfer
prior evidence automatically.
This corrective version changes release/test integration only; the product,
archive format, cryptography, codec, and SDK ABI remain unchanged.
Windows and macOS jobs. Its v5.2.6 corrections reached exact-tag run
`33442264243`, where 13 jobs succeeded but macOS arm64 failed on unused x86
SHA-NI test-helper declarations under `-Werror`, and Windows aborted during safe
UTF-8 fixture argv transcoding. Version 5.2.7 corrected those failures, but
exact-tag run `33445470664` ended with 13 successful jobs, a macOS raw-C1
fixture failure with `EILSEQ`, and a cancelled Windows job after the hosted job
stalled in `make check`; a MinGW/Wine reproduction isolated a non-console
password-prompt hang in `_getch`. Manual 5.2.8 pre-tag run `33452602634`
subsequently passed 14 of 15 jobs, including native macOS and the complete
Windows distribution checks, before an old MSYS `grep` non-BMP pattern failed
in the later smoke. ZUPT's redirected listing was byte-correct; the corrected
gate uses byte-exact, locale-independent checks and requires extraction plus a
full tree diff. The failed run is diagnostic evidence only.
Exact-tag run `33456209269` subsequently passed all 15 jobs at
`ebb9ab3aa1d42c50030ca02883f6162dc4771fe1`, including the pinned local OBS
source-service chain, native
Windows/macOS, and every package gate. Promotion run `33457868306` published
the exact tested 13-file set; the source archive SHA-256 is
`378b9506211545b9594cf0d38ac8955d9b1cac34eb6b379ae0ec26b84edb65f7`.
Corrective packages and release assets use `v5.2.8`; never move or
overwrite an earlier tag or checksum, and never transfer prior evidence
automatically. Version 5.2.8 corrects those native test boundaries, hardens
three path-race boundaries, and adds the SDK regression to release/hosted Linux
gates. The archive format, cryptography, codec, and SDK ABI remain unchanged.
## Source-only boundary
@ -58,7 +77,7 @@ Audit the current tree or a generated archive with:
```sh
scripts/check-source-only.sh
scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz
scripts/check-source-only.sh --archive /path/to/zupt-5.2.8.tar.gz
```
The scanner reports paths, not file contents, and exits nonzero on a violation.
@ -71,14 +90,15 @@ the result before moving it to its destination. Exporting the tree rather than
the commit omits Git's commit-ID PAX header:
```sh
SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" \
make DIST_TARBALL=/tmp/zupt-5.2.6.tar.gz dist
sha256sum /tmp/zupt-5.2.6.tar.gz
make DIST_TARBALL=/tmp/zupt-5.2.8.tar.gz dist
sha256sum /tmp/zupt-5.2.8.tar.gz
```
With identical committed input and `SOURCE_DATE_EPOCH`, repeated exports must
have the same SHA-256 digest. Do not generate a release tarball from uncommitted
working-tree files.
The canonical release uses the tracked `.source-date-epoch`; an explicit
`SOURCE_DATE_EPOCH` override intentionally creates a different archive. With
identical committed input and epoch, repeated exports must have the same
SHA-256 digest. Do not generate a release tarball from uncommitted working-tree
files.
The AUR, Homebrew, and Guix recipes pin the checksum of this tarball. They are
marked `export-ignore` in `.gitattributes` so their own checksum fields do not
@ -119,12 +139,12 @@ private-library RPATH.
| openSUSE / OBS | `packaging/opensuse/` | source and binary RPM through OBS |
| Debian / Ubuntu | `packaging/debian/`, `packaging/build-deb.sh` | Debian metadata and binary DEB after the target gate |
| RPM release artifact | `packaging/opensuse/zupt.spec`, `packaging/build-rpm.sh` | source and binary RPM after the target gate |
| GUI DEB | `packaging/build-gui-deb.sh` | `zupt-gui_5.2.6_all.deb` after payload/dependency and installed integration gates |
| GUI RPM | `packaging/build-gui-rpm.sh` | `zupt-gui-5.2.6-1.noarch.rpm` and matching `.src.rpm` after package and installed integration gates |
| Linux CLI archive | `.github/workflows/ci.yml` | `zupt-5.2.6-linux-x86_64.tar.xz` with notices after dependency, member, and extracted functional gates |
| Portable GUI source | `packaging/portable/`, `.github/workflows/ci.yml` | `zupt-gui-5.2.6-portable.zip` after source scan, member allowlist, and extracted off-screen integration gate |
| GUI DEB | `packaging/build-gui-deb.sh` | `zupt-gui_5.2.8_all.deb` after payload/dependency and installed integration gates |
| GUI RPM | `packaging/build-gui-rpm.sh` | `zupt-gui-5.2.8-1.noarch.rpm` and matching `.src.rpm` after package and installed integration gates |
| Linux CLI archive | `.github/workflows/ci.yml` | `zupt-5.2.8-linux-x86_64.tar.xz` with notices after dependency, member, and extracted functional gates |
| Portable GUI source | `packaging/portable/`, `.github/workflows/ci.yml` | `zupt-gui-5.2.8-portable.zip` after source scan, member allowlist, and extracted off-screen integration gate |
| Fedora / RPM-based systems | `packaging/rpm/zupt.spec` | downstream RPM starting point |
| AppImage helper | `packaging/build-appimage.sh` | downstream-only helper; no 5.2.6 AppImage is promoted |
| AppImage helper | `packaging/build-appimage.sh` | downstream-only helper; no 5.2.8 AppImage is promoted |
| Windows | `.github/workflows/cross-platform.yml` | native ZIP (executable plus notices) after the required native gate |
| macOS | `packaging/build-dmg.sh` | native-architecture DMG after the native gate |
| Arch Linux | `packaging/aur/PKGBUILD` | AUR package recipe |
@ -200,17 +220,17 @@ expectations, then test the installed launcher off-screen against the matching
### Portable and native release artifacts
The Linux x86_64 gate packages the tested `zupt` executable as
`zupt-5.2.6-linux-x86_64.tar.xz` beside README, changelog, security guidance,
`zupt-5.2.8-linux-x86_64.tar.xz` beside README, changelog, security guidance,
and every applicable public license and notice. Its dynamic-library allowlist,
archive member allowlist, and extracted CLI functional suite must pass.
The `zupt-gui-5.2.6-portable.zip` artifact is source-only: it contains the GUI
The `zupt-gui-5.2.8-portable.zip` artifact is source-only: it contains the GUI
Python source, shell/macOS/Windows launchers, icons, provenance, changelog, and
licenses, but no Python, Qt, CLI, or compiled runtime. The gate scans both the
assembled and extracted trees, verifies an exact safe member allowlist, and
runs the extracted launcher off-screen against the tested CLI.
AppImage creation is deliberately offline and is not a 5.2.6 release gate.
AppImage creation is deliberately offline and is not a 5.2.8 release gate.
Supply a locally verified `appimagetool`, type-2 runtime, and the complete
license/source-relink compliance notice for those exact runtime bytes; the
helper never downloads any input:
@ -226,7 +246,7 @@ APPIMAGE_RUNTIME_COMPLIANCE_FILE=/verified/path/runtime-compliance.txt \
The runtime inspected while preparing 5.2.2 omitted a linked component from
its notice and did not provide the complete LGPL source/relink handoff required
by this release policy. No AppImage produced by this helper is promoted by the
upstream 5.2.6 workflow. AppDir and Flatpak bundles and GUI platform installers
upstream 5.2.8 workflow. AppDir and Flatpak bundles and GUI platform installers
are also excluded. Bare Linux and Windows executables are not promoted; their
CLI programs appear only inside notice-bearing archives. The Windows ZIP and
macOS DMG remain CLI-only.
@ -241,8 +261,8 @@ DIST_DIR="$release_dir" RUN_CHECKS=1 packaging/build-dmg.sh
The Windows ZIP (including its executable and notices) must be built and tested
by the Windows job in `.github/workflows/cross-platform.yml`; it is not a
cross-compiled release claim from a Linux build. No Wine result is retained as
5.2.6 release evidence. Extended-length/device namespace paths, raw UNC output
roots, and mapped/network-drive output are not supported in 5.2.6. Publish the
5.2.8 release evidence. Extended-length/device namespace paths, raw UNC output
roots, and mapped/network-drive output are not supported in 5.2.8. Publish the
exact architecture recorded by the native job.
These helpers create binary distribution artifacts for the release page, not
content to be committed to Git or included in the source archive.
@ -250,7 +270,7 @@ content to be committed to Git or included in the source archive.
### AUR, Homebrew, Guix, and Nix
After calculating the final reproducible source archive, but before creating or
publishing the immutable tag, update each recipe to version 5.2.6 and to the
publishing the immutable tag, update each recipe to version 5.2.8 and to the
exact digest or content hash expected by its package manager. These recipe
directories are excluded from the source archive, so this does not create a
checksum cycle. Commit the pinned recipes in the tagged tree, then build and
@ -269,7 +289,7 @@ build.
For every published artifact:
1. start from the immutable `v5.2.6` tag;
1. start from the immutable `v5.2.8` tag;
2. keep `WITH_SDK=0 WITH_PQBOX=0` unless system dependencies are declared;
3. record the exact OS, distribution release, architecture, and toolchain;
4. run format validation plus installed `--version`, `--help`, and archive
@ -281,13 +301,14 @@ For every published artifact:
Do not infer multi-architecture compatibility from portable source. Do not add
precompiled optional libraries to make a package build.
Publish release assets at the canonical GitHub release. If an expected asset is
absent or has a different checksum, report that target as unpublished rather
than redirecting consumers to an unverified file.
The 13 gated assets are published at the
[canonical GitHub release](https://github.com/cristiancmoises/zupt/releases/tag/v5.2.8).
If an expected asset is absent or has a different checksum, report that target
as unpublished rather than redirecting consumers to an unverified file.
## Downstream checklist
- [ ] The source URL resolves to the immutable `v5.2.6` tag.
- [ ] The source URL resolves to the immutable `v5.2.8` tag.
- [ ] The source archive passes `scripts/check-source-only.sh --archive`.
- [ ] The recipe checksum matches the downloaded source exactly.
- [ ] `WITH_SDK=0 WITH_PQBOX=0` is explicit, or system dependencies are complete.

View file

@ -1,4 +1,4 @@
# Installing ZUPT 5.2.6
# Installing ZUPT 5.2.8
This guide covers the ZUPT command-line program and the optional Python GUI.
The canonical source repository is
@ -27,17 +27,28 @@ working-directory contract. This is release/test integration only; the product,
archive format, cryptography, codec, and SDK ABI are unchanged. The immutable
`v5.2.5` candidate was likewise not promoted: exact-tag GitHub Actions run
`33434986357` recorded 13 successful jobs and failed native Windows/macOS jobs.
Version 5.2.6 corrects the Windows byte-exact fixture and macOS secure-wipe/Bash
3.2 portability defects, but those corrections still require fresh gates. Do
not treat any prior candidate's artifacts or evidence as 5.2.6 packages or
validation.
The immutable `v5.2.6` candidate was not promoted after run `33442264243`
recorded 13 successful jobs and two native failures: unused x86 SHA-NI helper
declarations on macOS arm64 under `-Werror`, and early Windows abortion while
argv-transcoding a safe UTF-8 fixture. Version 5.2.7 corrected those boundaries
but was not promoted after exact-tag run `33445470664`: 13 jobs succeeded,
macOS failed because its filesystem rejected the raw-C1 filename fixture with
`EILSEQ`, and Windows was cancelled after the hosted job stalled in `make
check`; a MinGW/Wine reproduction isolated the cause to the non-console
password-prompt test entering `_getch`. Version 5.2.8 makes both
fixtures portable, hardens the three CodeQL High path-race boundaries described
in the security documents, and adds `sdk-test` to release and hosted Linux
gates. Exact-tag run `33456209269` passed all 15 jobs, and promotion run
`33457868306` published the exact tested set. Do not treat any prior candidate's
artifacts or evidence as 5.2.8 packages or validation.
The 5.2.6 package set eligible for promotion after each target gate succeeds is:
The published 5.2.8 package set is exactly these 13 gated assets:
| Component | Gated artifacts |
|---|---|
| CLI | `zupt-5.2.6.tar.gz`, `zupt_5.2.6_amd64.deb`, openSUSE x86_64 binary/source RPMs, `zupt-5.2.6-linux-x86_64.tar.xz`, `zupt-5.2.6-windows-x86_64.zip`, and `ZUPT-5.2.6-macOS-*.dmg` |
| GUI | `zupt-gui_5.2.6_all.deb`, `zupt-gui-5.2.6-1.noarch.rpm`, `zupt-gui-5.2.6-1.src.rpm`, and `zupt-gui-5.2.6-portable.zip` |
| Source and checksums | `zupt-5.2.8.tar.gz`, `zupt-5.2.8.tar.gz.sha256`, and `SHA256SUMS` |
| CLI | `zupt_5.2.8_amd64.deb`, `zupt-5.2.8-0.x86_64.rpm`, `zupt-5.2.8-0.src.rpm`, `zupt-5.2.8-linux-x86_64.tar.xz`, `zupt-5.2.8-windows-x86_64.zip`, and exactly one `ZUPT-5.2.8-macOS-{x86_64\|arm64}.dmg` |
| GUI | `zupt-gui_5.2.8_all.deb`, `zupt-gui-5.2.8-1.noarch.rpm`, `zupt-gui-5.2.8-1.src.rpm`, and `zupt-gui-5.2.8-portable.zip` |
The GUI packages require the matching `zupt` CLI package and must pass exact
payload/dependency checks plus an installed off-screen GUI/CLI integration
@ -45,7 +56,7 @@ test. The source-only portable GUI ZIP bundles launchers, notices, and GUI
source, but not Python, Qt, or the CLI. The Linux tar.xz carries the tested CLI
beside the complete public license/notice payload. AppImage, AppDir, Flatpak
bundles, GUI platform installers, and bare Linux/Windows executables are not
promoted for 5.2.6. The Windows ZIP and macOS DMG contain the CLI only. Exact
promoted for 5.2.8. The Windows ZIP and macOS DMG contain the CLI only. Exact
target boundaries are listed in `README.md`.
The release's `SHA256SUMS` and validation notes, not the mere presence of a
download link, identify an artifact that completed its gate.
@ -82,7 +93,7 @@ sudo pacman -S base-devel gzip
```
Package names can differ by distribution release. These commands are examples,
not a statement that 5.2.6 has been accepted into each distribution repository.
not a statement that 5.2.8 has been accepted into each distribution repository.
## Build and test from source
@ -102,7 +113,7 @@ From a release archive, run the scanner as follows before extraction or from a
trusted checkout after download:
```sh
scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz
scripts/check-source-only.sh --archive /path/to/zupt-5.2.8.tar.gz
```
The default build provides the native password, ML-KEM-768 + X25519 hybrid

View file

@ -540,6 +540,7 @@ test-all: check
# Release-only gates need a committed Git checkout and packaging metadata.
# Keep them out of downstream %check, which intentionally has no dist rebuild.
release-check: test-all audit-licenses
$(Q)$(MAKE) sdk-test
$(Q)bash tests/test_static_analysis.sh
$(Q)bash tests/test_packaging_syntax.sh
$(Q)bash scripts/test-installed-zupt.sh ./$(TARGET)

203
README.md
View file

@ -1,35 +1,95 @@
# ZUPT 5.2.6
# ZUPT 5.2.8
ZUPT is a command-line backup archiver written in C11. It combines the
bundled VaptVupt compression codec with authenticated AES-256-CTR +
HMAC-SHA256 encryption, native ML-KEM-768/X25519 hybrid encryption, archive
integrity checks, multithreaded operation, and a Python/Qt graphical frontend.
Version 5.2.6 corrects portability defects exposed by the native release gates:
macOS and NetBSD use the compiler-resistant volatile secure-wipe fallback
instead of assuming an `explicit_bzero` symbol, the source-only scanner handles
empty arrays under the system Bash 3.2, and the Windows hostile-path regression
passes explicitly encoded bytes to its fixture. The immutable `v5.2.5`
candidate was not promoted after exact-tag GitHub Actions run `33434986357`:
13 jobs succeeded, while the native Windows and macOS jobs failed. This is a
release/test integration correction; it does not change the archive format,
cryptography, bundled codec, or SDK ABI. No v5.2.5 evidence transfers
automatically to v5.2.6.
Version 5.2.8 closes three CodeQL High path-race findings: SDK key copies now
publish atomically through an already-open private object, POSIX disk restore
classifies and retains the descriptor it actually opened, and benchmark cleanup
traverses only pinned descriptors or handles without following links or Windows
reparse points. It also makes the raw-C1 scanner fixture explicitly skip a
filesystem that rejects creation with `EILSEQ`, normalizes Bash 3.2 signed-byte
diagnostics, and brings `sdk-test` into the
release and hosted Linux gates. Windows password prompts now reject redirected
input before entering `_getch` and treat console EOF as an error; its key-file
regression validates the protected current-user-only DACL rather than an MSYS
POSIX-mode projection. The C/C++ default-branch scan run `33452563116` of
commit `7a8e5c5` completed successfully after the follow-up changed the new SDK
regression to no-follow descriptors plus `fstat` and descriptor reads. Alerts
#5 through #10 are fixed, and the authenticated code-scanning API reported zero
open alerts. Final release-commit CodeQL run `33456049125` also completed
successfully, with the API still reporting zero open alerts. These corrections
do not change archive format v1.6,
cryptography, the bundled codec release, or the SDK ABI.
The predecessor `v5.2.7` tag is immutable and was not promoted. Exact-tag run
`33445470664` concluded `cancelled` at `2026-08-31T23:11:19Z`, with 13
successful jobs, a macOS raw-C1/EILSEQ fixture failure, and a cancelled Windows
job. The hosted Windows job stalled in `make check`; a MinGW/Wine reproduction
attributed the stall to a non-console password prompt entering `_getch`. No
v5.2.7 evidence transfers automatically to v5.2.8.
Manual pre-tag run `33452602634` then completed 14 of 15 jobs successfully at
`7a8e5c5`, including the native macOS DMG gate and the Windows build and full
distribution checks. Its Windows smoke test failed only when the old MSYS
`grep` matched a literal non-BMP filename after the product had already
compressed and verified all five files. MinGW/Wine reproduction confirmed the
exact `F0 9F 98 80` UTF-8 bytes in ZUPT's redirected listing. The corrected
gate creates that name from byte escapes, validates Latin-1, BMP, and non-BMP
listing bytes with Python, and requires extraction plus a full tree diff. The
failed run is diagnostic evidence, not release-candidate approval.
The immutable `v5.2.8` candidate at commit
`ebb9ab3aa1d42c50030ca02883f6162dc4771fe1` subsequently passed all 15 jobs in
manually dispatched exact-tag run `33456209269`. That run includes the pinned
local OBS source-service chain, reproducible source checks, GCC/Clang, analyzers,
sanitizers, DEB/RPM/SRPM and portable-package gates, the native Windows ZIP
round trip, and the mounted macOS arm64 DMG test. Corrected promotion run
`33457868306` validated and published exactly 13 assets. The canonical source
archive is 798296 bytes with SHA-256
`378b9506211545b9594cf0d38ac8955d9b1cac34eb6b379ae0ec26b84edb65f7`.
Version 5.2.2 restored the original ZUPT product name and the `zupt` command.
The `.zupt` archive extension, format v1.6, magic bytes, codec identifiers, and
SDK ABI remain unchanged. An optional `vaptvupt` command alias may be provided
for scripts written against versions 3.0.0 through 5.2.1.
## Corrective changes in 5.2.6
## Corrective changes in 5.2.8
Darwin and NetBSD now select the secure volatile wipe fallback supported by the
existing portable implementation; scanner option/path arrays are guarded for
Bash 3.2; and the Windows path fixture verifies requested bytes in the archive
while rejecting each dangerous raw byte fragment from diagnostic output. All
current release paths move to 5.2.6 and require fresh
exact-tag hosted CI, package, native-platform, source-only, checksum, OBS, and
promotion evidence. The `v5.2.5` tag remains immutable and unpromoted.
SDK key saves use atomic descriptor/handle-backed publication and preserve the
requested private/public modes without reopening the destination. Disk restore
opens a POSIX target once before its type, identity, and device-capacity
decisions, and benchmark cleanup is descriptor-relative on POSIX and
handle/reparse-point aware on Windows. The live-workspace symlink regression,
SDK link-target/mode regression, static path-race guards, portable raw-C1
fixture with Bash 3.2 unsigned-byte normalization, native redirected-prompt
and protected-DACL regressions, byte-exact BMP/non-BMP Windows list and extract
checks, and `sdk-test` CI step cover these boundaries. All current release
paths moved to 5.2.8 and received fresh exact-tag hosted CI, package,
native-platform, source-only, checksum, OBS, and promotion evidence in runs
`33456209269` and `33457868306`.
## Corrective changes introduced in 5.2.7
The SHA-NI regression keeps x86-only helper declarations out of unsupported
arm64 builds, and the safe UTF-8 Windows fixture crosses the argv boundary in a
byte-stable representation. Those test-harness changes did not alter the
archive format, cryptography, codec, or SDK ABI. Exact-tag run `33445470664`
subsequently exposed the separate macOS raw-C1/EILSEQ fixture failure; Windows
was cancelled after the hosted job stalled in `make check`; MinGW/Wine then
isolated the stall to a non-console password prompt entering `_getch`. The run recorded 13 successful jobs, one
failure, and one cancellation; v5.2.7 remained unpromoted.
## Corrective changes introduced in 5.2.6
Darwin and NetBSD select the portable compiler-resistant secure-wipe fallback;
scanner option/path arrays are guarded for Bash 3.2; and hostile Windows path
fixtures use explicit bytes and reject dangerous raw diagnostic fragments.
Those corrections changed release/test integration only. The resulting v5.2.6
candidate was not promoted because its next exact-tag run exposed the distinct
arm64 SHA-NI helper and safe UTF-8 Windows argv failures described above.
## Corrective changes introduced in 5.2.5
@ -130,8 +190,12 @@ See [CHANGELOG.md](CHANGELOG.md) for the release record.
## Canonical source
- Canonical: https://github.com/cristiancmoises/zupt
- Codeberg mirror: https://codeberg.org/berkeley/zupt
- SecurityOps Brazil mirror: https://git.securityops.com.br/cristiancmoises/zupt
- SecurityOps global mirror: https://git.securityops.co/cristiancmoises/zupt
Release tags and source archives are published from this repository.
GitHub remains canonical. The `v5.2.8` tag and its 13 release assets are also
published byte-for-byte on the three mirrors above.
## Source-only policy
@ -145,10 +209,10 @@ users. Those assets must be built from the tagged source, tested on their target
environment, and kept outside Git and the source archive. A format that was not
built and tested is not presented as supported.
## 5.2.6 release artifacts
## 5.2.8 release artifacts
The 5.2.6 release workflow is defined to produce the following files only after
the corresponding target gate succeeds. `SHA256SUMS` records the exact promoted
The published 5.2.8 release contains exactly the following 13 files after
every corresponding target gate succeeded. `SHA256SUMS` records the exact promoted
filenames and digests. The release notes identify the tested commit and the
manually dispatched CI run; that run's job definitions and logs are the runtime
evidence for runner image, architecture, toolchain, results, and explicit
@ -156,23 +220,26 @@ skips. This table is not a substitute for that evidence.
| Format | Intended target and validation boundary |
| --- | --- |
| `zupt-5.2.6.tar.gz` | Reproducible, source-only archive; scanned twice-built input plus SHA-256. |
| `zupt_5.2.6_amd64.deb` | Ubuntu 24.04 amd64 package; install, functional round trip, and uninstall gate. |
| `zupt-5.2.6-*.x86_64.rpm` and `.src.rpm` | openSUSE Tumbleweed x86_64 source/binary RPM gate; package inspection, install, round trip, and uninstall. |
| `zupt-5.2.6-linux-x86_64.tar.xz` | Linux x86_64 CLI plus the complete public license/notice payload; dependency allowlist and extracted-package functional gate. |
| `zupt-gui_5.2.6_all.deb` | Architecture-independent Python/Qt GUI package; exact dependency/payload checks plus installed off-screen GUI/CLI integration gate. |
| `zupt-gui-5.2.6-1.noarch.rpm` | Architecture-independent Python/Qt GUI RPM; package inspection plus installed off-screen GUI/CLI integration gate. |
| `zupt-gui-5.2.6-1.src.rpm` | Source RPM corresponding exactly to the gated noarch GUI RPM. |
| `zupt-gui-5.2.6-portable.zip` | Source-only GUI and launchers with licenses/provenance; source scan, exact member allowlist, and extracted off-screen GUI/CLI gate. |
| `zupt-5.2.6-windows-x86_64.zip` | Native Windows x86_64 executable with notices; extracted-ZIP round-trip gate. |
| `ZUPT-5.2.6-macOS-*.dmg` | Native macOS image; mounted packaged executable round-trip gate, with the actual architecture in the filename. |
| `zupt-5.2.8.tar.gz` | Reproducible, source-only archive; scanned twice-built input plus SHA-256. |
| `zupt-5.2.8.tar.gz.sha256` | SHA-256 sidecar for the reproducible source archive. |
| `zupt_5.2.8_amd64.deb` | Ubuntu 24.04 amd64 package; install, functional round trip, and uninstall gate. |
| `zupt-5.2.8-0.x86_64.rpm` | openSUSE Tumbleweed x86_64 binary RPM; package inspection, install, round trip, and uninstall gate. |
| `zupt-5.2.8-0.src.rpm` | Source RPM corresponding exactly to the gated openSUSE binary RPM. |
| `zupt-5.2.8-linux-x86_64.tar.xz` | Linux x86_64 CLI plus the complete public license/notice payload; dependency allowlist and extracted-package functional gate. |
| `zupt-gui_5.2.8_all.deb` | Architecture-independent Python/Qt GUI package; exact dependency/payload checks plus installed off-screen GUI/CLI integration gate. |
| `zupt-gui-5.2.8-1.noarch.rpm` | Architecture-independent Python/Qt GUI RPM; package inspection plus installed off-screen GUI/CLI integration gate. |
| `zupt-gui-5.2.8-1.src.rpm` | Source RPM corresponding exactly to the gated noarch GUI RPM. |
| `zupt-gui-5.2.8-portable.zip` | Source-only GUI and launchers with licenses/provenance; source scan, exact member allowlist, and extracted off-screen GUI/CLI gate. |
| `zupt-5.2.8-windows-x86_64.zip` | Native Windows x86_64 executable with notices; extracted-ZIP round-trip gate. |
| Exactly one `ZUPT-5.2.8-macOS-{x86_64\|arm64}.dmg` | Native macOS image; mounted packaged executable round-trip gate, with the actual runner architecture in the filename. |
| `SHA256SUMS` | Deterministic manifest covering the other 12 promoted files. |
An asset absent from the release was not promoted through its mandatory gate.
Do not infer support for another distribution release, OS version, CPU
architecture, raw UNC/SMB destination, or package manager from a similarly
named file. Binary assets are release outputs, never source-build inputs.
No AppImage is promised for 5.2.6. The inspected upstream type-2 runtime lacked
No AppImage is promised for 5.2.8. The inspected upstream type-2 runtime lacked
a complete notice/source-relink handoff for every statically linked component,
so redistributing it would not meet this release's provenance gate. AppDir and
Flatpak bundles and GUI platform installers are likewise outside the promoted
@ -188,9 +255,10 @@ software already installed on the target. Other historical GUI packages and
platform installers are not carried forward implicitly.
The canonical source repository is
<https://github.com/cristiancmoises/zupt>. Release assets referenced by the AUR,
Homebrew, Guix, or generic RPM recipes must exist in the canonical GitHub
release at their recorded URL before those recipes are published.
<https://github.com/cristiancmoises/zupt>. The canonical release is
<https://github.com/cristiancmoises/zupt/releases/tag/v5.2.8>. Assets referenced
by the AUR, Homebrew, Guix, or generic RPM recipes must exist there at their
recorded URL before those recipes are published.
Audit the current checkout and its Git archive with:
@ -202,8 +270,8 @@ bash tests/test_source_only.sh
For a tag or an existing source archive:
~~~sh
bash scripts/check-source-only.sh --tag v5.2.6
bash scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz
bash scripts/check-source-only.sh --tag v5.2.8
bash scripts/check-source-only.sh --archive /path/to/zupt-5.2.8.tar.gz
~~~
Unknown `.bin` files fail the scan. A necessary binary data fixture may be
@ -307,6 +375,7 @@ The principal source-only gates are:
~~~sh
make WITH_SDK=0 WITH_PQBOX=0 check
make WITH_SDK=0 WITH_PQBOX=0 test-all
make sdk-test
make test-asan
make test-asan-run
make audit-licenses
@ -339,7 +408,7 @@ sanitizer-detected crash. An earlier off-screen GUI smoke run remains supporting
evidence rather than an exact-candidate package result.
Those results are historical upstream self-audit evidence, not independent
certification and not 5.2.6 results. Post-tag CI integration failures prevented
certification and not 5.2.8 results. Post-tag CI integration failures prevented
5.2.2 promotion. The immutable 5.2.3 candidate was also not promoted because its
source-policy test assumed LF for a `.bat` checkout that correctly used CRLF.
The immutable v5.2.4 candidate then recorded 12 successful jobs in exact-tag CI
@ -347,18 +416,26 @@ run `33431386002`; the sole openSUSE service-harness job failed because the
standalone executor did not enter its service directory, so dependent Windows
and macOS jobs were skipped. A local Tumbleweed reproduction proved the explicit
tag ref and corrected working-directory contract, but neither that reproduction
nor the successful v5.2.4 jobs are v5.2.6 evidence. The immutable v5.2.5
nor the successful v5.2.4 jobs are v5.2.8 evidence. The immutable v5.2.5
candidate was not promoted after exact-tag GitHub Actions run `33434986357`:
13 jobs succeeded, but the native Windows hostile-path fixture and macOS
build/check gate failed. The corrective byte-exact fixture, portable secure-wipe
fallback, and Bash 3.2 array handling therefore require new 5.2.6 evidence. The
exact 5.2.6 candidate must repeat all required gates. Native Windows and macOS,
hosted GitHub CI/release promotion, authenticated OBS, and resolution of the
openSUSE automatic `debugsource` rpmlint `no-binary`
finding remain pending until recorded otherwise. Unexecuted gates are `SKIP`,
never `PASS`.
build/check gate failed. Their 5.2.6 corrections were followed by exact-tag run
`33442264243`, which also completed 13 jobs successfully but failed native
macOS on arm64-unused SHA-NI helper declarations under `-Werror` and native
Windows during safe UTF-8 fixture argv transcoding. The immutable v5.2.6 tag was
not promoted. The immutable v5.2.7 tag was also not promoted: exact-tag run
`33445470664` reached the macOS raw-C1 filename-creation failure with `EILSEQ`,
recorded 13 successful jobs, and cancelled Windows after the hosted job stalled
in `make check`; a MinGW/Wine reproduction isolated the stall to
`test --password-prompt ... </dev/null` entering `_getch`. The
exact 5.2.8 candidate then repeated all required gates: exact-tag run
`33456209269` completed 15/15 jobs successfully, including native Windows and
macOS, the pinned local OBS service chain, source/package gates, and the
openSUSE RPM checks. Promotion run `33457868306` published the exact tested
asset allowlist. Unexecuted environments remain `SKIP`, never `PASS`; these
project-run results are not independent certification.
On Windows, 5.2.6 scopes output handling to normal local Win32 paths. A MinGW
On Windows, 5.2.8 scopes output handling to normal local Win32 paths. A MinGW
cross-build or Wine run is not native-Windows evidence; the `windows-latest`
package job, including its Unicode round trip, remains a mandatory publication
gate. Win32 extended-length and device-namespace paths, raw UNC output roots
@ -380,7 +457,7 @@ downgrading authentication of header and footer metadata.
`disk restore`, and exists only to recover a known, trusted archive created
before AIT was introduced. Do not use that override for an archive from
untrusted or attacker-writable storage; verify and migrate the recovered data to
a newly created 5.2.6 archive. Compression and disk backup never create a
a newly created 5.2.8 archive. Compression and disk backup never create a
no-AIT archive.
`info` is deliberately different: it reports unauthenticated framing metadata,
@ -398,10 +475,9 @@ lists, tests, extracts, and restores it byte-exact. The full local Linux gate
passed on commit `ff99770`. This is not a claim that a 5.2.1 reader understands every new
flag-gated 5.2.2 encoding or that every historical combination was tested.
The candidate commands and outcome fields for 5.2.6 are maintained in the
release handoff and
[packaging/opensuse/README.md](packaging/opensuse/README.md). They must be
updated from the final release candidate before tagging. No architecture or
The build/audit commands and recorded 5.2.8 outcomes are maintained in the
release evidence and
[packaging/opensuse/README.md](packaging/opensuse/README.md). No architecture or
distribution is claimed merely because the code has a fallback path.
## Source archive
@ -410,9 +486,9 @@ Generate the reproducible source archive outside the repository:
~~~sh
make dist
sha256sum /tmp/zupt-5.2.6.tar.gz
sha256sum /tmp/zupt-5.2.8.tar.gz
bash scripts/check-source-only.sh \
--archive /tmp/zupt-5.2.6.tar.gz
--archive /tmp/zupt-5.2.8.tar.gz
~~~
Archive ordering, ownership and timestamps are normalized. The default epoch is
@ -422,13 +498,13 @@ the same SHA-256. The AUR, Homebrew and Guix recipes are `export-ignore` so
their checksum fields do not make the archive self-referential. `make dist`
archives the verified `HEAD` tree object rather than embedding the commit ID,
so a commit changing only those ignored recipes leaves the fixed-epoch archive
byte-identical. The recipes remain versioned in Git and must be filled with the
final digest before the tag is published.
byte-identical. The recipes remain versioned in Git and pin the final tagged
digest `378b9506211545b9594cf0d38ac8955d9b1cac34eb6b379ae0ec26b84edb65f7`.
## openSUSE and OBS
The maintained upstream recipe is in packaging/opensuse. It is prepared for an
immutable v5.2.6 tag, disables submodules and Git LFS, builds with
The maintained upstream recipe is in packaging/opensuse. It targets the
immutable v5.2.8 tag, disables submodules and Git LFS, builds with
WITH_SDK=0 WITH_PQBOX=0, runs real checks, and installs without the renamed-era
`vaptvupt` alias.
@ -471,7 +547,7 @@ The optional GUI is under `gui/`. It invokes the `zupt` CLI and needs Python 3
plus PySide6 or PyQt6. GUI image assets are data files whose purpose,
provenance and license are recorded in [gui/assets/README.md](gui/assets/README.md).
The integrated source and lightweight consistency checks do not constitute a
target-native audit of every historical GUI format. The 5.2.6 artifact promise
target-native audit of every historical GUI format. The 5.2.8 artifact promise
is limited to the gated GUI DEB, noarch/source RPM, and source-only portable ZIP
listed above; AppImage, AppDir, Flatpak bundles, and platform GUI installers
remain excluded.
@ -481,13 +557,14 @@ remain excluded.
Cristian Cezar Moisés is the creator and current upstream maintainer of ZUPT and
the author of the current upstream source, build, test, documentation, and
packaging changes, including the 5.2.2 baseline and corrective
5.2.3/5.2.4/5.2.5/5.2.6 work.
5.2.3/5.2.4/5.2.5/5.2.6/5.2.7/5.2.8 work.
Alessandro de Oliveira Faria (Cabelo) is credited as the openSUSE collaborator
and downstream package maintainer. He reviews the handoff, commits it in the
OBS project he maintains, and may make the additional openSUSE-side adjustments
he considers necessary. That downstream role is not attribution of ZUPT source
authorship or of the upstream 5.2.2, 5.2.3, 5.2.4, 5.2.5, or 5.2.6 changes.
authorship or of the upstream 5.2.2, 5.2.3, 5.2.4, 5.2.5, 5.2.6, 5.2.7, or
5.2.8 changes.
## License

View file

@ -1,4 +1,4 @@
# Security Policy — ZUPT 5.2.6
# Security Policy — ZUPT 5.2.8
## Reporting vulnerabilities
@ -49,6 +49,9 @@ On POSIX terminals, the explicit prompt saves terminal state and installs
signal-aware cleanup so a handled interruption restores echo and other changed
settings before termination. This behavior is covered by a PTY regression and
passed in the full local Linux gate for commit `ff99770`.
On Windows, the prompt requires a real console input handle before entering
`_getch`; redirected input and console EOF fail instead of blocking a native
release gate.
## Native key files
@ -66,7 +69,7 @@ partially accepted.
### Optional integrations
The 5.2.6 default is `WITH_SDK=0 WITH_PQBOX=0`:
The 5.2.8 default is `WITH_SDK=0 WITH_PQBOX=0`:
- `WITH_SDK=1` enables libvuptsdk-backed features, including the SDK PQ mode
and Argon2id support, using a separately installed system development package.
@ -78,6 +81,13 @@ downloads it. A missing requested dependency is a build error. Security
properties of these optional libraries are outside the source-only CLI audit
unless their exact source package and version are reviewed separately.
The in-repository SDK adapter saves copied keys through the core atomic output
publisher. POSIX permissions are applied to its already-open temporary
descriptor, and publication replaces only the requested directory entry after
copy/close checks succeed. `make sdk-test` exercises private/public modes and
preservation of pre-existing symlink and hardlink targets. This covers the
adapter boundary; it does not certify the separately installed SDK library.
## Cryptographic construction
Encrypted blocks use a fresh 128-bit nonce, AES-256-CTR, and HMAC-SHA256. The
@ -133,7 +143,7 @@ can compromise archives encrypted to it.
## Constant-time and side-channel scope
Portable C is the 5.2.6 default. Sensitive comparisons and selections use
Portable C is the 5.2.8 default. Sensitive comparisons and selections use
branchless helpers, but generated machine-code behavior remains dependent on
the compiler and platform. This is not a formal whole-program constant-time
claim. The C AES implementation uses table lookups and is unsuitable for a
@ -190,21 +200,50 @@ appropriate. Extract untrusted archives as a dedicated unprivileged user into
a new empty directory, inspect the result before moving it, and apply OS
sandboxing where available.
Benchmark workspaces are random private directories. POSIX cleanup opens each
directory component without following links and removes entries relative to
pinned descriptors. Windows retains no-delete-sharing handles for the resolved
ancestors and refuses reparse-point recursion. After emptying a directory, it
reopens that entry relative to the pinned parent, verifies the volume and file
index against the traversal handle, and marks only the identity-checked handle
for deletion. An injected link is removed as a link rather than traversed to
its target.
Disk restore has a separate destructive-device boundary. It measures and
copies the compacted archive to an exclusively created, auto-deleted private
scratch file before opening the target, then performs both validation and
restoration from that same snapshot. `ZUPT_TMPDIR` is an explicit existing
scratch-directory override; an invalid override fails without fallback. Raw
block devices are opened only after their capacity has been determined and
shown sufficient on supported Linux, macOS, or FreeBSD interfaces. Unknown
device capacity, an undersized device, a source/destination identity match, or
a snapshot failure stops before the first target write. These checks do not
scratch file, then performs validation and restoration from that same snapshot.
`ZUPT_TMPDIR` is an explicit existing scratch-directory override; an invalid
override fails without fallback. On POSIX, the target is opened once without
truncation or final-symlink following, classified with `fstat`, and—when it is
a supported Linux, macOS, or FreeBSD raw block device—the same descriptor is
retained through capacity checks and writes. Regular files continue through
atomic publication. Unknown device capacity, an undersized device, a
source/destination identity match, or a snapshot failure stops before the
first target write. These checks do not
make raw-device restore non-destructive: verify both operands and keep recovery
media before proceeding.
These changes address the three 5.2.8 CodeQL High reports: #5 at SDK key
publication, #6 at POSIX disk-target classification/use, and #7 at benchmark
workspace cleanup. The regressions and source review are project evidence, not
an independent certification. Exact-tag run `33456209269` subsequently passed
all 15 jobs at `ebb9ab3aa1d42c50030ca02883f6162dc4771fe1`.
The C/C++ default-branch analysis of commit `69fc26b` closed #5, #6, and #7,
then opened test-only High #8, #9, and #10 because the new SDK regression used
path-level `stat`/`lstat` before later reads or cleanup. Each individual test
check now uses a no-follow descriptor with `fstat` or descriptor reads; the
static gate rejects reintroduction of path-level metadata checks there. The
subsequent C/C++ default-branch scan run `33452563116` completed successfully at
commit `7a8e5c5`; alerts #5 through #10 are fixed, and the authenticated
code-scanning API reported zero open alerts.
The final release-commit CodeQL run `33456049125` also completed successfully;
the authenticated API again reported zero open alerts, with #5 through #10
recorded as fixed rather than dismissed.
The Windows handle-relative implementation is scoped to normal local Win32
paths. Win32 extended-length and device-namespace paths, raw UNC output roots,
and mapped/network-drive output are not supported in 5.2.6. Cross-build and
and mapped/network-drive output are not supported in 5.2.8. Cross-build and
Wine results are not native-Windows evidence; the `windows-latest` package gate
must pass its Unicode round trip before Windows assets are published. Restore
to a normal local directory first and move verified output to network storage
@ -247,7 +286,7 @@ shared/static library, or distribution package. Audit them with:
```sh
scripts/check-source-only.sh
scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz
scripts/check-source-only.sh --archive /path/to/zupt-5.2.8.tar.gz
```
Nested archive inspection is required to enforce bounded recursion, member
@ -257,13 +296,13 @@ limit violations. On commit `ff99770`, the source-only scanner suite passed
DEB, binary RPM, SRPM, notice-bearing Linux tar.xz, source-only portable GUI
ZIP, Windows ZIP, and macOS DMG release assets are separate outputs. An
AppImage is not promoted for 5.2.6. A bare Linux or Windows executable is also
AppImage is not promoted for 5.2.8. A bare Linux or Windows executable is also
excluded; executables are distributed only inside their notice-bearing
archives. Trust an artifact only when its exact format has a recorded build,
content/metadata inspection, extracted or installed smoke test, and applicable
archive round trip. Never treat an unexecuted platform as passing.
The gated 5.2.6 set is the CLI package/archive set plus the exact GUI DEB,
The gated 5.2.8 set is the CLI package/archive set plus the exact GUI DEB,
noarch/source RPM, and source-only portable ZIP documented in the README. The
portable GUI ZIP contains no compiled runtime and is scanned as source before
and after extraction. Other GUI packages, AppImage, AppDir and Flatpak bundles,
@ -280,6 +319,7 @@ make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf 1)" \
WITH_SDK=0 WITH_PQBOX=0 V=1
make WITH_SDK=0 WITH_PQBOX=0 check
make WITH_SDK=0 WITH_PQBOX=0 test-all
make sdk-test
```
Where the compiler supports them, run the sanitizer target separately:
@ -303,7 +343,7 @@ result.
Post-tag CI integration failures prevented 5.2.2 promotion. Those upstream
self-audit results are not independent certification and do not transfer to
5.2.6. The immutable 5.2.3 candidate was not promoted because its source-policy
5.2.8. The immutable 5.2.3 candidate was not promoted because its source-policy
test assumed LF for a Windows `.bat` file checked out as CRLF. The immutable
v5.2.4 candidate was not promoted after exact-tag GitHub Actions run
`33431386002`: 12 jobs succeeded, the sole openSUSE job failed in its
@ -313,18 +353,39 @@ Tumbleweed reproduction confirmed that `refs/tags/v5.2.4` is valid and that
`os.chdir(service_dir)` lets `obs_scm`, `tar`, and `recompress` complete with a
source-scanned archive. This was a release/test integration defect, not a
product, archive, cryptographic, codec, or SDK ABI change, and its evidence does
not transfer automatically to 5.2.6. The immutable v5.2.5 candidate was also
not transfer automatically to 5.2.8. The immutable v5.2.5 candidate was also
not promoted: exact-tag GitHub Actions run `33434986357` recorded 13 successful
jobs and failed native Windows/macOS jobs. Its Windows fixture-byte and macOS
secure-wipe/Bash 3.2 defects are corrected for 5.2.6, but those changes have not
yet passed exact-5.2.6 hosted or native gates. A targeted clean-clone run of the
corrected scanner under genuine GNU Bash 3.2.57 passed repository, standalone
secure-wipe/Bash 3.2 defects were corrected for 5.2.6. A targeted clean-clone
run of the corrected scanner under genuine GNU Bash 3.2.57 passed repository, standalone
tree, standalone archive, and root-plus-tag modes; that local compatibility
result does not transfer to any other gate. The exact 5.2.6 candidate must
repeat the required suite. Native Windows and macOS, hosted GitHub CI/release
promotion, authenticated OBS, and the openSUSE automatic `debugsource` rpmlint
`no-binary` finding remain pending until recorded otherwise. An unavailable or
unexecuted environment remains `SKIP`, never `PASS`.
result does not transfer to any other gate. Exact-tag v5.2.6 run `33442264243`
then completed 13 jobs successfully but failed native macOS because x86 SHA-NI
test helpers were unused on arm64 under `-Werror`, and failed native Windows
when argv transcoding aborted the safe UTF-8 fixture. Those are test-harness
integration defects, not product, archive, cryptographic, codec, or SDK ABI
changes; v5.2.6 remained unpromoted, so its results did not transfer to the
required 5.2.8 suite. The immutable v5.2.7 candidate was likewise not
promoted: exact-tag run `33445470664` concluded `cancelled` at
`2026-08-31T23:11:19Z`, with 13 successful jobs, one failed macOS job after
raw-C1 fixture creation returned `EILSEQ`, and one cancelled Windows job after
the hosted job stalled in `make check`; a MinGW/Wine reproduction isolated the
cause to a redirected password prompt entering `_getch`. Version 5.2.8 makes
both test boundaries fail or skip without hanging. Manual pre-tag run
`33452602634` subsequently passed 14 of 15 jobs, including the native macOS
DMG and the Windows source audit, build, and full distribution checks. The
remaining Windows smoke failure was an old MSYS `grep` non-BMP pattern boundary
after ZUPT had compressed and verified all inputs; MinGW/Wine confirmed ZUPT's
byte-exact UTF-8 listing. The corrected gate validates Latin-1, BMP, and
non-BMP listing bytes without locale-sensitive matching, then requires
extraction and a full tree diff. The failed run is not exact-candidate
evidence. Exact-tag run `33456209269` then completed 15/15 jobs successfully,
including native Windows/macOS, the pinned local OBS service chain, package
installation/round trips, source-only checks, analyzers, and sanitizers.
Promotion run `33457868306` published the exact 13-file allowlist after
format, metadata, payload, and checksum validation. An unavailable or
unexecuted environment remains `SKIP`, never `PASS`; successful project CI is
still not independent security certification.
Run target-native static analyzers and package checks as additional evidence.
Do not infer x86_64, aarch64, ppc64le, s390x, riscv64, macOS, Windows, Leap, or

View file

@ -159,7 +159,7 @@ grant attached to their unchanged Git blobs, are recorded in
## AppImage type-2 runtime
No AppImage is a promised or promoted 5.2.6 release asset. The upstream
No AppImage is a promised or promoted 5.2.8 release asset. The upstream
type-2 runtime inspected during the 5.2.2 review statically linked musl, libfuse,
squashfuse, zstd, zlib, and mimalloc, but its own license notice did not list
mimalloc and the available release inputs did not provide a complete
@ -171,7 +171,7 @@ no network input and requires the operator to supply both a locally verified
runtime and `APPIMAGE_RUNTIME_COMPLIANCE_FILE`, containing the license notices,
source correspondence or offer, and relink information applicable to those
exact runtime bytes. An artifact produced independently with that helper is
not covered by the 5.2.6 upstream release gates.
not covered by the 5.2.8 upstream release gates.
## Reporting attribution issues

View file

@ -1,4 +1,4 @@
# ZUPT 5.2.6 threat model
# ZUPT 5.2.8 threat model
This document defines the security boundary of the ZUPT archive tool. It is
not a certification, a guarantee against every hostile input, or a substitute
@ -17,7 +17,7 @@ plausibly deniable.
## Baseline considered here
The upstream baseline is built from the 5.2.6 source with:
The upstream baseline is built from the 5.2.8 source with:
```sh
make WITH_SDK=0 WITH_PQBOX=0
@ -88,6 +88,8 @@ duplicates but shares the underlying stream/offset and may buffer beyond the
line, so callers should provide a descriptor dedicated to that password read.
On POSIX, handled prompt interruptions restore the saved terminal state before
termination; an exact-candidate PTY regression is required before release.
On Windows, a prompt is entered only for a real console input handle;
redirected input and console EOF fail instead of blocking in `_getch`.
Native private-key generation uses no-replace creation with POSIX mode `0600`
or a Windows current-user-only DACL. A failed write, flush/fsync, or close leaves
@ -99,6 +101,13 @@ and public/private role validation. This prevents role confusion and
partial/trailing-key acceptance; it does not protect a key after endpoint or
account compromise.
When the optional system SDK is enabled, the in-repository adapter copies a key
through the core atomic publisher, applies POSIX mode through the already-open
temporary descriptor, and publishes only after copy/close checks succeed. Its
`sdk-test` regression preserves existing symlink/hardlink targets and verifies
private/public modes. This narrows the adapter boundary; it does not extend the
baseline assessment to the external SDK implementation.
### Encrypted archive integrity
Current encrypted archives authenticate ciphertext, canonical block metadata,
@ -175,22 +184,39 @@ temporary through its descriptor or handle. These controls reduce traversal,
link, race, and partial-output risks, but do not establish that no parser or
filesystem bug can exist.
The Windows handle-relative boundary in 5.2.6 covers normal local Win32 paths.
Benchmark scratch data lives in a random private directory. Cleanup resolves
POSIX components without following links and deletes relative to pinned
descriptors. On Windows it retains no-delete-sharing ancestor handles, refuses
reparse-point recursion, then reopens each emptied directory relative to its
pinned parent and verifies its filesystem identity before handle-based
deletion. An attacker who inserts a link can cause cleanup failure, but the
cleanup must not traverse to the link target.
The Windows handle-relative boundary in 5.2.8 covers normal local Win32 paths.
Win32 extended-length and device-namespace paths, raw UNC output roots, and
mapped/network-drive output are not supported. Cross-build and Wine results are
not a substitute for the required native `windows-latest` Unicode package
gate. Restore locally before moving verified output to network storage.
Disk restore copies the measured compacted archive into one exclusively
created, auto-deleted scratch file before it opens a destructive destination.
Preflight and restoration consume that same open snapshot. An explicit
`ZUPT_TMPDIR` selects an existing scratch directory; failure there does not
fall back to consuming the mutable source pathname. On supported Linux, macOS,
and FreeBSD interfaces, a raw block-device target is rejected before writing if
its capacity is unknown or smaller than the image. These controls reduce source
exchange and immediate overrun risk but do not protect against a compromised
kernel/device, a wrongly selected sufficiently large device, power loss, or
hardware failure.
created, auto-deleted scratch file. Preflight and restoration consume that same
open snapshot. An explicit `ZUPT_TMPDIR` selects an existing scratch directory;
failure there does not fall back to consuming the mutable source pathname. On
POSIX, the destination is opened once without truncation or final-symlink
following, classified with `fstat`, and the same raw-device descriptor is
retained for supported Linux, macOS, and FreeBSD capacity checks and writes.
Regular-file output retains atomic publication. A raw target is rejected before
writing if its capacity is unknown or smaller than the image. These controls
reduce source exchange, target exchange, and immediate overrun risk but do not
protect against a compromised kernel/device, a wrongly selected sufficiently
large device, power loss, or hardware failure.
The SDK publication, POSIX disk-target, and benchmark-cleanup changes address
CodeQL High #5, #6, and #7 respectively. Their source review and regressions
alone are project evidence, not independent certification. Exact-tag run
`33456209269` subsequently passed all 15 hosted jobs at
`ebb9ab3aa1d42c50030ca02883f6162dc4771fe1`; final release-commit CodeQL run
`33456049125` completed successfully with zero open alerts.
For an untrusted archive:
@ -250,9 +276,9 @@ tagged source. Each artifact extends the trust boundary to its builder,
toolchain, runner image, and packaging scripts. Treat it as validated only when
the exact target has a recorded build, content/package inspection, extracted or
installed smoke test, and applicable archive round trip. An AppImage is not
promoted for 5.2.6; bare Linux and Windows executables are also excluded.
promoted for 5.2.8; bare Linux and Windows executables are also excluded.
For 5.2.6, that gated artifact scope covers the CLI files plus the exact GUI
For 5.2.8, that gated artifact scope covers the CLI files plus the exact GUI
DEB, noarch/source RPM, and source-only portable ZIP named in the README. The
portable ZIP contains no compiled runtime and crosses the release boundary only
after source scans and an exact safe-member check. AppDir and Flatpak bundles
@ -265,7 +291,7 @@ strict Clang, GCC `-fanalyzer`, the 9/9 tool-enabled static-analysis run,
ASan/UBSan/LSan, and 1,000 mutation-fuzz iterations passed. Earlier off-screen
GUI smoke evidence is retained separately. Post-tag CI integration failures
prevented 5.2.2 promotion. This upstream self-review is not an independent
certification and is not 5.2.6 evidence. The immutable 5.2.3 candidate was not
certification and is not 5.2.8 evidence. The immutable 5.2.3 candidate was not
promoted because its source-policy test assumed LF for a Windows `.bat` checkout
that correctly used CRLF. The immutable v5.2.4 candidate was not promoted after
exact-tag GitHub Actions run `33431386002`: 12 jobs succeeded, the sole openSUSE
@ -275,14 +301,27 @@ Tumbleweed reproduction established that the explicit `refs/tags/v5.2.4`
revision works and that `os.chdir(service_dir)` completes the source-service
chain. This narrows the failure to release/test integration; it changes no
product, archive, cryptographic, codec, or SDK ABI boundary and supplies no
automatic 5.2.6 evidence. The immutable v5.2.5 candidate was not promoted after
automatic 5.2.8 evidence. The immutable v5.2.5 candidate was not promoted after
exact-tag GitHub Actions run `33434986357`: 13 jobs succeeded, but native
Windows and macOS failed on fixture-byte preservation and Darwin/Bash 3.2
portability respectively. The corresponding 5.2.6 corrections do not establish
their own test result. Hosted GitHub CI and release promotion, native
Windows/macOS, authenticated OBS, and the openSUSE automatic `debugsource`
rpmlint `no-binary` finding remain pending until an exact 5.2.6 candidate
records them.
portability respectively. The corresponding 5.2.6 corrections were followed by
exact-tag run `33442264243`: 13 jobs succeeded, while native macOS failed on
x86-only SHA-NI helper declarations unused on arm64 under `-Werror`, and native
Windows aborted during safe UTF-8 fixture argv transcoding. The v5.2.6 tag was
not promoted. Version 5.2.7 corrected those two boundaries, but its exact-tag
run `33445470664` concluded `cancelled` at `2026-08-31T23:11:19Z`, with 13
successful jobs, one failed macOS job after raw-C1 filename creation returned
`EILSEQ`, and one cancelled Windows job after the hosted job stalled in `make
check`; a MinGW/Wine reproduction isolated the cause to a redirected password
prompt entering `_getch`.
The corresponding 5.2.8 fixture and prompt corrections alone did not establish
a result. Exact-tag run `33456209269` then passed 15/15 jobs, including
`sdk-test`, native Windows/macOS, the pinned local OBS source-service chain,
and the package/source gates. Promotion run `33457868306` published the exact
13 tested assets. Official authenticated OBS/Factory acceptance, the full
automatic debug-package result, and untested architectures remain unclaimed;
the earlier `debugsource` rpmlint `no-binary` finding remains unresolved and
unsuppressed.
## Historical compatibility notes
@ -309,7 +348,7 @@ These are historical facts about earlier releases, retained to support recovery:
combinations remain unclaimed.
Historical test counts in the changelog describe those releases. They do not
automatically become 5.2.6 results; current outcomes belong in the release
automatically become 5.2.8 results; current outcomes belong in the release
validation record, with unavailable environments marked `SKIP`. In particular,
runs made before the final positional-AAD and mandatory-AIT changes are not
final release gates for the resulting candidate.
@ -320,4 +359,4 @@ Email **zupt@riseup.net** with `[security]` in the subject. Include the version,
platform, impact, and a minimal non-sensitive reproducer. Do not disclose the
issue publicly until a coordinated timeline has been agreed.
Document version: 5.2.6, 2026-08-31.
Document version: 5.2.8, 2026-08-31.

View file

@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: AGPL-3.0-or-later
.\" Copyright (c) 2025-2026 Cristian Cezar Moisés
.TH ZUPT-GUI 1 "2026-08-31" "ZUPT 5.2.6" "User Commands"
.TH ZUPT-GUI 1 "2026-08-31" "ZUPT 5.2.8" "User Commands"
.SH NAME
zupt-gui \- Qt interface for the ZUPT backup utility
.SH SYNOPSIS
@ -47,7 +47,7 @@ or
only when libvuptsdk or libpqvaptvupt is independently reported enabled.
These two optional integrations are detected separately.
.PP
The gated 5.2.6 GUI release set is limited to the architecture-independent DEB,
The gated 5.2.8 GUI release set is limited to the architecture-independent DEB,
noarch/source RPM, and source-only portable ZIP named in the project README.
Package gates require exact checks and installed off-screen GUI/CLI integration.
The portable ZIP receives source scans, an exact safe-member allowlist, and an

View file

@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: AGPL-3.0-or-later
.\" Copyright (c) 2025-2026 Cristian Cezar Moisés
.TH ZUPT 1 "2026-08-31" "ZUPT 5.2.6" "User Commands"
.TH ZUPT 1 "2026-08-31" "ZUPT 5.2.8" "User Commands"
.
.SH NAME
zupt \- source-built backup compression and authenticated-encryption utility
@ -89,7 +89,7 @@ Git and the upstream source tarball are source-only. Separately built CLI DEB,
binary RPM, SRPM, notice-bearing Linux tar.xz, Windows ZIP, and macOS DMG assets
may be published from the immutable tag only after their target-specific gates
pass; they never enter Git or the source tarball. An AppImage is not promoted
for 5.2.6; neither are AppDir/Flatpak bundles, GUI platform installers, or bare
for 5.2.8; neither are AppDir/Flatpak bundles, GUI platform installers, or bare
Linux/Windows executables. The Python/Qt frontend remains available as source;
its gated architecture-independent DEB, noarch/source RPM, and source-only
portable ZIP are included in the release claim. The portable ZIP contains no
@ -477,7 +477,7 @@ then traverse below a pinned directory descriptor with no-follow operations.
Windows builds use handle-relative traversal and
no-replace publication for normal local Win32 destinations. Extended-length and
device-namespace paths, raw UNC output roots, and mapped/network-drive output
are not supported in 5.2.6. Cross-compilation and Wine results are not native
are not supported in 5.2.8. Cross-compilation and Wine results are not native
Windows evidence; the native Windows package gate, including its Unicode round
trip, is separate and mandatory before publication.
.
@ -577,7 +577,7 @@ and the encrypted-dedup linear AAD sequence published through 5.2.1. The narrow
compatibility fixture is an actual v5.2.1 password-encrypted
DATA/DATA/REF/DATA disk archive stored as hexadecimal text with source and hash
provenance. The candidate lists, tests, extracts, and restores that fixture
byte-exact. The exact 5.2.6 candidate must repeat the gate. It does not imply
byte-exact. The exact 5.2.8 candidate must repeat the gate. It does not imply
that a 5.2.1 reader
accepts every new 5.2.2 archive or that every historical encrypted mode was
retested.

View file

@ -1,6 +1,6 @@
# ZUPT GUI
The ZUPT GUI is a Python/Qt front end for the ZUPT 5.2.6 command-line
The ZUPT GUI is a Python/Qt front end for the ZUPT 5.2.8 command-line
program. It starts the CLI as a subprocess; compression, archive parsing, and
cryptography remain in the C program.
@ -83,10 +83,10 @@ operating systems and must be tested on the target system.
Release pages provide only these GUI artifacts after their separate package and
installed off-screen GUI/CLI integration gates pass:
- `zupt-gui_5.2.6_all.deb`;
- `zupt-gui-5.2.6-1.noarch.rpm`;
- `zupt-gui-5.2.6-1.src.rpm`;
- `zupt-gui-5.2.6-portable.zip`.
- `zupt-gui_5.2.8_all.deb`;
- `zupt-gui-5.2.8-1.noarch.rpm`;
- `zupt-gui-5.2.8-1.src.rpm`;
- `zupt-gui-5.2.8-portable.zip`.
The DEB/RPM packages install the Python/Qt source and depend on the matching
`zupt` CLI package. The portable ZIP contains source, launchers, icons, licenses,
@ -97,7 +97,7 @@ An absent artifact did not pass its gate and must not be inferred from another
format's result.
GUI AppImage, AppDir and Flatpak bundles, and Windows/macOS GUI installers are
not promoted by the upstream 5.2.6 release gates.
not promoted by the upstream 5.2.8 release gates.
`packaging/build-gui-appimage.sh` is a downstream-only helper and fails unless
its operator supplies the exact verified runtime plus a complete
license/source-relink notice through `APPIMAGE_RUNTIME_COMPLIANCE_FILE`; that
@ -110,7 +110,7 @@ notices. It fails unless the directory also has non-empty
`PYTHON-NOTICE.txt`, `PYINSTALLER-NOTICE.txt`, `QT-NOTICE.txt`, and either
`PYSIDE6-NOTICE.txt` or `PYQT6-NOTICE.txt`. The installer includes that
directory together with every ZUPT license and notice. This requirement does
not make the untested GUI installer a 5.2.6 release asset. The promoted Windows
not make the untested GUI installer a 5.2.8 release asset. The promoted Windows
ZIP and macOS DMG are CLI-only.
Packaging recipes and scripts under `gui/packaging/` and `packaging/` are build

View file

@ -1,9 +1,9 @@
Package: zupt-gui
Version: 5.2.6
Version: 5.2.8
Section: utils
Priority: optional
Architecture: all
Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= 5.2.6)
Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= 5.2.8)
Maintainer: Cristian Cezar Moisés <sac@securityops.co>
Homepage: https://github.com/cristiancmoises/zupt
Description: Qt graphical interface for the ZUPT backup utility

View file

@ -13,7 +13,7 @@ rem runtime files embedded by this local build.
setlocal EnableExtensions
for %%I in ("%~dp0\..\..\..") do set "REPO_ROOT=%%~fI"
set "VERSION=%~1"
if not defined VERSION set "VERSION=5.2.6"
if not defined VERSION set "VERSION=5.2.8"
if not defined ZUPT_DIST_DIR set "ZUPT_DIST_DIR=%TEMP%\zupt-release"
if not defined ZUPT_CLI_EXE set "ZUPT_CLI_EXE=%REPO_ROOT%\zupt.exe"
set "WORK=%TEMP%\zupt-gui-build-%RANDOM%-%RANDOM%"

View file

@ -121,7 +121,9 @@ static inline int zupt_win_mkdir_utf8(const char *path) {
/* v5.2.4 makes package metadata checks CRLF-safe; format remains v1.6. */
/* v5.2.5 corrects the OBS service harness cwd; format remains v1.6. */
/* v5.2.6 corrects native release-gate portability; format remains v1.6. */
#define ZUPT_VERSION_STRING "5.2.6"
/* v5.2.7 corrects native test-harness portability; format remains v1.6. */
/* v5.2.8 hardens three path-race boundaries; format remains v1.6. */
#define ZUPT_VERSION_STRING "5.2.8"
/* Vendored codec release (upstream tag) — single source for display strings.
* The codec's own VV_VERSION_* is its internal API version, not the release. */
#define ZUPT_CODEC_RELEASE "2.65.3"

View file

@ -6,7 +6,7 @@
set -Eeuo pipefail
umask 077
VERSION=${VERSION:-5.2.6}
VERSION=${VERSION:-5.2.8}
PREFIX=${PREFIX:-/usr/local}
echo "🔧 Installing ZUPT..."

View file

@ -11,7 +11,7 @@
# Test locally with `makepkg -s` after the release archive is published.
pkgname=zupt
pkgver=5.2.6
pkgver=5.2.8
pkgrel=1
pkgdesc='Pure-C11 post-quantum backup compression utility (AES-256-CTR + HMAC-SHA256 + ML-KEM-768 + X25519)'
arch=('x86_64')
@ -22,8 +22,8 @@ makedepends=('gcc' 'git' 'make')
checkdepends=('python')
source=("${pkgname}-${pkgver}.tar.gz::https://github.com/cristiancmoises/zupt/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
# Byte-reproducible upstream v5.2.6 source archive.
sha256sums=('e64bd92aac1b50018efd3c8b678c70771320a2069d47f32c03349c569100b27c')
# Byte-reproducible upstream v5.2.8 source archive.
sha256sums=('378b9506211545b9594cf0d38ac8955d9b1cac34eb6b379ae0ec26b84edb65f7')
build() {
cd "${pkgname}-${pkgver}"

View file

@ -124,20 +124,49 @@ mapfile -t source_rpms < <(find "$top/SRPMS" -type f -name "zupt-gui-$version-*.
[[ ${#main_rpms[@]} -eq 1 ]] || die "expected one GUI RPM, found ${#main_rpms[@]}"
[[ ${#source_rpms[@]} -eq 1 ]] || die "expected one GUI source RPM, found ${#source_rpms[@]}"
rpm -qpl "${main_rpms[0]}" >"$work/contents.txt"
main_rpm=${main_rpms[0]}
source_rpm=${source_rpms[0]}
[[ $(rpm -qp --qf '%{NAME}' "$main_rpm") == zupt-gui ]] || \
die 'GUI binary RPM name metadata is not zupt-gui'
[[ $(rpm -qp --qf '%{VERSION}' "$main_rpm") == "$version" ]] || \
die 'GUI binary RPM version metadata does not match the release'
[[ $(rpm -qp --qf '%{RELEASE}' "$main_rpm") == 1 ]] || \
die 'GUI binary RPM release metadata is not 1'
[[ $(rpm -qp --qf '%{SOURCEPACKAGE}' "$main_rpm") == '(none)' ]] || \
die 'GUI binary RPM is marked as a source package'
[[ $(rpm -qp --qf '%{SOURCERPM}' "$main_rpm") == "$(basename -- "$source_rpm")" ]] || \
die 'GUI binary RPM does not reference the matching source RPM'
[[ $(rpm -qp --qf '%{NAME}' "$source_rpm") == zupt-gui ]] || \
die 'GUI source RPM name metadata is not zupt-gui'
[[ $(rpm -qp --qf '%{VERSION}' "$source_rpm") == "$version" ]] || \
die 'GUI source RPM version metadata does not match the release'
[[ $(rpm -qp --qf '%{RELEASE}' "$source_rpm") == 1 ]] || \
die 'GUI source RPM release metadata is not 1'
[[ $(rpm -qp --qf '%{SOURCEPACKAGE}' "$source_rpm") == 1 ]] || \
die 'GUI source RPM is not marked as a source package'
[[ $(rpm -qp --qf '%{SOURCERPM}' "$source_rpm") == '(none)' ]] || \
die 'GUI source RPM unexpectedly references another source RPM'
mapfile -t source_members < <(rpm -qpl "$source_rpm" | sort)
expected_source_members=("zupt-gui-${version}.tar.gz" zupt-gui.spec)
mapfile -t expected_source_members < <(printf '%s\n' "${expected_source_members[@]}" | sort)
[[ ${#source_members[@]} -eq 2 && \
${source_members[*]} == "${expected_source_members[*]}" ]] || \
die 'GUI source RPM payload is not the exact Source0/spec pair'
rpm -qpl "$main_rpm" >"$work/contents.txt"
grep -q '^/usr/bin/zupt-gui$' "$work/contents.txt" || die 'GUI launcher missing from RPM'
if grep -Eq '(^/usr/bin/vaptvupt-gui$|\.(o|obj|a|so|so\.[^/]+|dll|dylib|exe)$)' "$work/contents.txt"; then
cat "$work/contents.txt" >&2
die 'forbidden compatibility alias or compiled artifact in GUI RPM'
fi
(cd -- "$extract" && rpm2cpio "${main_rpms[0]}" | cpio -idm --quiet)
(cd -- "$extract" && rpm2cpio "$main_rpm" | cpio -idm --quiet)
PYTHONDONTWRITEBYTECODE=1 python3 - <<PY
from pathlib import Path
p = Path("$extract/usr/share/zupt-gui/zupt_gui.py")
compile(p.read_text(encoding="utf-8"), str(p), "exec")
PY
for artifact in "${main_rpms[0]}" "${source_rpms[0]}"; do
for artifact in "$main_rpm" "$source_rpm"; do
destination=$dist_dir/$(basename -- "$artifact")
[[ ! -e $destination ]] || die "refusing to overwrite existing output: $destination"
cp -- "$artifact" "$destination"

View file

@ -80,6 +80,33 @@ mapfile -t source_rpms < <(find "$top/SRPMS" -type f -name "zupt-${version}-*.sr
[[ ${#source_rpms[@]} -eq 1 ]] || die "expected one source RPM, found ${#source_rpms[@]}"
source_rpm=${source_rpms[0]}
[[ $(rpm -qp --qf '%{NAME}' "$main_rpm") == zupt ]] || \
die 'binary RPM name metadata is not zupt'
[[ $(rpm -qp --qf '%{VERSION}' "$main_rpm") == "$version" ]] || \
die 'binary RPM version metadata does not match the release'
[[ $(rpm -qp --qf '%{RELEASE}' "$main_rpm") == 0 ]] || \
die 'binary RPM release metadata is not 0'
[[ $(rpm -qp --qf '%{SOURCEPACKAGE}' "$main_rpm") == '(none)' ]] || \
die 'binary RPM is marked as a source package'
[[ $(rpm -qp --qf '%{SOURCERPM}' "$main_rpm") == "$(basename -- "$source_rpm")" ]] || \
die 'binary RPM does not reference the matching source RPM'
[[ $(rpm -qp --qf '%{NAME}' "$source_rpm") == zupt ]] || \
die 'source RPM name metadata is not zupt'
[[ $(rpm -qp --qf '%{VERSION}' "$source_rpm") == "$version" ]] || \
die 'source RPM version metadata does not match the release'
[[ $(rpm -qp --qf '%{RELEASE}' "$source_rpm") == 0 ]] || \
die 'source RPM release metadata is not 0'
[[ $(rpm -qp --qf '%{SOURCEPACKAGE}' "$source_rpm") == 1 ]] || \
die 'source RPM is not marked as a source package'
[[ $(rpm -qp --qf '%{SOURCERPM}' "$source_rpm") == '(none)' ]] || \
die 'source RPM unexpectedly references another source RPM'
mapfile -t source_members < <(rpm -qpl "$source_rpm" | sort)
expected_source_members=("zupt-${version}.tar.gz" zupt.spec)
mapfile -t expected_source_members < <(printf '%s\n' "${expected_source_members[@]}" | sort)
[[ ${#source_members[@]} -eq 2 && \
${source_members[*]} == "${expected_source_members[*]}" ]] || \
die 'source RPM payload is not the exact Source0/spec pair'
rpm -qpi "$main_rpm" >/dev/null
rpm -qpl "$main_rpm" > "$work/contents.txt"
if grep -Eq '(^/usr/bin/vaptvupt$|\.(o|obj|a|so|so\.[^/]+|dll|dylib)$)' "$work/contents.txt"; then

View file

@ -1,3 +1,27 @@
zupt (5.2.8-1) UNRELEASED; urgency=medium
* Close CodeQL High path-race findings in SDK key publication, disk-restore
target handling, and benchmark workspace cleanup.
* Treat a filesystem refusal to create the macOS raw-C1 scanner fixture as
an explicit skip; reject redirected Windows prompts before _getch; and run
sdk-test in the release and hosted Linux gates.
* Preserve immutable, unpromoted v5.2.7 run 33445470664: 13 jobs succeeded,
macOS failed the raw-C1 fixture, and Windows was cancelled after the hosted
job stalled; a MinGW/Wine reproduction isolated redirected _getch entry.
Require fresh 5.2.8 evidence.
-- Cristian Cezar Moisés <sac@securityops.co> Mon, 31 Aug 2026 23:30:00 +0000
zupt (5.2.7-1) UNRELEASED; urgency=medium
* Scope SHA-NI test helpers to supported x86 builds so macOS arm64 strict
compilation does not fail on unused declarations.
* Preserve safe UTF-8 fixture bytes across the Windows argv boundary.
* Preserve the immutable, unpromoted 5.2.6 history and require fresh 5.2.7
package, checksum, native-platform, OBS, and promotion gates.
-- Cristian Cezar Moisés <sac@securityops.co> Mon, 31 Aug 2026 23:00:00 +0000
zupt (5.2.6-1) UNRELEASED; urgency=medium
* Use the compiler-resistant volatile wipe fallback on macOS and NetBSD, and

View file

@ -62,7 +62,7 @@
xcb-util-renderutil xcb-util-wm xcb-util-cursor
libinput-minimal mtdev libevdev eudev))
(define %zupt-version "5.2.6")
(define %zupt-version "5.2.8")
(define %zupt-source
(origin
@ -72,7 +72,7 @@
"/releases/download/v" %zupt-version
"/zupt-" %zupt-version ".tar.gz"))
(sha256
(base32 "0z5j028md71l0cng6iwx0si204vpf266g2rwzn702l0vmhmdjjz6"))))
(base32 "1xv5vd7bh9pcw2d3fszb6jn1r6sxjp48mlzh9icvji8m4439b2rp"))))
(define-public zupt
(package

View file

@ -22,9 +22,9 @@
class Zupt < Formula
desc "Post-quantum backup compression utility (ML-KEM-768 + AES-256-CTR + HMAC-SHA256)"
homepage "https://github.com/cristiancmoises/zupt"
url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.6/zupt-5.2.6.tar.gz"
version "5.2.6"
sha256 "e64bd92aac1b50018efd3c8b678c70771320a2069d47f32c03349c569100b27c"
url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.8/zupt-5.2.8.tar.gz"
version "5.2.8"
sha256 "378b9506211545b9594cf0d38ac8955d9b1cac34eb6b379ae0ec26b84edb65f7"
license all_of: ["AGPL-3.0-or-later", "GPL-3.0-or-later", "BSD-2-Clause", "BSD-3-Clause", "CC0-1.0"]
depends_on "python@3.12" => :test # only for test-suite tamper harness

View file

@ -9,7 +9,7 @@
# nix flake check # lint the flake
#
# To consume from another flake:
# inputs.zupt.url = "github:cristiancmoises/zupt/v5.2.6";
# inputs.zupt.url = "github:cristiancmoises/zupt/v5.2.8";
# ...packages.x86_64-linux.default = inputs.zupt.packages.x86_64-linux.zupt;
#
# `make dist` has its own reproducibility gate. This development flake has no
@ -30,7 +30,7 @@
zupt = pkgs.stdenv.mkDerivation {
pname = "zupt";
version = "5.2.6";
version = "5.2.8";
# When publishing, replace this with `fetchurl` against the
# release tarball. For local development the flake assumes it

View file

@ -1,4 +1,4 @@
# ZUPT 5.2.6 for openSUSE Build Service
# ZUPT 5.2.8 for openSUSE Build Service
This directory is the upstream, source-only OBS recipe for ZUPT. It is a
handoff for the downstream maintainer; its presence does not mean that the
@ -10,14 +10,14 @@ changes in this handoff. Alessandro de Oliveira Faria (Cabelo) is credited only
as the openSUSE collaborator and downstream OBS package maintainer: he reviews
the handoff, commits it through the portal/project he maintains, and may make
the openSUSE-side adjustments he considers necessary. This role does not
attribute upstream code or the 5.2.2/5.2.3/5.2.4/5.2.5/5.2.6 upstream changes to
Cabelo.
attribute upstream code or the
5.2.2/5.2.3/5.2.4/5.2.5/5.2.6/5.2.7/5.2.8 upstream changes to Cabelo.
## Files and source policy
| File | Purpose |
|---|---|
| `_service` | Fetch the immutable `v5.2.6` tag and create `Source0` at build time. |
| `_service` | Fetch the immutable `v5.2.8` tag and create `Source0` at build time. |
| `zupt.spec` | Build and test the CLI with optional external system integrations disabled. |
| `zupt.changes` | openSUSE-format package history. |
| `source-audit.sh` | Handoff wrapper for the repository scanner; run it from the complete handoff tree. |
@ -30,11 +30,11 @@ https://github.com/cristiancmoises/zupt.git
```
`obs_scm` stores an `.obscpio` plus `.obsinfo`. The `tar` and `recompress`
services reconstruct `zupt-5.2.6.tar.gz` inside the build environment, which
services reconstruct `zupt-5.2.8.tar.gz` inside the build environment, which
matches `Source0` in the spec.
This source policy does not prohibit separately built release-page packages.
The upstream 5.2.6 gates may publish the CLI source tarball, DEB, binary RPM,
The upstream 5.2.8 gates may publish the CLI source tarball, DEB, binary RPM,
SRPM, notice-bearing Linux tar.xz, Windows ZIP, and macOS DMG, together with a
GUI DEB, noarch RPM, GUI SRPM, and source-only portable GUI ZIP after each
format-specific test succeeds. None of those files is an OBS `Source0` input
@ -139,7 +139,7 @@ reconstructed by the build-time services. Neither `%build` nor `%check` may
access the network.
For a source RPM check outside OBS, place the service-produced
`zupt-5.2.6.tar.gz` next to the spec and use a disposable RPM build tree:
`zupt-5.2.8.tar.gz` next to the spec and use a disposable RPM build tree:
```sh
rpm_top=$(mktemp -d)
@ -167,7 +167,7 @@ unavailable rather than passing it. Earlier off-screen GUI smoke evidence is
supporting evidence, not an exact-commit package result.
Post-tag CI integration failures prevented 5.2.2 promotion. These historical
local results do not establish 5.2.6, native Windows or macOS success, hosted
local results do not establish 5.2.8, native Windows or macOS success, hosted
GitHub CI/release promotion, authenticated OBS acceptance, or resolution of the
automatic openSUSE `debugsource` rpmlint `no-binary` finding. The immutable
5.2.3 candidate was not promoted because its source-policy test assumed LF for
@ -193,10 +193,10 @@ source scanner reported `PASS source-only: 204 files, 1 archives`.
This result establishes that the explicit tag revision works and isolates a
release/test harness defect. It does not change the product, archive format,
cryptography, codec, or SDK ABI; it does not make skipped native jobs pass or
establish authenticated OBS/Factory acceptance. No v5.2.4 evidence transfers
automatically to v5.2.6. The exact v5.2.6 candidate must repeat every applicable
gate, and the automatic openSUSE `debugsource` rpmlint `no-binary` finding
remains unresolved and unsuppressed.
establish authenticated OBS/Factory acceptance. No v5.2.4 evidence transferred
automatically to v5.2.8; the exact candidate later repeated every applicable
upstream gate in run `33456209269`, as recorded below. The automatic openSUSE
`debugsource` rpmlint `no-binary` finding remains unresolved and unsuppressed.
## Prior 5.2.5 exact-tag native-gate evidence
@ -206,8 +206,54 @@ macOS jobs. Windows exposed a hostile-path fixture that did not preserve its
requested bytes across the command-line boundary; macOS exposed the unsupported
`explicit_bzero` assumption and Bash 3.2 empty-array handling. The 5.2.6
corrections address those release/test integration defects without an archive,
cryptographic, codec, or SDK ABI change. They do not establish 5.2.6 hosted,
native, OBS, or promotion evidence.
cryptographic, codec, or SDK ABI change.
## Prior 5.2.6 exact-tag native-gate evidence
The immutable v5.2.6 candidate was not promoted. Exact-tag GitHub Actions run
`33442264243` completed 13 jobs successfully and failed two native jobs. The
macOS arm64 SHA-NI test build treated unused x86-only helper declarations as
errors under `-Werror`; Windows argv transcoding aborted the safe printable
UTF-8 fixture before its intended path assertions. The 5.2.7 changes correct
those test-harness boundaries without an archive-format, cryptographic, codec,
or SDK ABI change. They do not establish 5.2.8 hosted, native, OBS, or promotion
evidence.
## Prior 5.2.7 exact-tag native-gate evidence
The immutable v5.2.7 candidate was not promoted. Exact-tag GitHub Actions run
`33445470664` concluded `cancelled` at `2026-08-31T23:11:19Z`, with 13
successful jobs, one failed macOS job, and one cancelled Windows job. macOS
rejected creation of the raw-C1 scanner fixture
with `EILSEQ`; the hosted Windows job stalled in `make check`, and a MinGW/Wine
reproduction isolated the cause to a redirected password prompt entering
`_getch`. Version 5.2.8 makes those test
boundaries fail or skip without hanging, addresses CodeQL High #5/#6/#7 in SDK
key publication, disk restore, and benchmark cleanup, and adds `sdk-test` to
release and hosted Linux gates. None of those changes establishes an exact
5.2.8 OBS, native, hosted-CI, or promotion result.
## 5.2.8 exact-tag upstream package evidence
Manually dispatched exact-tag GitHub Actions run `33456209269` passed all 15
jobs at `ebb9ab3aa1d42c50030ca02883f6162dc4771fe1`. Its openSUSE Tumbleweed
x86_64 job parsed and normalized the spec, executed the pinned standalone OBS
source-service chain, source-scanned the resulting archive, built the binary
RPM and genuine SRPM, ran `rpmlint` without suppressions, and completed the
install/round-trip/uninstall test. The canonical source archive is 798296 bytes
with SHA-256
`378b9506211545b9594cf0d38ac8955d9b1cac34eb6b379ae0ec26b84edb65f7`.
Promotion run `33457868306` published the exact tested binary RPM and SRPM with
the other gated assets. The source package is identified by
`%{SOURCEPACKAGE}=1` and an absent `%{SOURCERPM}`; its `%{ARCH}` legitimately
reflects the spec's build architecture and is not the SRPM discriminator.
Repository, Git archive, and upstream source tarball scans remain binary-free.
This is upstream local-service and package evidence, not a claim that the
package was submitted to or accepted by openSUSE Factory, nor a result for the
full set of automatically generated OBS debug packages or any untested
architecture.
## Prior openSUSE packaging validation
@ -246,11 +292,11 @@ gate.
## Handoff procedure for Alessandro/Cabelo
1. Upstream completes every applicable pre-tag source and local audit gate,
then creates and verifies the annotated `v5.2.6` tag. Exact-tag hosted,
then creates and verifies the annotated `v5.2.8` tag. Exact-tag hosted,
native-platform, package, and promotion gates must pass before release or
downstream handoff; the tag itself is never moved to repair a failure.
2. With Git, `file`, bsdtar, tar, zip, unzip and SHA-256 tools installed, run
`scripts/export-opensuse-package.sh v5.2.6`. Verify the reported ZIP and
`scripts/export-opensuse-package.sh v5.2.8`. Verify the reported ZIP and
SHA-256 outside the Git index. The handoff includes both
`packaging/opensuse/source-audit.sh` and its required
`scripts/check-source-only.sh`; keep that relative layout while auditing.
@ -262,7 +308,7 @@ gate.
```
4. From the extracted handoff root, run
`packaging/opensuse/source-audit.sh --archive /path/to/zupt-5.2.6.tar.gz`.
`packaging/opensuse/source-audit.sh --archive /path/to/zupt-5.2.8.tar.gz`.
Then copy `_service`, `zupt.spec`, `zupt.changes` and `README.md`
into the flat OBS package checkout. The audit wrapper is not an OBS build
source and must not be copied without its companion `scripts/` directory.

View file

@ -4,7 +4,7 @@
<service name="obs_scm" mode="manual">
<param name="url">https://github.com/cristiancmoises/zupt.git</param>
<param name="scm">git</param>
<param name="revision">refs/tags/v5.2.6</param>
<param name="revision">refs/tags/v5.2.8</param>
<param name="versionformat">@PARENT_TAG@</param>
<param name="versionrewrite-pattern">^v(.*)$</param>
<param name="versionrewrite-replacement">\1</param>

View file

@ -1,3 +1,27 @@
-------------------------------------------------------------------
Mon Aug 31 23:30:00 UTC 2026 - Cristian Cezar Moisés <sac@securityops.co>
- Update to 5.2.8:
* Close CodeQL High path-race findings in SDK key publication,
descriptor-first disk restore, and benchmark workspace cleanup.
* Make the raw-C1 scanner fixture explicitly skip filesystems that reject
the byte with EILSEQ, reject redirected Windows prompts before _getch, and
add sdk-test to release/hosted Linux gates.
* Preserve immutable, unpromoted v5.2.7 run 33445470664: 13 jobs succeeded,
macOS failed the raw-C1 fixture, and Windows was cancelled after the hosted
job stalled; a MinGW/Wine reproduction isolated redirected _getch entry.
* Pin the OBS source service to the immutable v5.2.8 tag and require fresh
exact-candidate evidence before promotion.
-------------------------------------------------------------------
Mon Aug 31 23:00:00 UTC 2026 - Cristian Cezar Moisés <sac@securityops.co>
- Update to 5.2.7:
* Scope SHA-NI test helpers away from unsupported macOS arm64 builds.
* Preserve safe UTF-8 fixture bytes across the Windows argv boundary.
* Preserve immutable, unpromoted 5.2.6 history and require fresh 5.2.7 gates.
* Pin the OBS source service to the immutable v5.2.7 tag.
-------------------------------------------------------------------
Mon Aug 31 21:30:00 UTC 2026 - Cristian Cezar Moisés <sac@securityops.co>

View file

@ -18,7 +18,7 @@
#
Name: zupt
Version: 5.2.6
Version: 5.2.8
Release: 0
Summary: Backup compression with authenticated and post-quantum encryption
License: AGPL-3.0-or-later AND GPL-3.0-or-later AND BSD-2-Clause AND BSD-3-Clause AND CC0-1.0

View file

@ -27,7 +27,7 @@ Requirements
------------
1. Python 3.9 or newer.
2. PySide6 6.5 or newer, or a compatible PyQt6 package.
3. ZUPT 5.2.6, installed as `zupt` on PATH or placed beside the launcher
3. ZUPT 5.2.8, installed as `zupt` on PATH or placed beside the launcher
(`zupt.exe` on Windows). A local command must have been built
and tested independently; this bundle never downloads one.
@ -46,7 +46,7 @@ Troubleshooting
---------------
* "requires PySide6 or PyQt6": install one Qt binding through your operating
system package manager or another trusted, preconfigured Python source.
* "zupt not found": install ZUPT 5.2.6 or place its command beside
* "zupt not found": install ZUPT 5.2.8 or place its command beside
the launcher.
* Set ZUPT_DEBUG=1 to print command-discovery diagnostics to stderr.

View file

@ -20,7 +20,7 @@
# installed smoke test.
Name: zupt
Version: 5.2.6
Version: 5.2.8
Release: 1%{?dist}
Summary: Backup compression with authenticated and post-quantum encryption
@ -101,6 +101,20 @@ comments. Plain archives use non-cryptographic checksums.
%endif
%changelog
* Mon Aug 31 2026 Cristian Cezar Moisés <sac@securityops.co> - 5.2.8-1
- Close CodeQL High path-race findings in SDK key save, disk restore, and
benchmark cleanup; add the SDK gate, portable raw-C1 fixture handling, and
redirected Windows password-prompt rejection.
- Preserve immutable, unpromoted v5.2.7 run 33445470664: 13 jobs succeeded,
macOS failed the raw-C1 fixture, and Windows was cancelled after the hosted
job stalled; a MinGW/Wine reproduction isolated redirected _getch entry.
- Require fresh 5.2.8 gates.
* Mon Aug 31 2026 Cristian Cezar Moisés <sac@securityops.co> - 5.2.7-1
- Correct native test integration: scope SHA-NI helpers away from macOS arm64
and preserve safe UTF-8 fixture bytes across the Windows argv boundary.
- Preserve immutable, unpromoted 5.2.6 history and require fresh 5.2.7 gates.
* Mon Aug 31 2026 Cristian Cezar Moisés <sac@securityops.co> - 5.2.6-1
- Correct native release gates: use the secure volatile wipe fallback on
macOS and NetBSD, support Bash 3.2 empty arrays in the source scanner, and

View file

@ -165,6 +165,10 @@ safe_path_for_output() {
for ((index = 0; index < length; index++)); do
character=${path:index:1}
printf -v byte '%d' "'$character"
# Bash 3.2 can sign-extend bytes >= 0x80 when converting a character
# with %d. Normalize to an unsigned octet before UTF-8 validation and
# diagnostic escaping.
byte=$((byte & 0xff))
if ((byte < 0x20 || byte == 0x7f)); then
printf -v escaped '\\x%02x' "$byte"
@ -185,6 +189,7 @@ safe_path_for_output() {
if ((byte >= 0xc2 && byte <= 0xdf && index + 1 < length)); then
character=${path:index+1:1}
printf -v byte2 '%d' "'$character"
byte2=$((byte2 & 0xff))
if ((byte2 >= 0x80 && byte2 <= 0xbf)); then
codepoint=$(((byte & 0x1f) << 6 | (byte2 & 0x3f)))
sequence=${path:index:2}
@ -192,8 +197,10 @@ safe_path_for_output() {
elif ((byte >= 0xe0 && byte <= 0xef && index + 2 < length)); then
character=${path:index+1:1}
printf -v byte2 '%d' "'$character"
byte2=$((byte2 & 0xff))
character=${path:index+2:1}
printf -v byte3 '%d' "'$character"
byte3=$((byte3 & 0xff))
if ((byte3 >= 0x80 && byte3 <= 0xbf &&
((byte == 0xe0 && byte2 >= 0xa0 && byte2 <= 0xbf) ||
(byte >= 0xe1 && byte <= 0xec && byte2 >= 0x80 && byte2 <= 0xbf) ||
@ -206,10 +213,13 @@ safe_path_for_output() {
elif ((byte >= 0xf0 && byte <= 0xf4 && index + 3 < length)); then
character=${path:index+1:1}
printf -v byte2 '%d' "'$character"
byte2=$((byte2 & 0xff))
character=${path:index+2:1}
printf -v byte3 '%d' "'$character"
byte3=$((byte3 & 0xff))
character=${path:index+3:1}
printf -v byte4 '%d' "'$character"
byte4=$((byte4 & 0xff))
if ((byte3 >= 0x80 && byte3 <= 0xbf &&
byte4 >= 0x80 && byte4 <= 0xbf &&
((byte == 0xf0 && byte2 >= 0x90 && byte2 <= 0xbf) ||

View file

@ -556,20 +556,46 @@ void zuptsdk_keypair_destroy(zuptsdk_keypair_t *kp) {
static int zsdk_copy_file(const char *src, const char *dst, mode_t mode) {
FILE *fi = fopen(src, "rb");
if (!fi) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open %s", src);
FILE *fo = fopen(dst, "wb");
if (!fo) { fclose(fi); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create %s", dst); }
uint8_t buf[4096];
size_t n;
int rc = ZUPTSDK_OK;
while ((n = fread(buf, 1, sizeof(buf), fi)) > 0)
if (fwrite(buf, 1, n, fo) != n) { rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "write %s", dst); break; }
zuptsdk_secure_zero(buf, sizeof(buf));
fclose(fi); fclose(fo);
FILE *fo = NULL;
zupt_atomic_output_t *output = zupt_atomic_output_open(dst, &fo);
if (!output) {
int saved_errno = errno;
fclose(fi);
errno = saved_errno;
return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create %s", dst);
}
#ifndef _WIN32
if (rc == ZUPTSDK_OK) chmod(dst, mode);
/* Apply permissions to the private temporary object, never to a
* re-resolved destination path. */
if (fchmod(fileno(fo), mode) != 0) {
int saved_errno = errno;
fclose(fi);
(void)zupt_atomic_output_finish(output, 0);
errno = saved_errno;
return ZSDK_FAIL(ZUPTSDK_ERR_IO, "set permissions on %s", dst);
}
#else
(void)mode;
#endif
uint8_t buf[4096];
size_t n;
int rc = ZUPTSDK_OK;
while ((n = fread(buf, 1, sizeof(buf), fi)) > 0) {
if (fwrite(buf, 1, n, fo) != n) {
rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "write %s", dst);
break;
}
}
if (rc == ZUPTSDK_OK && ferror(fi))
rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "read %s", src);
zuptsdk_secure_zero(buf, sizeof(buf));
if (fclose(fi) != 0 && rc == ZUPTSDK_OK)
rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "close %s", src);
if (zupt_atomic_output_finish(output, rc == ZUPTSDK_OK) != 0 &&
rc == ZUPTSDK_OK)
rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "publish %s", dst);
return rc;
}

View file

@ -12,6 +12,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _WIN32
#include <fcntl.h>
#include <sys/stat.h>
#endif
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
@ -47,6 +51,98 @@ static const uint8_t TEST_DATA[] =
"Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod. "
"End of test data.\n";
#ifndef _WIN32
static int file_matches(const char *path, const void *expected,
size_t expected_size) {
struct stat info;
char observed[128];
if (expected_size > sizeof(observed))
return 0;
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) return 0;
int ok = fstat(fd, &info) == 0 && S_ISREG(info.st_mode) &&
info.st_size >= 0 &&
(uint64_t)info.st_size == (uint64_t)expected_size;
size_t got = 0;
while (ok && got < expected_size) {
ssize_t count = read(fd, observed + got, expected_size - got);
if (count <= 0) {
ok = 0;
break;
}
got += (size_t)count;
}
if (close(fd) != 0) ok = 0;
return ok && got == expected_size &&
memcmp(observed, expected, expected_size) == 0;
}
static int regular_file_info(const char *path, struct stat *info) {
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) return 0;
int ok = fstat(fd, info) == 0 && S_ISREG(info->st_mode);
if (close(fd) != 0) ok = 0;
return ok;
}
static int private_key_save_avoids_link_targets(const zuptsdk_keypair_t *kp) {
static const char sentinel[] = "do not replace through a symlink\n";
char workspace[] = "/tmp/zupt-sdk-link-save.XXXXXX";
char target[192];
char symlink_path[192];
char hardlink_path[192];
FILE *stream;
struct stat target_st;
struct stat output_st;
int ok = 0;
if (!mkdtemp(workspace)) return 0;
snprintf(target, sizeof(target), "%s/target", workspace);
snprintf(symlink_path, sizeof(symlink_path), "%s/symlink-output",
workspace);
snprintf(hardlink_path, sizeof(hardlink_path), "%s/hardlink-output",
workspace);
stream = fopen(target, "wb");
if (!stream) goto cleanup;
size_t written = fwrite(sentinel, 1, sizeof(sentinel) - 1, stream);
int close_rc = fclose(stream);
if (written != sizeof(sentinel) - 1 || close_rc != 0)
goto cleanup;
if (symlink(target, symlink_path) != 0 ||
zuptsdk_keypair_save_private(kp, symlink_path) != ZUPTSDK_OK ||
!file_matches(target, sentinel, sizeof(sentinel) - 1) ||
!regular_file_info(target, &target_st) ||
!regular_file_info(symlink_path, &output_st) ||
(target_st.st_dev == output_st.st_dev &&
target_st.st_ino == output_st.st_ino) ||
output_st.st_size <= 0 ||
(output_st.st_mode & 0777) != 0600)
goto cleanup;
if (link(target, hardlink_path) != 0 ||
zuptsdk_keypair_save_private(kp, hardlink_path) != ZUPTSDK_OK ||
!file_matches(target, sentinel, sizeof(sentinel) - 1) ||
!regular_file_info(target, &target_st) ||
!regular_file_info(hardlink_path, &output_st) ||
(target_st.st_dev == output_st.st_dev &&
target_st.st_ino == output_st.st_ino) ||
output_st.st_size <= 0 ||
(output_st.st_mode & 0777) != 0600)
goto cleanup;
ok = 1;
cleanup:
unlink(symlink_path);
unlink(hardlink_path);
unlink(target);
rmdir(workspace);
return ok;
}
#endif
static void test_version(void) {
TEST("version_string returns non-NULL");
const char *v = zuptsdk_version_string();
@ -250,6 +346,19 @@ cleanup:
static void test_keypair_pq(void) {
TEST("keypair_generate + compress_pq + extract_pq");
char saved_priv[160];
char saved_pub[160];
#ifdef _WIN32
snprintf(saved_priv, sizeof(saved_priv), "/tmp/_zsdk_priv_%ld.key",
(long)getpid());
snprintf(saved_pub, sizeof(saved_pub), "/tmp/_zsdk_pub_%ld.key",
(long)getpid());
unlink(saved_priv);
unlink(saved_pub);
#else
char saved_workspace[] = "/tmp/zupt-sdk-roundtrip.XXXXXX";
#endif
zuptsdk_ctx_t *ctx = NULL;
CHECK(zuptsdk_ctx_create(&ctx), "ctx");
@ -257,17 +366,45 @@ static void test_keypair_pq(void) {
int rc = zuptsdk_keypair_generate(ctx, &kp);
if (rc != ZUPTSDK_OK) { FAIL("keygen"); zuptsdk_ctx_destroy(ctx); return; }
#ifndef _WIN32
if (!mkdtemp(saved_workspace)) {
FAIL("private temporary workspace");
zuptsdk_keypair_destroy(kp);
zuptsdk_ctx_destroy(ctx);
return;
}
snprintf(saved_priv, sizeof(saved_priv), "%s/private.key",
saved_workspace);
snprintf(saved_pub, sizeof(saved_pub), "%s/public.key",
saved_workspace);
if (!private_key_save_avoids_link_targets(kp)) {
FAIL("private key save followed a symlink or hardlink target");
goto err;
}
#endif
/* Save and load to exercise that path too */
rc = zuptsdk_keypair_save_private(kp, "/tmp/_zsdk_priv.key");
rc = zuptsdk_keypair_save_private(kp, saved_priv);
if (rc != ZUPTSDK_OK) { FAIL("save priv"); goto err; }
rc = zuptsdk_keypair_save_public(kp, "/tmp/_zsdk_pub.key");
rc = zuptsdk_keypair_save_public(kp, saved_pub);
if (rc != ZUPTSDK_OK) { FAIL("save pub"); goto err; }
#ifndef _WIN32
struct stat private_st;
struct stat public_st;
if (!regular_file_info(saved_priv, &private_st) ||
!regular_file_info(saved_pub, &public_st) ||
(private_st.st_mode & 0777) != 0600 ||
(public_st.st_mode & 0777) != 0644) {
FAIL("saved key permissions do not match the requested modes");
goto err;
}
#endif
zuptsdk_pubkey_t *pub = NULL;
zuptsdk_privkey_t *priv = NULL;
rc = zuptsdk_pubkey_load("/tmp/_zsdk_pub.key", &pub);
rc = zuptsdk_pubkey_load(saved_pub, &pub);
if (rc != ZUPTSDK_OK) { FAIL("load pub"); goto err; }
rc = zuptsdk_privkey_load("/tmp/_zsdk_priv.key", &priv);
rc = zuptsdk_privkey_load(saved_priv, &priv);
if (rc != ZUPTSDK_OK) { FAIL("load priv"); zuptsdk_pubkey_destroy(pub); goto err; }
zuptsdk_options_t *opts = NULL;
@ -295,8 +432,10 @@ static void test_keypair_pq(void) {
zuptsdk_privkey_destroy(priv);
zuptsdk_options_destroy(opts);
unlink("/tmp/_zsdk_priv.key");
unlink("/tmp/_zsdk_pub.key");
if (unlink(saved_priv) != 0 || unlink(saved_pub) != 0) ok = 0;
#ifndef _WIN32
if (rmdir(saved_workspace) != 0) ok = 0;
#endif
if (!ok) { FAIL("byte mismatch or rc != OK"); zuptsdk_keypair_destroy(kp); zuptsdk_ctx_destroy(ctx); return; }
zuptsdk_keypair_destroy(kp);
@ -305,6 +444,11 @@ static void test_keypair_pq(void) {
return;
err:
unlink(saved_priv);
unlink(saved_pub);
#ifndef _WIN32
rmdir(saved_workspace);
#endif
zuptsdk_keypair_destroy(kp);
zuptsdk_ctx_destroy(ctx);
}

View file

@ -1220,23 +1220,35 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
#else
int tgt_fd = -1;
int is_block_dev = 0;
struct stat target_st;
if (lstat(target_path, &target_st) == 0) {
if (S_ISLNK(target_st.st_mode)) {
fprintf(stderr, "Error: refusing a symbolic-link restore target.\n");
fclose(f);
return ZUPT_ERR_INVALID;
}
if (S_ISREG(target_st.st_mode)) {
if (target_st.st_dev == archive_identity.device &&
target_st.st_ino == archive_identity.inode) {
/* Resolve the target exactly once before making any type or identity
* decision. The open is non-truncating, O_NOFOLLOW rejects a final
* symlink, and fstat classifies the kernel object that was actually
* opened. Device restores retain this same descriptor through the final
* write, so a concurrent pathname exchange cannot redirect the restore. */
tgt_fd = open(target_path, O_WRONLY | O_NOFOLLOW | O_CLOEXEC |
O_NONBLOCK | O_SYNC);
if (tgt_fd >= 0) {
struct stat opened_st;
if (fstat(tgt_fd, &opened_st) != 0) {
int saved_errno = errno;
close(tgt_fd);
tgt_fd = -1;
errno = saved_errno;
} else if (S_ISREG(opened_st.st_mode)) {
int close_result = close(tgt_fd);
tgt_fd = -1;
if (close_result != 0) {
fclose(f);
return ZUPT_ERR_IO;
}
if (opened_st.st_dev == archive_identity.device &&
opened_st.st_ino == archive_identity.inode) {
fprintf(stderr,
"Error: archive and restore target are the same file.\n");
fclose(f);
return ZUPT_ERR_INVALID;
}
if (target_st.st_nlink != 1) {
if (opened_st.st_nlink != 1) {
fprintf(stderr,
"Error: refusing a multiply-linked restore target.\n");
fclose(f);
@ -1244,59 +1256,46 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
}
target_atomic =
zupt_atomic_output_open(target_path, &target_stream);
} else if (S_ISBLK(target_st.st_mode) ||
S_ISCHR(target_st.st_mode)) {
tgt_fd = open(target_path, O_WRONLY | O_NOFOLLOW | O_CLOEXEC |
O_NONBLOCK | O_SYNC);
if (tgt_fd >= 0) {
struct stat opened_st;
if (fstat(tgt_fd, &opened_st) != 0 ||
opened_st.st_dev != target_st.st_dev ||
opened_st.st_ino != target_st.st_ino ||
!(S_ISBLK(opened_st.st_mode) ||
S_ISCHR(opened_st.st_mode))) {
} else if (S_ISBLK(opened_st.st_mode) ||
S_ISCHR(opened_st.st_mode)) {
int flags = fcntl(tgt_fd, F_GETFL);
if (flags < 0 ||
fcntl(tgt_fd, F_SETFL, flags & ~O_NONBLOCK) != 0) {
close(tgt_fd);
tgt_fd = -1;
} else {
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
uint64_t target_capacity = 0;
if (!disk_restore_target_capacity(
tgt_fd, &opened_st, &target_capacity)) {
fprintf(stderr,
"Error: cannot determine restore device "
"capacity safely.\n");
close(tgt_fd);
tgt_fd = -1;
errno = EAGAIN;
} else if (expected_size > target_capacity) {
fprintf(stderr,
"Error: disk image (%llu bytes) exceeds "
"restore device capacity (%llu bytes).\n",
(unsigned long long)expected_size,
(unsigned long long)target_capacity);
close(tgt_fd);
tgt_fd = -1;
errno = EFBIG;
} else {
int flags = fcntl(tgt_fd, F_GETFL);
if (flags < 0 ||
fcntl(tgt_fd, F_SETFL, flags & ~O_NONBLOCK) != 0) {
close(tgt_fd);
tgt_fd = -1;
} else {
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
uint64_t target_capacity = 0;
if (!disk_restore_target_capacity(
tgt_fd, &opened_st, &target_capacity)) {
fprintf(stderr,
"Error: cannot determine restore device "
"capacity safely.\n");
close(tgt_fd);
tgt_fd = -1;
} else if (expected_size > target_capacity) {
fprintf(stderr,
"Error: disk image (%llu bytes) exceeds "
"restore device capacity (%llu bytes).\n",
(unsigned long long)expected_size,
(unsigned long long)target_capacity);
close(tgt_fd);
tgt_fd = -1;
errno = EFBIG;
} else {
is_block_dev = 1;
}
#else
fprintf(stderr,
"Error: restore-device capacity queries are "
"not supported on this platform.\n");
close(tgt_fd);
tgt_fd = -1;
#endif
}
is_block_dev = 1;
}
#else
fprintf(stderr,
"Error: restore-device capacity queries are "
"not supported on this platform.\n");
close(tgt_fd);
tgt_fd = -1;
#endif
}
} else {
close(tgt_fd);
tgt_fd = -1;
fprintf(stderr,
"Error: restore target is not a regular file or device.\n");
fclose(f);
@ -1304,8 +1303,12 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
}
} else if (errno == ENOENT) {
target_atomic = zupt_atomic_output_open(target_path, &target_stream);
} else if (errno == ELOOP) {
fprintf(stderr, "Error: refusing a symbolic-link restore target.\n");
fclose(f);
return ZUPT_ERR_INVALID;
} else {
fprintf(stderr, "Error: Cannot inspect target '%s': %s\n",
fprintf(stderr, "Error: Cannot open target '%s': %s\n",
target_path, strerror(errno));
fclose(f);
return ZUPT_ERR_IO;

View file

@ -4221,7 +4221,7 @@ static zupt_error_t open_archive(FILE *f, zupt_options_t *opts,
* parser ignored block_type at this position and decoded whatever it
* found making the byte truly unauthenticated. Now it is structurally
* validated (rejected at parse time on mismatch), which is the
* OPAQUE-class coverage promised by PROMPT.md §5. */
* OPAQUE-class structural coverage recorded in the audit history. */
if (ib.block_type != ZUPT_BLOCK_INDEX) {
free(ib.payload);
return ZUPT_ERR_CORRUPT;

View file

@ -25,7 +25,9 @@
#ifdef _WIN32
#include <conio.h>
#include <windows.h>
#include <winternl.h>
#else
#include <fcntl.h>
#include <signal.h>
#include <termios.h>
#endif
@ -100,9 +102,11 @@ static int zupt_create_private_temp_directory(char *output, size_t capacity) {
}
return 0;
#else
static const char pattern[] = "/tmp/zupt-bench-XXXXXX";
if (sizeof(pattern) > capacity) return 0;
memcpy(output, pattern, sizeof(pattern));
char temp_root[ZUPT_MAX_PATH];
if (!realpath("/tmp", temp_root)) return 0;
int written = snprintf(output, capacity, "%s/zupt-bench-XXXXXX",
temp_root);
if (written < 0 || (size_t)written >= capacity) return 0;
if (!mkdtemp(output)) return 0;
if (chmod(output, 0700) != 0) {
rmdir(output);
@ -114,7 +118,97 @@ static int zupt_create_private_temp_directory(char *output, size_t capacity) {
}
#ifdef _WIN32
static int zupt_remove_tree_wide(const wchar_t *directory) {
static void zupt_win_set_cleanup_errno(NTSTATUS status) {
if (status == (NTSTATUS)0xC0000034L || /* STATUS_OBJECT_NAME_NOT_FOUND */
status == (NTSTATUS)0xC000003AL) { /* STATUS_OBJECT_PATH_NOT_FOUND */
errno = ENOENT;
} else {
errno = EACCES;
}
}
/* Open one entry relative to a pinned parent. Omitting FILE_SHARE_DELETE
* keeps the name bound to this handle until cleanup finishes; opening the
* reparse point itself prevents a junction or symlink from redirecting the
* recursive walk. */
static HANDLE zupt_win_open_cleanup_entry(HANDLE parent,
const wchar_t *name,
int directory_only,
int delete_access) {
size_t name_length = wcslen(name);
if (name_length == 0 ||
name_length > (size_t)USHRT_MAX / sizeof(wchar_t)) {
errno = ENAMETOOLONG;
return INVALID_HANDLE_VALUE;
}
UNICODE_STRING object_name;
object_name.Buffer = (PWSTR)name;
object_name.Length = (USHORT)(name_length * sizeof(wchar_t));
object_name.MaximumLength = object_name.Length + sizeof(wchar_t);
OBJECT_ATTRIBUTES attributes;
InitializeObjectAttributes(&attributes, &object_name,
OBJ_CASE_INSENSITIVE, parent, NULL);
IO_STATUS_BLOCK status_block;
HANDLE handle = INVALID_HANDLE_VALUE;
ACCESS_MASK access = FILE_LIST_DIRECTORY | FILE_TRAVERSE |
FILE_READ_ATTRIBUTES | SYNCHRONIZE;
if (delete_access) access |= DELETE;
ULONG share = FILE_SHARE_READ | FILE_SHARE_WRITE;
if (delete_access) share |= FILE_SHARE_DELETE;
ULONG options = FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT;
if (directory_only) options |= FILE_DIRECTORY_FILE;
NTSTATUS status = NtCreateFile(
&handle, access, &attributes, &status_block, NULL,
FILE_ATTRIBUTE_NORMAL, share, FILE_OPEN,
options, NULL, 0);
if (status < 0 || handle == INVALID_HANDLE_VALUE) {
zupt_win_set_cleanup_errno(status);
return INVALID_HANDLE_VALUE;
}
return handle;
}
/* Mark the exact object held by an identity-checked deletion handle. */
static int zupt_win_delete_cleanup_handle(HANDLE handle) {
FILE_DISPOSITION_INFO disposition;
disposition.DeleteFile = TRUE;
if (SetFileInformationByHandle(handle, FileDispositionInfo,
&disposition, sizeof(disposition)))
return 1;
errno = EACCES;
return 0;
}
/* Reopen an emptied child only after closing its no-delete-sharing traversal
* handle. Comparing the filesystem identity before marking the new handle
* for deletion makes a close/reopen name exchange fail safely. */
static int zupt_win_delete_cleanup_entry(
HANDLE parent, const wchar_t *name,
const BY_HANDLE_FILE_INFORMATION *expected) {
HANDLE handle = zupt_win_open_cleanup_entry(parent, name, 1, 1);
if (handle == INVALID_HANDLE_VALUE) return 0;
BY_HANDLE_FILE_INFORMATION current;
int same = GetFileInformationByHandle(handle, &current) &&
(current.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
(current.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 &&
current.dwVolumeSerialNumber == expected->dwVolumeSerialNumber &&
current.nFileIndexHigh == expected->nFileIndexHigh &&
current.nFileIndexLow == expected->nFileIndexLow;
int deleted = same && zupt_win_delete_cleanup_handle(handle);
int closed = CloseHandle(handle) != 0;
if (!same) errno = EBUSY;
return deleted && closed;
}
static int zupt_win_plain_directory(HANDLE handle) {
BY_HANDLE_FILE_INFORMATION info;
return GetFileInformationByHandle(handle, &info) &&
(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
(info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0;
}
static int zupt_remove_tree_wide(HANDLE directory_handle,
const wchar_t *directory) {
size_t directory_length = wcslen(directory);
wchar_t *pattern = (wchar_t *)calloc(directory_length + 3u,
sizeof(*pattern));
@ -125,6 +219,8 @@ static int zupt_remove_tree_wide(const wchar_t *directory) {
WIN32_FIND_DATAW data;
HANDLE search = FindFirstFileW(pattern, &data);
DWORD search_error = search == INVALID_HANDLE_VALUE
? GetLastError() : ERROR_SUCCESS;
free(pattern);
int failed = 0;
if (search != INVALID_HANDLE_VALUE) {
@ -143,23 +239,217 @@ static int zupt_remove_tree_wide(const wchar_t *directory) {
child[directory_length] = L'\\';
memcpy(child + directory_length + 1u, data.cFileName,
(name_length + 1u) * sizeof(*child));
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
if (!RemoveDirectoryW(child)) failed = 1;
} else if (zupt_remove_tree_wide(child) != 0) {
failed = 1;
}
} else {
SetFileAttributesW(child, FILE_ATTRIBUTE_NORMAL);
if (!DeleteFileW(child)) failed = 1;
if (DeleteFileW(child) || RemoveDirectoryW(child)) {
free(child);
continue;
}
DWORD delete_error = GetLastError();
if (delete_error == ERROR_FILE_NOT_FOUND ||
delete_error == ERROR_PATH_NOT_FOUND) {
free(child);
continue;
}
HANDLE child_handle = zupt_win_open_cleanup_entry(
directory_handle, data.cFileName, 1, 0);
if (child_handle == INVALID_HANDLE_VALUE) {
if (errno != ENOENT) failed = 1;
free(child);
continue;
}
int child_failed = 0;
BY_HANDLE_FILE_INFORMATION child_identity;
if (!GetFileInformationByHandle(child_handle, &child_identity) ||
(child_identity.dwFileAttributes &
FILE_ATTRIBUTE_DIRECTORY) == 0 ||
(child_identity.dwFileAttributes &
FILE_ATTRIBUTE_REPARSE_POINT) != 0 ||
zupt_remove_tree_wide(child_handle, child) != 0)
child_failed = 1;
if (!CloseHandle(child_handle)) child_failed = 1;
if (!child_failed && !zupt_win_delete_cleanup_entry(
directory_handle, data.cFileName, &child_identity))
child_failed = 1;
if (child_failed) failed = 1;
free(child);
} while (FindNextFileW(search, &data));
if (GetLastError() != ERROR_NO_MORE_FILES) failed = 1;
if (!FindClose(search)) failed = 1;
} else if (GetLastError() != ERROR_FILE_NOT_FOUND) {
} else if (search_error != ERROR_FILE_NOT_FOUND) {
failed = 1;
}
if (!RemoveDirectoryW(directory)) failed = 1;
return failed ? -1 : 0;
}
/* Resolve the absolute temporary path one component at a time and retain
* every ancestor handle. This makes the pathname used for enumeration
* stable even if another process tries to exchange an ancestor directory. */
static int zupt_win_open_cleanup_path(
const wchar_t *directory, wchar_t full[ZUPT_MAX_PATH + 256],
HANDLE **handles_out, size_t *handle_count_out) {
if (!_wfullpath(full, directory, ZUPT_MAX_PATH + 256)) {
errno = EINVAL;
return 0;
}
for (wchar_t *p = full; *p; p++) if (*p == L'/') *p = L'\\';
if ((full[0] == L'\\' && full[1] == L'\\') ||
!(full[0] && full[1] == L':' && full[2] == L'\\')) {
errno = EINVAL;
return 0;
}
size_t capacity = wcslen(full) + 1u;
HANDLE *handles = (HANDLE *)calloc(capacity, sizeof(*handles));
if (!handles) return 0;
wchar_t drive_root[4] = {full[0], L':', L'\\', L'\0'};
HANDLE current = CreateFileW(
drive_root,
FILE_LIST_DIRECTORY | FILE_TRAVERSE | FILE_READ_ATTRIBUTES |
SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
if (current == INVALID_HANDLE_VALUE ||
!zupt_win_plain_directory(current)) {
DWORD open_error = current == INVALID_HANDLE_VALUE
? GetLastError() : ERROR_ACCESS_DENIED;
if (current != INVALID_HANDLE_VALUE) CloseHandle(current);
free(handles);
errno = open_error == ERROR_FILE_NOT_FOUND ||
open_error == ERROR_PATH_NOT_FOUND
? ENOENT : EACCES;
return 0;
}
size_t count = 0;
handles[count++] = current;
wchar_t *scan = full + 3;
while (*scan) {
wchar_t *separator = wcschr(scan, L'\\');
if (separator) *separator = L'\0';
HANDLE next = zupt_win_open_cleanup_entry(
current, scan, 1, 0);
if (separator) *separator = L'\\';
if (next == INVALID_HANDLE_VALUE ||
!zupt_win_plain_directory(next)) {
if (next != INVALID_HANDLE_VALUE) CloseHandle(next);
while (count > 0) CloseHandle(handles[--count]);
free(handles);
if (next != INVALID_HANDLE_VALUE) errno = EACCES;
return 0;
}
handles[count++] = next;
current = next;
if (!separator) break;
scan = separator + 1;
}
*handles_out = handles;
*handle_count_out = count;
return 1;
}
#endif
#ifndef _WIN32
/* Resolve every component without following symlinks and return both the
* pinned target and its pinned parent. The caller can therefore remove the
* final directory with unlinkat() instead of resolving its pathname again. */
static int zupt_open_temp_tree(const char *path, int *parent_out,
int *directory_out, char *leaf,
size_t leaf_capacity) {
if (!path || !*path || !parent_out || !directory_out || !leaf ||
leaf_capacity == 0) {
errno = EINVAL;
return 0;
}
int current = open(path[0] == '/' ? "/" : ".",
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (current < 0) return 0;
const char *cursor = path;
while (*cursor == '/') cursor++;
while (*cursor) {
const char *start = cursor;
while (*cursor && *cursor != '/') cursor++;
size_t component_length = (size_t)(cursor - start);
while (*cursor == '/') cursor++;
int final_component = *cursor == '\0';
if ((component_length == 1u && start[0] == '.') ||
component_length == 0u) {
if (final_component) {
close(current);
errno = EINVAL;
return 0;
}
continue;
}
if (component_length == 2u && start[0] == '.' && start[1] == '.') {
close(current);
errno = EINVAL;
return 0;
}
if (component_length >= leaf_capacity) {
close(current);
errno = ENAMETOOLONG;
return 0;
}
memcpy(leaf, start, component_length);
leaf[component_length] = '\0';
int next = openat(current, leaf,
O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (next < 0) {
int saved_errno = errno;
close(current);
errno = saved_errno;
return 0;
}
if (final_component) {
*parent_out = current;
*directory_out = next;
return 1;
}
close(current);
current = next;
}
close(current);
errno = EINVAL;
return 0;
}
/* Delete leaves before attempting to open them as directories. unlinkat()
* never follows a symlink; a directory is recursively visited only through
* an O_NOFOLLOW descriptor returned by openat(). */
static int zupt_remove_temp_tree_fd(int directory_fd) {
DIR *stream = fdopendir(directory_fd);
if (!stream) {
close(directory_fd);
return -1;
}
int failed = 0;
int parent_fd = dirfd(stream);
for (;;) {
errno = 0;
struct dirent *entry = readdir(stream);
if (!entry) {
if (errno != 0) failed = 1;
break;
}
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
if (unlinkat(parent_fd, entry->d_name, 0) == 0 || errno == ENOENT)
continue;
int child_fd = openat(parent_fd, entry->d_name,
O_RDONLY | O_DIRECTORY | O_NOFOLLOW |
O_CLOEXEC);
if (child_fd < 0) {
if (errno != ENOENT) failed = 1;
continue;
}
if (zupt_remove_temp_tree_fd(child_fd) != 0) failed = 1;
if (unlinkat(parent_fd, entry->d_name, AT_REMOVEDIR) != 0 &&
errno != ENOENT)
failed = 1;
}
if (closedir(stream) != 0) failed = 1;
return failed ? -1 : 0;
}
#endif
@ -169,41 +459,43 @@ static int zupt_remove_temp_tree(const char *directory) {
#ifdef _WIN32
wchar_t *wide = zupt_win_utf8_to_wide_alloc(directory);
if (!wide) return -1;
int result = zupt_remove_tree_wide(wide);
wchar_t full[ZUPT_MAX_PATH + 256];
HANDLE *handles = NULL;
size_t handle_count = 0;
if (!zupt_win_open_cleanup_path(wide, full, &handles, &handle_count)) {
int result = errno == ENOENT ? 0 : -1;
free(wide);
return result;
}
HANDLE root_handle = handles[handle_count - 1u];
int result = zupt_remove_tree_wide(root_handle, full);
BY_HANDLE_FILE_INFORMATION root_identity;
if (result == 0 && !GetFileInformationByHandle(root_handle,
&root_identity))
result = -1;
const wchar_t *root_name = wcsrchr(full, L'\\');
if (!root_name || root_name[1] == L'\0') result = -1;
else root_name++;
if (!CloseHandle(handles[--handle_count])) result = -1;
if (result == 0 && !zupt_win_delete_cleanup_entry(
handles[handle_count - 1u], root_name, &root_identity))
result = -1;
while (handle_count > 0)
if (!CloseHandle(handles[--handle_count])) result = -1;
free(handles);
free(wide);
return result;
#else
DIR *stream = opendir(directory);
if (!stream) return errno == ENOENT ? 0 : -1;
int failed = 0;
struct dirent *entry;
while ((entry = readdir(stream)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
size_t needed = strlen(directory) + strlen(entry->d_name) + 2u;
char *child = (char *)malloc(needed);
if (!child) {
failed = 1;
continue;
}
if (!zupt_join_temp_path(child, needed, directory, entry->d_name)) {
free(child);
failed = 1;
continue;
}
struct stat info;
if (lstat(child, &info) != 0) {
failed = 1;
} else if (S_ISDIR(info.st_mode)) {
if (zupt_remove_temp_tree(child) != 0) failed = 1;
} else if (unlink(child) != 0) {
failed = 1;
}
free(child);
}
if (closedir(stream) != 0) failed = 1;
if (rmdir(directory) != 0) failed = 1;
int parent_fd = -1;
int directory_fd = -1;
char leaf[ZUPT_MAX_PATH];
if (!zupt_open_temp_tree(directory, &parent_fd, &directory_fd,
leaf, sizeof(leaf)))
return errno == ENOENT ? 0 : -1;
int failed = zupt_remove_temp_tree_fd(directory_fd) != 0;
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) != 0 && errno != ENOENT)
failed = 1;
if (close(parent_fd) != 0) failed = 1;
return failed ? -1 : 0;
#endif
}
@ -429,7 +721,11 @@ static int prompt_password(const char *prompt, char *buf, size_t cap) {
if (!buf || cap < 2) return 0;
buf[0] = '\0';
#ifdef _WIN32
if (!_isatty(_fileno(stdin))) {
HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE);
DWORD input_mode = 0;
if (input_handle == NULL || input_handle == INVALID_HANDLE_VALUE ||
GetFileType(input_handle) != FILE_TYPE_CHAR ||
!GetConsoleMode(input_handle, &input_mode)) {
fprintf(stderr, "Error: password prompt requires a terminal.\n");
return 0;
}
@ -445,6 +741,11 @@ static int prompt_password(const char *prompt, char *buf, size_t cap) {
int too_long = 0;
for (;;) {
int c = _getch();
if (c == EOF) {
zupt_secure_wipe(buf, cap);
fprintf(stderr, "\nError: cannot read password prompt.\n");
return 0;
}
if (c == '\r' || c == '\n') break;
if (c == 0 || c == 0xe0) {
(void)_getch();

View file

@ -2,7 +2,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
#
# Flake-stress harness — §3 of PROMPT.md.
# Repeated-suite flake-stress harness.
#
# Runs every short test suite N times (default 50) and aborts on the
# first non-deterministic outcome. Specifically targeted at the audit
@ -21,7 +21,7 @@ set -u
# F-02's repro needed 50 runs to be statistically convincing (~10%
# baseline flake rate), but at 20 runs we still have ~88% chance of
# catching a 10%-flake — fine for routine CI. For a hardened audit
# pass, invoke with 50 or 100 (see PROMPT.md §3).
# pass, invoke with 50 or 100 for a deeper audit run.
N="${1:-20}"
ZUPT_BIN="${ZUPT_BIN:-./zupt}"

View file

@ -3,6 +3,11 @@
set -Eeuo pipefail
bin=${1:-./zupt}
repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd -P)
case $bin in
/*) ;;
*) bin="$(pwd -P)/${bin#./}" ;;
esac
tmp=$(mktemp -d "${TMPDIR:-/tmp}/zupt-bench-safety.XXXXXXXX")
trap 'rm -rf -- "$tmp"' EXIT HUP INT TERM
@ -11,9 +16,36 @@ fail() {
exit 1
}
# CodeQL #7 reported the old lstat(child) -> recursive pathname operation as
# cpp/toctou-race-condition. Keep the platform-specific cleanup primitives in
# the source gate as well as exercising the runtime symlink boundary below.
cleanup_source=$repo_root/src/zupt_main.c
grep -Fq 'static int zupt_remove_temp_tree_fd(int directory_fd)' \
"$cleanup_source" || fail 'POSIX descriptor-relative cleanup is missing'
grep -Fq 'unlinkat(parent_fd, entry->d_name, 0)' "$cleanup_source" ||
fail 'POSIX leaf cleanup is not unlinkat-relative'
grep -Fq 'directory_handle, data.cFileName, 1, 0)' "$cleanup_source" ||
fail 'Windows recursive cleanup is not handle-relative'
grep -Fq 'FILE_OPEN_REPARSE_POINT' "$cleanup_source" ||
fail 'Windows cleanup no longer opens reparse points without following'
grep -Fq 'zupt_win_delete_cleanup_entry(' "$cleanup_source" ||
fail 'Windows cleanup lacks identity-checked handle deletion'
grep -Fq 'current.nFileIndexLow == expected->nFileIndexLow' "$cleanup_source" ||
fail 'Windows cleanup no longer rejects a close/reopen name exchange'
if grep -Fq 'RemoveDirectoryW(full)' "$cleanup_source"; then
fail 'Windows root cleanup restored post-handle pathname deletion'
fi
if grep -Fq 'lstat(child' "$cleanup_source" ||
grep -Fq 'zupt_remove_temp_tree(child' "$cleanup_source"; then
fail 'temporary cleanup restored a check-then-use pathname traversal'
fi
case $(uname -s 2>/dev/null || printf unknown) in
MINGW*|MSYS*|CYGWIN*)
printf 'SKIP: historical POSIX /tmp symlink benchmark test is not native on Windows\n'
"$bin" bench --compare >/dev/null 2>&1 ||
fail 'native Windows handle-relative benchmark cleanup failed'
printf 'SKIP: adversarial POSIX symlink injection is not native on Windows\n'
printf 'private Windows handle-relative benchmark workspace: PASS\n'
exit 0
;;
esac
@ -46,4 +78,50 @@ if [[ -d $old_directory ]]; then
mv "$old_directory" "$tmp/historical-remnant"
fi
printf 'private benchmark workspace: PASS\n'
# Inject a directory symlink into the private workspace while a real benchmark
# is active. Cleanup must remove the link itself and never visit its target.
mkdir "$tmp/symlink-target"
printf 'cleanup sentinel must survive\n' > "$tmp/symlink-target/sentinel"
cp "$tmp/symlink-target/sentinel" "$tmp/symlink-target.expected"
dd if=/dev/urandom of="$tmp/injection-input" bs=65536 count=128 2>/dev/null
physical_tmp=$(CDPATH='' cd -P -- /tmp && pwd -P)
: > "$tmp/preexisting-workspaces"
for candidate in "$physical_tmp"/zupt-bench-*; do
if [[ -d $candidate && ! -L $candidate ]]; then
printf '%s\n' "$candidate" >> "$tmp/preexisting-workspaces"
fi
done
(cd "$tmp" && "$bin" bench injection-input >/dev/null 2>&1) &
bench_pid=$!
injected=0
injected_workspace=
attempt=0
while (( attempt < 1000 )); do
for candidate in "$physical_tmp"/zupt-bench-*; do
[[ -d $candidate && ! -L $candidate ]] || continue
if grep -Fqx -- "$candidate" "$tmp/preexisting-workspaces"; then
continue
fi
if ln -s "$tmp/symlink-target" "$candidate/attacker-link" \
2>/dev/null; then
injected=1
injected_workspace=$candidate
break
fi
done
(( injected == 1 )) && break
kill -0 "$bench_pid" 2>/dev/null || break
sleep 0.01
attempt=$((attempt + 1))
done
wait "$bench_pid" || fail 'benchmark with injected symlink failed'
(( injected == 1 )) || fail 'could not observe the private benchmark workspace'
if [[ -e $injected_workspace || -L $injected_workspace ]]; then
fail 'injected workspace was not the benchmark tree that was removed'
fi
cmp "$tmp/symlink-target.expected" "$tmp/symlink-target/sentinel" ||
fail 'temporary cleanup followed an injected directory symlink'
printf 'private descriptor/handle-relative benchmark workspace: PASS\n'

View file

@ -41,17 +41,76 @@ file_mode() {
fi
}
windows_private_acl() {
local output=$1 windows_path
command -v cygpath >/dev/null 2>&1 || return 1
command -v powershell.exe >/dev/null 2>&1 || return 1
windows_path=$(cygpath -aw -- "$output") || return 1
# PowerShell variables must remain literal until powershell.exe evaluates
# this single-quoted Bash argument.
# shellcheck disable=SC2016
ZUPT_KEY_ACL_PATH=$windows_path powershell.exe -NoLogo -NoProfile \
-NonInteractive -Command '
$ErrorActionPreference = "Stop"
$acl = Get-Acl -LiteralPath $env:ZUPT_KEY_ACL_PATH
$sidType = [System.Security.Principal.SecurityIdentifier]
$rules = @($acl.GetAccessRules($true, $true, $sidType))
$currentSid =
[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
if (-not $acl.AreAccessRulesProtected) {
throw "private-key DACL permits inheritance"
}
if ($rules.Count -ne 1) {
throw "private-key DACL does not contain exactly one ACE"
}
$rule = $rules[0]
if ($rule.IsInherited) {
throw "private-key ACE is inherited"
}
if ($rule.AccessControlType -ne
[System.Security.AccessControl.AccessControlType]::Allow) {
throw "private-key ACE is not an allow rule"
}
if ($rule.IdentityReference.Value -ne $currentSid) {
throw "private-key ACE is not restricted to the current user"
}
if ($rule.InheritanceFlags -ne
[System.Security.AccessControl.InheritanceFlags]::None -or
$rule.PropagationFlags -ne
[System.Security.AccessControl.PropagationFlags]::None) {
throw "private-key ACE unexpectedly propagates"
}
$fullControl = [int64](
[System.Security.AccessControl.FileSystemRights]::FullControl)
$actualRights = [int64]($rule.FileSystemRights)
if (($actualRights -band $fullControl) -ne $fullControl) {
throw "private-key ACE does not grant current-user full control"
}
' </dev/null >/dev/null
}
generate_with_mode() {
local label=$1 mask=$2 output=$3
shift 3
if (umask "$mask"; "$zupt_bin" keygen "$@" -o "$output" >/dev/null 2>&1); then
local mode
mode=$(file_mode "$output")
if [[ $mode == 600 ]]; then
pass "$label is mode 0600 under umask $mask"
else
fail "$label mode under umask $mask is $mode, expected 600"
fi
case $(uname -s 2>/dev/null || printf unknown) in
MINGW*|MSYS*|CYGWIN*)
if windows_private_acl "$output"; then
pass "$label has a protected current-user-only DACL under umask $mask"
else
fail "$label lacks a protected current-user-only DACL under umask $mask"
fi
;;
*)
local mode
mode=$(file_mode "$output")
if [[ $mode == 600 ]]; then
pass "$label is mode 0600 under umask $mask"
else
fail "$label mode under umask $mask is $mode, expected 600"
fi
;;
esac
else
fail "$label generation failed under umask $mask"
fi

View file

@ -56,9 +56,30 @@ if "$binary" test --pass-fd not-a-number archive.zupt >/dev/null 2>&1; then
exit 1
fi
if "$binary" test --password-prompt archive.zupt </dev/null >/dev/null 2>&1; then
prompt_log=$test_root/non-interactive-prompt.log
if command -v timeout >/dev/null 2>&1; then
set +e
timeout 10 "$binary" test --password-prompt archive.zupt \
</dev/null >"$prompt_log" 2>&1
prompt_status=$?
set -e
else
set +e
"$binary" test --password-prompt archive.zupt \
</dev/null >"$prompt_log" 2>&1
prompt_status=$?
set -e
fi
if ((prompt_status == 124)); then
printf '%s\n' 'FAIL: non-interactive password prompt timed out' >&2
exit 1
elif ((prompt_status == 0)); then
printf '%s\n' 'FAIL: non-interactive password prompt unexpectedly succeeded' >&2
exit 1
elif ! grep -Fq 'password prompt requires a terminal.' "$prompt_log"; then
printf 'FAIL: non-interactive password prompt returned status %d without a terminal rejection\n' \
"$prompt_status" >&2
exit 1
fi
case $(uname -s) in

View file

@ -248,14 +248,33 @@ else
fail 'backslash separators are normalized within the extraction root'
fi
make_fixture "$TEST_ROOT/legitimate.zupt" 'safe dir/ação.txt'
legitimate_entry_hex=73616665206469722f61c3a7c3a36f2df09f98802e747874
MSYS2_ARG_CONV_EXCL='--entry-hex=' \
"$FIXTURE" "$TEST_ROOT/legitimate.zupt" \
"--entry-hex=$legitimate_entry_hex"
mkdir "$TEST_ROOT/legitimate-out"
if "$ZUPT_BIN" extract -o "$TEST_ROOT/legitimate-out" \
if file_contains_hex_bytes "$TEST_ROOT/legitimate.zupt" \
"$legitimate_entry_hex" &&
"$ZUPT_BIN" list "$TEST_ROOT/legitimate.zupt" \
> "$TEST_ROOT/legitimate-list.log" 2>&1 &&
file_contains_hex_bytes "$TEST_ROOT/legitimate-list.log" \
"$legitimate_entry_hex" &&
"$ZUPT_BIN" extract -o "$TEST_ROOT/legitimate-out" \
"$TEST_ROOT/legitimate.zupt" > "$TEST_ROOT/legitimate.log" 2>&1 &&
[[ $(<"$TEST_ROOT/legitimate-out/safe dir/ação.txt") == 'fixture content' ]]; then
pass 'safe nested UTF-8 path extracts normally'
python3 - "$TEST_ROOT/legitimate-out" "$legitimate_entry_hex" <<'PY'
import pathlib
import sys
# All process arguments are ASCII. Decode the exact UTF-8 archive bytes here
# so the native MinGW fixture's narrow-argv transcoding cannot affect the test.
relative_path = bytes.fromhex(sys.argv[2]).decode("utf-8")
extracted = pathlib.Path(sys.argv[1]).joinpath(*relative_path.split("/"))
raise SystemExit(0 if extracted.read_bytes() == b"fixture content\n" else 1)
PY
then
pass 'safe nested BMP and non-BMP UTF-8 path lists and extracts normally'
else
fail 'safe nested UTF-8 path extracts normally'
fail 'safe nested BMP and non-BMP UTF-8 path lists and extracts normally'
fi
mkdir -p "$TEST_ROOT/relative-root/work"

View file

@ -30,6 +30,7 @@
#define HAVE_SHANI_BUILD 1
#endif
#ifdef HAVE_SHANI_BUILD
static const uint32_t IV[8] = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
@ -45,6 +46,7 @@ static void bad(const char *m) { printf(" \xE2\x9C\x97 %s\n", m); fail++; }
static void hex(const uint8_t *b, int n, char *out) {
for (int i = 0; i < n; i++) sprintf(out + i*2, "%02x", b[i]);
}
#endif
int main(void) {
zupt_detect_cpu(&zupt_cpu);

View file

@ -154,16 +154,19 @@ case "$(uname -s)" in
tree=$(fresh_tree raw-c1-path)
control_name=$'raw-\200.txt'
printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - raw C1 path was not rejected\n'
exit 1
elif ! grep -Fq 'raw-\x80.txt' "$TEST_TMP/output" ||
LC_ALL=C grep -q $'\200' "$TEST_TMP/output"; then
printf 'not ok - raw C1 path was not rendered safely\n'
exit 1
if { printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"; } 2>/dev/null; then
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - raw C1 path was not rejected\n'
exit 1
elif ! grep -Fq 'raw-\x80.txt' "$TEST_TMP/output" ||
LC_ALL=C grep -q $'\200' "$TEST_TMP/output"; then
printf 'not ok - raw C1 path was not rendered safely\n'
exit 1
else
pass 'scanner escapes invalid raw C1 bytes in reported paths'
fi
else
pass 'scanner escapes invalid raw C1 bytes in reported paths'
skip 'raw C1 filenames are forbidden by this filesystem'
fi
tree=$(fresh_tree utf8-c1-path)

View file

@ -187,6 +187,44 @@ else
F "ECHO bit-clear missing the explicit (tcflag_t) cast"
fi
# A restore to a device is irreversible. Classify the already-open descriptor
# rather than checking target_path and resolving that mutable name again.
if grep -Fq 'lstat(target_path' src/zupt_disk.c; then
F "disk restore has a path-check/open TOCTOU pattern"
elif grep -Fq 'tgt_fd = open(target_path' src/zupt_disk.c &&
grep -Fq 'fstat(tgt_fd, &opened_st)' src/zupt_disk.c; then
P "disk restore classifies the opened target descriptor"
else
F "disk restore descriptor-first target guard is missing"
fi
# CodeQL #5 reported chmod(dst, mode) after reopening/resolving the SDK save
# path. Key copies must use the core's handle/descriptor-relative atomic
# publisher and apply POSIX permissions to its already-open temporary stream.
if grep -Fq 'chmod(dst, mode)' sdk/src/zuptsdk.c; then
F "SDK key save has a path-based chmod TOCTOU pattern"
elif grep -Fq 'zupt_atomic_output_open(dst, &fo)' sdk/src/zuptsdk.c &&
grep -Fq 'fchmod(fileno(fo), mode)' sdk/src/zuptsdk.c &&
grep -Fq 'zupt_atomic_output_finish(output, rc == ZUPTSDK_OK)' \
sdk/src/zuptsdk.c; then
P "SDK key save uses descriptor-relative atomic publication"
else
F "SDK key save atomic publication guard is missing"
fi
# The SDK regression must not recreate the same check/use pattern while
# inspecting its sentinels and key modes. Open once, then classify/read via
# that descriptor; this also keeps CodeQL evidence free of test-only races.
if grep -Eq '(^|[^[:alnum:]_])(stat|lstat)[[:space:]]*\(' \
sdk/tests/test_sdk_roundtrip.c; then
F "SDK regression uses path-level stat/lstat before later path operations"
elif grep -Fq 'fstat(fd, &info)' sdk/tests/test_sdk_roundtrip.c &&
grep -Fq 'fstat(fd, info)' sdk/tests/test_sdk_roundtrip.c; then
P "SDK regression inspects already-open file descriptors"
else
F "SDK regression descriptor-based inspection guard is missing"
fi
echo ""
echo " ───────────────────────────────────────"
echo " Static analysis: $PASS passed, $FAIL failed"