From 7821523942a167aa0452b32c3afa139dd3b85fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 18:12:27 -0300 Subject: [PATCH 01/18] release: prepare ZUPT 5.2.6 native gates --- .github/workflows/promote-release.yml | 2 +- AUDIT.md | 46 ++++++++--- CHANGELOG.md | 26 ++++++ DISTRIBUTION.md | 46 ++++++----- INSTALL.md | 23 ++++-- README.md | 105 ++++++++++++++---------- SECURITY.md | 40 ++++++--- THIRD-PARTY-NOTICES.md | 4 +- THREAT_MODEL.md | 24 +++--- doc/zupt-gui.1 | 4 +- doc/zupt.1 | 8 +- gui/README.md | 14 ++-- gui/packaging/deb/control | 4 +- gui/packaging/windows/build-windows.bat | 2 +- include/zupt.h | 3 +- install.sh | 2 +- packaging/aur/PKGBUILD | 6 +- packaging/debian/changelog | 10 +++ packaging/guix/zupt.scm | 4 +- packaging/homebrew/zupt.rb | 6 +- packaging/nix/flake.nix | 4 +- packaging/opensuse/README.md | 33 +++++--- packaging/opensuse/_service | 2 +- packaging/opensuse/zupt.changes | 10 +++ packaging/opensuse/zupt.spec | 2 +- packaging/portable/README.txt | 4 +- packaging/rpm/zupt.spec | 8 +- scripts/check-source-only.sh | 72 +++++++++------- src/vv_encoder.c | 7 +- tests/archive_path_fixture.c | 49 +++++++++-- tests/test_path_traversal.sh | 53 +++++++++--- 31 files changed, 417 insertions(+), 206 deletions(-) diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml index 9104b14..7b7d843 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/promote-release.yml @@ -11,7 +11,7 @@ on: required: true type: number tag: - description: Existing annotated release tag, for example v5.2.5 + description: Existing annotated release tag, for example v5.2.6 required: true type: string diff --git a/AUDIT.md b/AUDIT.md index 8ddb599..70cb378 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,12 +1,12 @@ -# ZUPT 5.2.5 audit guide and finding history +# ZUPT 5.2.6 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.5 scope +## 5.2.6 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.5 baseline retains the source-only boundary introduced in 5.2.2, which +The 5.2.6 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.5 +scripts/check-source-only.sh --tag v5.2.6 # generated source archive -scripts/check-source-only.sh --archive /path/to/zupt-5.2.5.tar.gz +scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz ``` The scanner checks extensions and magic bytes, nested archives, symlink targets, @@ -109,7 +109,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.5 result, or evidence that release assets +not independent certification, a 5.2.6 result, or evidence that release assets were published. | Gate | Result | Recorded evidence | @@ -141,9 +141,33 @@ 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.5. +the skipped native jobs into passes or transfer any result to 5.2.6. -The exact 5.2.5 candidate must repeat the required suite. Native Windows and +## Prior 5.2.5 exact-tag native-gate evidence + +The immutable `v5.2.5` candidate was not promoted. Exact-tag GitHub Actions run +`33434986357` completed 13 jobs successfully, while its native Windows and +macOS jobs failed. The Windows regression did not preserve every requested +hostile path byte across its command-line boundary. The macOS gate exposed both +an unavailable `explicit_bzero` assumption and Bash 3.2 empty-array behavior in +the source scanner exercised by `make check`. + +The 5.2.6 corrections select the existing compiler-resistant volatile wipe on +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. + +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. + +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. @@ -165,7 +189,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.5 test result. +5.2.6 test result. | First corrected | Severity | Finding | Resolution recorded at the time | |---|---|---|---| @@ -206,12 +230,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.5 +and an extracted launcher test. An AppImage is not promoted by the 5.2.6 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.5. Cross-compilation +No Wine result is retained as release evidence for 5.2.6. 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a842cb0..eefcaf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # ZUPT Changelog +## [5.2.6] — 2026-08-31 — Native release-gate portability corrections + +Corrective successor to the immutable, unpromoted `v5.2.5` candidate. Exact-tag +GitHub Actions run `33434986357` completed 13 jobs successfully, but the native +Windows and macOS jobs failed, so no 5.2.5 assets were promoted. The tag and its +recorded evidence remain unchanged. + +- Use the compiler-resistant volatile wipe fallback on macOS and NetBSD instead + of assuming that their C libraries export `explicit_bzero`; supported glibc, + FreeBSD, and OpenBSD paths retain their existing selection. +- Make the source-only scanner's empty-array handling compatible with the + system Bash 3.2 shipped by macOS, including repository, tag, standalone-tree, + standalone-archive, and path-component traversal paths. +- Make hostile archive-path fixtures accept explicit hexadecimal bytes, verify + the requested path bytes in the generated archive, and reject the dangerous + raw byte fragment anywhere in diagnostic output, so the Windows regression + does not depend on command-line conversion or benign path prefixes. +- Carry the 5.2.5 source-only and security baseline forward without changing + the archive format, cryptography, bundled codec release, or SDK ABI. +- Realign current package, workflow, artifact, and tag references to 5.2.6; + leave release-archive checksums explicitly pending until the final source + archive is generated. +- Require fresh exact-`v5.2.6` source, checksum, hosted CI, native-platform, + package, OBS, and promotion evidence. No prior candidate result transfers + automatically, and this changelog entry does not claim those gates passed. + ## [5.2.5] — 2026-08-31 — OBS service working-directory correction Corrective successor to the immutable `v5.2.4` candidate. GitHub Actions run diff --git a/DISTRIBUTION.md b/DISTRIBUTION.md index 7aa4178..851ad90 100644 --- a/DISTRIBUTION.md +++ b/DISTRIBUTION.md @@ -1,4 +1,4 @@ -# Distributing ZUPT 5.2.5 +# Distributing ZUPT 5.2.6 This document describes the packaging material maintained in the ZUPT source repository. A recipe in `packaging/` is not evidence that a package has @@ -14,15 +14,19 @@ 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`, and `v5.2.4` tags are immutable non-promoted candidates. +The `v5.2.2`, `v5.2.3`, `v5.2.4`, and `v5.2.5` 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 by its working directory, and skipped dependent Windows/macOS jobs. A local Tumbleweed reproduction confirmed that `refs/tags/v5.2.4` is valid and that entering the service directory completes the source-service chain. Corrective -packages and release assets must use `v5.2.5`; never move or overwrite an -earlier tag or checksum, and never transfer prior evidence automatically. +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. @@ -54,7 +58,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.5.tar.gz +scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz ``` The scanner reports paths, not file contents, and exits nonzero on a violation. @@ -68,8 +72,8 @@ 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.5.tar.gz dist -sha256sum /tmp/zupt-5.2.5.tar.gz + make DIST_TARBALL=/tmp/zupt-5.2.6.tar.gz dist +sha256sum /tmp/zupt-5.2.6.tar.gz ``` With identical committed input and `SOURCE_DATE_EPOCH`, repeated exports must @@ -115,12 +119,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.5_all.deb` after payload/dependency and installed integration gates | -| GUI RPM | `packaging/build-gui-rpm.sh` | `zupt-gui-5.2.5-1.noarch.rpm` and matching `.src.rpm` after package and installed integration gates | -| Linux CLI archive | `.github/workflows/ci.yml` | `zupt-5.2.5-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.5-portable.zip` after source scan, member allowlist, and extracted off-screen integration 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 | | Fedora / RPM-based systems | `packaging/rpm/zupt.spec` | downstream RPM starting point | -| AppImage helper | `packaging/build-appimage.sh` | downstream-only helper; no 5.2.5 AppImage is promoted | +| AppImage helper | `packaging/build-appimage.sh` | downstream-only helper; no 5.2.6 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 | @@ -196,17 +200,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.5-linux-x86_64.tar.xz` beside README, changelog, security guidance, +`zupt-5.2.6-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.5-portable.zip` artifact is source-only: it contains the GUI +The `zupt-gui-5.2.6-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.5 release gate. +AppImage creation is deliberately offline and is not a 5.2.6 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: @@ -222,7 +226,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.5 workflow. AppDir and Flatpak bundles and GUI platform installers +upstream 5.2.6 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. @@ -237,8 +241,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.5 release evidence. Extended-length/device namespace paths, raw UNC output -roots, and mapped/network-drive output are not supported in 5.2.5. Publish the +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 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. @@ -246,7 +250,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.5 and to the +publishing the immutable tag, update each recipe to version 5.2.6 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 @@ -265,7 +269,7 @@ build. For every published artifact: -1. start from the immutable `v5.2.5` tag; +1. start from the immutable `v5.2.6` 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 @@ -283,7 +287,7 @@ than redirecting consumers to an unverified file. ## Downstream checklist -- [ ] The source URL resolves to the immutable `v5.2.5` tag. +- [ ] The source URL resolves to the immutable `v5.2.6` 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. diff --git a/INSTALL.md b/INSTALL.md index 2199dcf..d3af0a4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,4 +1,4 @@ -# Installing ZUPT 5.2.5 +# Installing ZUPT 5.2.6 This guide covers the ZUPT command-line program and the optional Python GUI. The canonical source repository is @@ -24,15 +24,20 @@ service-harness job failed because its executor did not enter the service directory, and dependent Windows/macOS jobs were skipped. A local Tumbleweed reproduction confirmed both the explicit tag ref and the corrected working-directory contract. This is release/test integration only; the product, -archive format, cryptography, codec, and SDK ABI are unchanged. Do not treat any -prior candidate's artifacts or evidence as 5.2.5 packages or validation. +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 5.2.5 package set eligible for promotion after each target gate succeeds is: +The 5.2.6 package set eligible for promotion after each target gate succeeds is: | Component | Gated artifacts | |---|---| -| CLI | `zupt-5.2.5.tar.gz`, `zupt_5.2.5_amd64.deb`, openSUSE x86_64 binary/source RPMs, `zupt-5.2.5-linux-x86_64.tar.xz`, `zupt-5.2.5-windows-x86_64.zip`, and `ZUPT-5.2.5-macOS-*.dmg` | -| GUI | `zupt-gui_5.2.5_all.deb`, `zupt-gui-5.2.5-1.noarch.rpm`, `zupt-gui-5.2.5-1.src.rpm`, and `zupt-gui-5.2.5-portable.zip` | +| 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` | The GUI packages require the matching `zupt` CLI package and must pass exact payload/dependency checks plus an installed off-screen GUI/CLI integration @@ -40,7 +45,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.5. The Windows ZIP and macOS DMG contain the CLI only. Exact +promoted for 5.2.6. 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. @@ -77,7 +82,7 @@ sudo pacman -S base-devel gzip ``` Package names can differ by distribution release. These commands are examples, -not a statement that 5.2.5 has been accepted into each distribution repository. +not a statement that 5.2.6 has been accepted into each distribution repository. ## Build and test from source @@ -97,7 +102,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.5.tar.gz +scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz ``` The default build provides the native password, ML-KEM-768 + X25519 hybrid diff --git a/README.md b/README.md index 5662fa2..97197f3 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,44 @@ -# ZUPT 5.2.5 +# ZUPT 5.2.6 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.5 corrects the working-directory contract in the exact-tag -openSUSE source-service harness. The immutable `v5.2.4` candidate was not -promoted after GitHub Actions run `33431386002`: 12 jobs succeeded, the sole -openSUSE job failed in the standalone service executor, and its dependent -Windows and macOS jobs were skipped. A local Tumbleweed reproduction confirmed -that `refs/tags/v5.2.4` resolves correctly and that running the executor after -`os.chdir(service_dir)` completes `obs_scm`, `tar`, and `recompress`. This is a +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, codec, or SDK ABI. No v5.2.4 evidence transfers automatically to -v5.2.5. +cryptography, bundled codec, or SDK ABI. No v5.2.5 evidence transfers +automatically to v5.2.6. 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.5 +## Corrective changes in 5.2.6 -The exact-tag openSUSE gate now executes its standalone service chain from the -directory containing `_service`. All current release paths move to 5.2.5 and -require fresh exact-tag hosted CI, package, native-platform, source-only, and -checksum evidence before promotion. The `v5.2.4` tag remains immutable and -unpromoted. +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. + +## Corrective changes introduced in 5.2.5 + +The exact-tag openSUSE gate executes its standalone service chain from the +directory containing `_service`. A local Tumbleweed reproduction confirmed +that `refs/tags/v5.2.4` resolves correctly and that entering the service +directory completes `obs_scm`, `tar`, and `recompress`. The immutable v5.2.4 +candidate recorded 12 successful jobs in run `33431386002`; its openSUSE job +failed before the correction and dependent Windows/macOS jobs were skipped. ## Corrective changes introduced in 5.2.4 @@ -134,9 +145,9 @@ 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.5 release artifacts +## 5.2.6 release artifacts -The 5.2.5 release workflow is defined to produce the following files only after +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 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 @@ -145,23 +156,23 @@ skips. This table is not a substitute for that evidence. | Format | Intended target and validation boundary | | --- | --- | -| `zupt-5.2.5.tar.gz` | Reproducible, source-only archive; scanned twice-built input plus SHA-256. | -| `zupt_5.2.5_amd64.deb` | Ubuntu 24.04 amd64 package; install, functional round trip, and uninstall gate. | -| `zupt-5.2.5-*.x86_64.rpm` and `.src.rpm` | openSUSE Tumbleweed x86_64 source/binary RPM gate; package inspection, install, round trip, and uninstall. | -| `zupt-5.2.5-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.5_all.deb` | Architecture-independent Python/Qt GUI package; exact dependency/payload checks plus installed off-screen GUI/CLI integration gate. | -| `zupt-gui-5.2.5-1.noarch.rpm` | Architecture-independent Python/Qt GUI RPM; package inspection plus installed off-screen GUI/CLI integration gate. | -| `zupt-gui-5.2.5-1.src.rpm` | Source RPM corresponding exactly to the gated noarch GUI RPM. | -| `zupt-gui-5.2.5-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.5-windows-x86_64.zip` | Native Windows x86_64 executable with notices; extracted-ZIP round-trip gate. | -| `ZUPT-5.2.5-macOS-*.dmg` | Native macOS image; mounted packaged executable round-trip gate, with the actual architecture in the filename. | +| `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. | 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.5. The inspected upstream type-2 runtime lacked +No AppImage is promised for 5.2.6. 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 @@ -191,8 +202,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.5 -bash scripts/check-source-only.sh --archive /path/to/zupt-5.2.5.tar.gz +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 ~~~ Unknown `.bin` files fail the scan. A necessary binary data fixture may be @@ -328,7 +339,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.5 results. Post-tag CI integration failures prevented +certification and not 5.2.6 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 @@ -336,14 +347,18 @@ 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.5 evidence. The exact 5.2.5 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` +nor the successful v5.2.4 jobs are v5.2.6 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`. -On Windows, 5.2.5 scopes output handling to normal local Win32 paths. A MinGW +On Windows, 5.2.6 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 @@ -365,7 +380,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.5 archive. Compression and disk backup never create a +a newly created 5.2.6 archive. Compression and disk backup never create a no-AIT archive. `info` is deliberately different: it reports unauthenticated framing metadata, @@ -383,7 +398,7 @@ 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.5 are maintained in the +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 @@ -395,9 +410,9 @@ Generate the reproducible source archive outside the repository: ~~~sh make dist -sha256sum /tmp/zupt-5.2.5.tar.gz +sha256sum /tmp/zupt-5.2.6.tar.gz bash scripts/check-source-only.sh \ - --archive /tmp/zupt-5.2.5.tar.gz + --archive /tmp/zupt-5.2.6.tar.gz ~~~ Archive ordering, ownership and timestamps are normalized. The default epoch is @@ -413,7 +428,7 @@ final digest before the tag is published. ## openSUSE and OBS The maintained upstream recipe is in packaging/opensuse. It is prepared for an -immutable v5.2.5 tag, disables submodules and Git LFS, builds with +immutable v5.2.6 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. @@ -456,7 +471,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.5 artifact promise +target-native audit of every historical GUI format. The 5.2.6 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. @@ -466,13 +481,13 @@ 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 work. +5.2.3/5.2.4/5.2.5/5.2.6 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, or 5.2.5 changes. +authorship or of the upstream 5.2.2, 5.2.3, 5.2.4, 5.2.5, or 5.2.6 changes. ## License diff --git a/SECURITY.md b/SECURITY.md index 3c7cc16..ffa42ff 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,4 @@ -# Security Policy — ZUPT 5.2.5 +# Security Policy — ZUPT 5.2.6 ## Reporting vulnerabilities @@ -66,7 +66,7 @@ partially accepted. ### Optional integrations -The 5.2.5 default is `WITH_SDK=0 WITH_PQBOX=0`: +The 5.2.6 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. @@ -133,12 +133,19 @@ can compromise archives encrypted to it. ## Constant-time and side-channel scope -Portable C is the 5.2.5 default. Sensitive comparisons and selections use +Portable C is the 5.2.6 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 claim of cache-timing resistance on hostile shared hardware. +Sensitive VaptVupt working buffers are cleared through a compiler-resistant +wipe helper. Platforms with a guaranteed libc `explicit_bzero` use it; macOS +and NetBSD use the portable volatile-write fallback because the supported +deployment targets do not guarantee that symbol. This source-level choice +resists ordinary dead-store elimination but is not a formal claim about every +compiler binary. + Textual assembly under `jasmin/` can be enabled explicitly with `WITH_JASMIN=1` on a supported x86_64 compiler target. The directory contains Jasmin-generated output and separately identified hand-written assembly; all of @@ -197,7 +204,7 @@ media before proceeding. 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.5. Cross-build and +and mapped/network-drive output are not supported in 5.2.6. 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 @@ -240,7 +247,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.5.tar.gz +scripts/check-source-only.sh --archive /path/to/zupt-5.2.6.tar.gz ``` Nested archive inspection is required to enforce bounded recursion, member @@ -250,13 +257,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.5. A bare Linux or Windows executable is also +AppImage is not promoted for 5.2.6. 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.5 set is the CLI package/archive set plus the exact GUI DEB, +The gated 5.2.6 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, @@ -296,7 +303,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.5. The immutable 5.2.3 candidate was not promoted because its source-policy +5.2.6. 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 @@ -306,11 +313,18 @@ 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.5. The exact 5.2.5 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`. +not transfer automatically to 5.2.6. 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 +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`. Run target-native static analyzers and package checks as additional evidence. Do not infer x86_64, aarch64, ppc64le, s390x, riscv64, macOS, Windows, Leap, or diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 97321f0..31529e1 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -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.5 release asset. The upstream +No AppImage is a promised or promoted 5.2.6 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.5 upstream release gates. +not covered by the 5.2.6 upstream release gates. ## Reporting attribution issues diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 4e7c222..c355cdd 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,4 +1,4 @@ -# ZUPT 5.2.5 threat model +# ZUPT 5.2.6 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.5 source with: +The upstream baseline is built from the 5.2.6 source with: ```sh make WITH_SDK=0 WITH_PQBOX=0 @@ -175,7 +175,7 @@ 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.5 covers normal local Win32 paths. +The Windows handle-relative boundary in 5.2.6 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 @@ -250,9 +250,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.5; bare Linux and Windows executables are also excluded. +promoted for 5.2.6; bare Linux and Windows executables are also excluded. -For 5.2.5, that gated artifact scope covers the CLI files plus the exact GUI +For 5.2.6, 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 +265,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.5 evidence. The immutable 5.2.3 candidate was not +certification and is not 5.2.6 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,9 +275,13 @@ 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.5 evidence. Hosted GitHub CI and release promotion, native +automatic 5.2.6 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.5 candidate +rpmlint `no-binary` finding remain pending until an exact 5.2.6 candidate records them. ## Historical compatibility notes @@ -305,7 +309,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.5 results; current outcomes belong in the release +automatically become 5.2.6 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. @@ -316,4 +320,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.5, 2026-08-31. +Document version: 5.2.6, 2026-08-31. diff --git a/doc/zupt-gui.1 b/doc/zupt-gui.1 index 29dcbd4..bd132dd 100644 --- a/doc/zupt-gui.1 +++ b/doc/zupt-gui.1 @@ -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.5" "User Commands" +.TH ZUPT-GUI 1 "2026-08-31" "ZUPT 5.2.6" "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.5 GUI release set is limited to the architecture-independent DEB, +The gated 5.2.6 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 diff --git a/doc/zupt.1 b/doc/zupt.1 index df66131..94a8670 100644 --- a/doc/zupt.1 +++ b/doc/zupt.1 @@ -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.5" "User Commands" +.TH ZUPT 1 "2026-08-31" "ZUPT 5.2.6" "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.5; neither are AppDir/Flatpak bundles, GUI platform installers, or bare +for 5.2.6; 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.5. Cross-compilation and Wine results are not native +are not supported in 5.2.6. 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.5 candidate must repeat the gate. It does not imply +byte-exact. The exact 5.2.6 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. diff --git a/gui/README.md b/gui/README.md index 9c7a139..4177574 100644 --- a/gui/README.md +++ b/gui/README.md @@ -1,6 +1,6 @@ # ZUPT GUI -The ZUPT GUI is a Python/Qt front end for the ZUPT 5.2.5 command-line +The ZUPT GUI is a Python/Qt front end for the ZUPT 5.2.6 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.5_all.deb`; -- `zupt-gui-5.2.5-1.noarch.rpm`; -- `zupt-gui-5.2.5-1.src.rpm`; -- `zupt-gui-5.2.5-portable.zip`. +- `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`. 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.5 release gates. +not promoted by the upstream 5.2.6 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.5 release asset. The promoted Windows +not make the untested GUI installer a 5.2.6 release asset. The promoted Windows ZIP and macOS DMG are CLI-only. Packaging recipes and scripts under `gui/packaging/` and `packaging/` are build diff --git a/gui/packaging/deb/control b/gui/packaging/deb/control index be5b85b..3c50f66 100644 --- a/gui/packaging/deb/control +++ b/gui/packaging/deb/control @@ -1,9 +1,9 @@ Package: zupt-gui -Version: 5.2.5 +Version: 5.2.6 Section: utils Priority: optional Architecture: all -Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= 5.2.5) +Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= 5.2.6) Maintainer: Cristian Cezar Moisés Homepage: https://github.com/cristiancmoises/zupt Description: Qt graphical interface for the ZUPT backup utility diff --git a/gui/packaging/windows/build-windows.bat b/gui/packaging/windows/build-windows.bat index 53e3f9b..8d8eec9 100644 --- a/gui/packaging/windows/build-windows.bat +++ b/gui/packaging/windows/build-windows.bat @@ -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.5" +if not defined VERSION set "VERSION=5.2.6" 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%" diff --git a/include/zupt.h b/include/zupt.h index d844746..f16e441 100644 --- a/include/zupt.h +++ b/include/zupt.h @@ -120,7 +120,8 @@ static inline int zupt_win_mkdir_utf8(const char *path) { /* v5.2.3 corrects release packaging and CI; archive format remains v1.6. */ /* 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. */ -#define ZUPT_VERSION_STRING "5.2.5" +/* v5.2.6 corrects native release-gate portability; format remains v1.6. */ +#define ZUPT_VERSION_STRING "5.2.6" /* 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" diff --git a/install.sh b/install.sh index e56044c..717562e 100644 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ set -Eeuo pipefail umask 077 -VERSION=${VERSION:-5.2.5} +VERSION=${VERSION:-5.2.6} PREFIX=${PREFIX:-/usr/local} echo "🔧 Installing ZUPT..." diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 73ef256..fdc8189 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -11,7 +11,7 @@ # Test locally with `makepkg -s` after the release archive is published. pkgname=zupt -pkgver=5.2.5 +pkgver=5.2.6 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.5 source archive. -sha256sums=('7b86d34a418a2bce24396610bd7c23705f58d27873e610728083bd57ebb69ba4') +# Byte-reproducible upstream v5.2.6 source archive. Pin after final generation. +sha256sums=('REPLACE_AFTER_FINAL_ARCHIVE_SHA256') build() { cd "${pkgname}-${pkgver}" diff --git a/packaging/debian/changelog b/packaging/debian/changelog index 21c900a..0376ac0 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,3 +1,13 @@ +zupt (5.2.6-1) UNRELEASED; urgency=medium + + * Use the compiler-resistant volatile wipe fallback on macOS and NetBSD, and + make the source scanner's empty-array handling compatible with Bash 3.2. + * Preserve hostile archive-path fixture bytes exactly on Windows. + * Preserve the immutable, unpromoted 5.2.5 history and require fresh 5.2.6 + package, checksum, native-platform, OBS, and promotion gates. + + -- Cristian Cezar Moisés Mon, 31 Aug 2026 21:30:00 +0000 + zupt (5.2.5-1) UNRELEASED; urgency=medium * Run the standalone OBS source-service chain from its isolated working diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index 042a2e0..5d5af79 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -62,7 +62,7 @@ xcb-util-renderutil xcb-util-wm xcb-util-cursor libinput-minimal mtdev libevdev eudev)) -(define %zupt-version "5.2.5") +(define %zupt-version "5.2.6") (define %zupt-source (origin @@ -72,7 +72,7 @@ "/releases/download/v" %zupt-version "/zupt-" %zupt-version ".tar.gz")) (sha256 - (base32 "194vnvmmggc3h1r11rkkg395hpvh4dybs43674jcwawa855d71kv")))) + (base32 "REPLACE_AFTER_FINAL_ARCHIVE_GUIX_BASE32")))) (define-public zupt (package diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index 609f1c1..d147aed 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -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.5/zupt-5.2.5.tar.gz" - version "5.2.5" - sha256 "7b86d34a418a2bce24396610bd7c23705f58d27873e610728083bd57ebb69ba4" + url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.6/zupt-5.2.6.tar.gz" + version "5.2.6" + sha256 "REPLACE_AFTER_FINAL_ARCHIVE_SHA256" 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 diff --git a/packaging/nix/flake.nix b/packaging/nix/flake.nix index 8b283ad..6caf616 100644 --- a/packaging/nix/flake.nix +++ b/packaging/nix/flake.nix @@ -9,7 +9,7 @@ # nix flake check # lint the flake # # To consume from another flake: -# inputs.zupt.url = "github:cristiancmoises/zupt/v5.2.5"; +# inputs.zupt.url = "github:cristiancmoises/zupt/v5.2.6"; # ...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.5"; + version = "5.2.6"; # When publishing, replace this with `fetchurl` against the # release tarball. For local development the flake assumes it diff --git a/packaging/opensuse/README.md b/packaging/opensuse/README.md index 8123840..09ed96e 100644 --- a/packaging/opensuse/README.md +++ b/packaging/opensuse/README.md @@ -1,4 +1,4 @@ -# ZUPT 5.2.5 for openSUSE Build Service +# ZUPT 5.2.6 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 upstream changes to +attribute upstream code or the 5.2.2/5.2.3/5.2.4/5.2.5/5.2.6 upstream changes to Cabelo. ## Files and source policy | File | Purpose | |---|---| -| `_service` | Fetch the immutable `v5.2.5` tag and create `Source0` at build time. | +| `_service` | Fetch the immutable `v5.2.6` 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.5.tar.gz` inside the build environment, which +services reconstruct `zupt-5.2.6.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.5 gates may publish the CLI source tarball, DEB, binary RPM, +The upstream 5.2.6 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.5.tar.gz` next to the spec and use a disposable RPM build tree: +`zupt-5.2.6.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.5, native Windows or macOS success, hosted +local results do not establish 5.2.6, 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 @@ -194,10 +194,21 @@ 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.5. The exact v5.2.5 candidate must repeat every applicable +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. +## Prior 5.2.5 exact-tag native-gate evidence + +The immutable v5.2.5 candidate was not promoted. Exact-tag GitHub Actions run +`33434986357` completed 13 jobs successfully and failed the native Windows and +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. + ## Prior openSUSE packaging validation The local results below were produced on 2026-08-24 from the 5.2.2 candidate @@ -235,11 +246,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.5` tag. Exact-tag hosted, + then creates and verifies the annotated `v5.2.6` 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.5`. Verify the reported ZIP and + `scripts/export-opensuse-package.sh v5.2.6`. 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. @@ -251,7 +262,7 @@ gate. ``` 4. From the extracted handoff root, run - `packaging/opensuse/source-audit.sh --archive /path/to/zupt-5.2.5.tar.gz`. + `packaging/opensuse/source-audit.sh --archive /path/to/zupt-5.2.6.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. diff --git a/packaging/opensuse/_service b/packaging/opensuse/_service index 8f1be66..82a8624 100644 --- a/packaging/opensuse/_service +++ b/packaging/opensuse/_service @@ -4,7 +4,7 @@ https://github.com/cristiancmoises/zupt.git git - refs/tags/v5.2.5 + refs/tags/v5.2.6 @PARENT_TAG@ ^v(.*)$ \1 diff --git a/packaging/opensuse/zupt.changes b/packaging/opensuse/zupt.changes index 33acb3b..228b91c 100644 --- a/packaging/opensuse/zupt.changes +++ b/packaging/opensuse/zupt.changes @@ -1,3 +1,13 @@ +------------------------------------------------------------------- +Mon Aug 31 21:30:00 UTC 2026 - Cristian Cezar Moisés + +- Update to 5.2.6: + * Use the compiler-resistant volatile wipe fallback on macOS and NetBSD. + * Make source-scanner empty-array handling compatible with Bash 3.2. + * Preserve hostile archive-path fixture bytes exactly on Windows. + * Preserve immutable, unpromoted 5.2.5 history and require fresh 5.2.6 gates. + * Pin the OBS source service to the immutable v5.2.6 tag. + ------------------------------------------------------------------- Mon Aug 31 19:55:00 UTC 2026 - Cristian Cezar Moisés diff --git a/packaging/opensuse/zupt.spec b/packaging/opensuse/zupt.spec index 770eeaa..aea660a 100644 --- a/packaging/opensuse/zupt.spec +++ b/packaging/opensuse/zupt.spec @@ -18,7 +18,7 @@ # Name: zupt -Version: 5.2.5 +Version: 5.2.6 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 diff --git a/packaging/portable/README.txt b/packaging/portable/README.txt index 925622f..2f02f53 100644 --- a/packaging/portable/README.txt +++ b/packaging/portable/README.txt @@ -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.5, installed as `zupt` on PATH or placed beside the launcher + 3. ZUPT 5.2.6, 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.5 or place its command beside + * "zupt not found": install ZUPT 5.2.6 or place its command beside the launcher. * Set ZUPT_DEBUG=1 to print command-discovery diagnostics to stderr. diff --git a/packaging/rpm/zupt.spec b/packaging/rpm/zupt.spec index 394abb5..10f1a82 100644 --- a/packaging/rpm/zupt.spec +++ b/packaging/rpm/zupt.spec @@ -20,7 +20,7 @@ # installed smoke test. Name: zupt -Version: 5.2.5 +Version: 5.2.6 Release: 1%{?dist} Summary: Backup compression with authenticated and post-quantum encryption @@ -101,6 +101,12 @@ comments. Plain archives use non-cryptographic checksums. %endif %changelog +* Mon Aug 31 2026 Cristian Cezar Moisés - 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 + preserve hostile archive-path fixture bytes exactly on Windows. +- Preserve immutable, unpromoted 5.2.5 history and require fresh 5.2.6 gates. + * Mon Aug 31 2026 Cristian Cezar Moisés - 5.2.5-1 - Run the standalone OBS source-service chain from its isolated working directory and add a packaging-policy regression for that contract. diff --git a/scripts/check-source-only.sh b/scripts/check-source-only.sh index 5d5e2f3..723cb4c 100755 --- a/scripts/check-source-only.sh +++ b/scripts/check-source-only.sh @@ -13,6 +13,9 @@ HAVE_EXTERNAL_TARGET=0 declare -a TAGS=() declare -a ARCHIVES=() declare -a TREES=() +TAG_COUNT=0 +ARCHIVE_COUNT=0 +TREE_COUNT=0 FAILURES=0 SCANNED=0 @@ -92,17 +95,20 @@ while (($#)); do --tag) (($# >= 2)) || { printf 'ERROR: --tag requires a revision\n' >&2; exit 2; } TAGS+=("$2") + TAG_COUNT=$((TAG_COUNT + 1)) shift 2 ;; --archive) (($# >= 2)) || { printf 'ERROR: --archive requires a file\n' >&2; exit 2; } ARCHIVES+=("$2") + ARCHIVE_COUNT=$((ARCHIVE_COUNT + 1)) HAVE_EXTERNAL_TARGET=1 shift 2 ;; --tree) (($# >= 2)) || { printf 'ERROR: --tree requires a directory\n' >&2; exit 2; } TREES+=("$2") + TREE_COUNT=$((TREE_COUNT + 1)) HAVE_EXTERNAL_TARGET=1 shift 2 ;; @@ -127,7 +133,7 @@ while (($#)); do esac done -if ((HAVE_EXTERNAL_TARGET)) && ((ROOT_REQUESTED == 0)) && ((${#TAGS[@]} == 0)); then +if ((HAVE_EXTERNAL_TARGET)) && ((ROOT_REQUESTED == 0)) && ((TAG_COUNT == 0)); then REPOSITORY_AUDIT=0 fi @@ -268,7 +274,9 @@ path_stays_below_root() { [[ $candidate != /* && $candidate != //* ]] || return 1 [[ ! $candidate =~ ^[[:alpha:]]: ]] || return 1 read -r -a components <<< "$candidate" - for component in "${components[@]}"; do + # Bash 3.2 treats an empty array expansion as unset under `set -u`. + # The + guard expands to no words for an empty path component list. + for component in ${components[@]+"${components[@]}"}; do case $component in ''|.) ;; ..) @@ -821,38 +829,44 @@ if ((REPOSITORY_AUDIT)); then else fail_path git-archive-HEAD HEAD 'repository has no commit' fi - for target in "${TAGS[@]}"; do - if git -C "$ROOT" rev-parse --verify -q "$target^{commit}" >/dev/null; then - scan_git_archive "$ROOT" "$target" "git-archive-$target" + if ((TAG_COUNT)); then + for target in "${TAGS[@]}"; do + if git -C "$ROOT" rev-parse --verify -q "$target^{commit}" >/dev/null; then + scan_git_archive "$ROOT" "$target" "git-archive-$target" + else + fail_path git-tag "$target" 'revision does not resolve to a commit' + fi + done + fi +fi + +if ((TREE_COUNT)); then + for target in "${TREES[@]}"; do + if [[ -d $target ]]; then + target=$(canonicalize_allow_missing "$target") || { + fail_path standalone-tree "$target" 'cannot canonicalize tree' + continue + } + scan_tree "$target" standalone-tree 0 else - fail_path git-tag "$target" 'revision does not resolve to a commit' + fail_path standalone-tree "$target" 'tree does not exist' fi done fi -for target in "${TREES[@]}"; do - if [[ -d $target ]]; then - target=$(canonicalize_allow_missing "$target") || { - fail_path standalone-tree "$target" 'cannot canonicalize tree' - continue - } - scan_tree "$target" standalone-tree 0 - else - fail_path standalone-tree "$target" 'tree does not exist' - fi -done - -for target in "${ARCHIVES[@]}"; do - if [[ -f $target ]]; then - target=$(canonicalize_allow_missing "$target") || { - fail_path standalone-archive "$target" 'cannot canonicalize archive' - continue - } - scan_archive "$target" "${target##*/}" standalone-archive 0 - else - fail_path standalone-archive "$target" 'archive does not exist' - fi -done +if ((ARCHIVE_COUNT)); then + for target in "${ARCHIVES[@]}"; do + if [[ -f $target ]]; then + target=$(canonicalize_allow_missing "$target") || { + fail_path standalone-archive "$target" 'cannot canonicalize archive' + continue + } + scan_archive "$target" "${target##*/}" standalone-archive 0 + else + fail_path standalone-archive "$target" 'archive does not exist' + fi + done +fi if ((FAILURES == 0)); then printf 'PASS source-only: %d files, %d archives\n' "$SCANNED" "$ARCHIVES_SCANNED" diff --git a/src/vv_encoder.c b/src/vv_encoder.c index 06d9bda..358d081 100644 --- a/src/vv_encoder.c +++ b/src/vv_encoder.c @@ -41,14 +41,15 @@ * * Implementation strategy: * - Prefer `explicit_bzero` (BSD/glibc 2.25+, guaranteed-secure) - * - Fall back to `memset_explicit` (C23) - * - Last resort: volatile-pointer memset (compiler cannot + * - Otherwise use a volatile-pointer loop (compiler cannot * prove the writes are dead) * ═══════════════════════════════════════════════════════════════ */ #if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 25)) # define VV_HAS_EXPLICIT_BZERO 1 -#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +/* Darwin intentionally uses the volatile fallback: current deployment targets + * do not guarantee an explicit_bzero symbol in libSystem. */ +#elif defined(__FreeBSD__) || defined(__OpenBSD__) # define VV_HAS_EXPLICIT_BZERO 1 #else # define VV_HAS_EXPLICIT_BZERO 0 diff --git a/tests/archive_path_fixture.c b/tests/archive_path_fixture.c index 05aa54e..e298df0 100644 --- a/tests/archive_path_fixture.c +++ b/tests/archive_path_fixture.c @@ -42,6 +42,31 @@ static size_t put_varint(uint8_t *out, uint64_t value) { return count; } +static int hex_nibble(unsigned char value) { + if (value >= '0' && value <= '9') return (int)(value - '0'); + if (value >= 'a' && value <= 'f') return (int)(value - 'a') + 10; + if (value >= 'A' && value <= 'F') return (int)(value - 'A') + 10; + return -1; +} + +static int decode_hex_entry(const char *hex, uint8_t *out, size_t capacity, + size_t *out_size) { + size_t hex_size = strlen(hex); + if (hex_size == 0 || (hex_size & 1u) != 0 || + hex_size / 2u >= capacity) + return -1; + + size_t decoded_size = hex_size / 2u; + for (size_t i = 0; i < decoded_size; i++) { + int high = hex_nibble((unsigned char)hex[i * 2u]); + int low = hex_nibble((unsigned char)hex[i * 2u + 1u]); + if (high < 0 || low < 0) return -1; + out[i] = (uint8_t)((high << 4) | low); + } + *out_size = decoded_size; + return 0; +} + static int write_block(FILE *stream, uint8_t type, const uint8_t *payload, size_t payload_size, uint64_t unpacked_size, uint64_t checksum) { @@ -66,11 +91,24 @@ static int write_block(FILE *stream, uint8_t type, const uint8_t *payload, int main(int argc, char **argv) { static const uint8_t content[] = "fixture content\n"; - const char *entry = argc == 3 && strncmp(argv[2], "--entry=", 8) == 0 - ? argv[2] + 8 : NULL; - if (!entry || argv[1][0] == '\0' || entry[0] == '\0' || - strlen(entry) >= ZUPT_MAX_PATH) { - fprintf(stderr, "usage: %s ARCHIVE --entry=ENTRY_PATH\n", argv[0]); + uint8_t decoded_entry[ZUPT_MAX_PATH]; + const uint8_t *entry = NULL; + size_t path_size = 0; + + if (argc == 3 && strncmp(argv[2], "--entry=", 8) == 0) { + entry = (const uint8_t *)argv[2] + 8; + path_size = strlen(argv[2] + 8); + } else if (argc == 3 && + strncmp(argv[2], "--entry-hex=", 12) == 0 && + decode_hex_entry(argv[2] + 12, decoded_entry, + sizeof(decoded_entry), &path_size) == 0) { + entry = decoded_entry; + } + if (!entry || argv[1][0] == '\0' || path_size == 0 || + path_size >= ZUPT_MAX_PATH) { + fprintf(stderr, + "usage: %s ARCHIVE --entry=ENTRY_PATH|--entry-hex=HEX_BYTES\n", + argv[0]); return 2; } @@ -106,7 +144,6 @@ int main(int argc, char **argv) { uint8_t index[ZUPT_MAX_PATH + 128]; size_t index_size = 0; - size_t path_size = strlen(entry); index_size += put_varint(index + index_size, 1); index_size += put_varint(index + index_size, path_size); memcpy(index + index_size, entry, path_size); diff --git a/tests/test_path_traversal.sh b/tests/test_path_traversal.sh index 4eedac8..195b54d 100755 --- a/tests/test_path_traversal.sh +++ b/tests/test_path_traversal.sh @@ -104,33 +104,62 @@ else fail 'control-byte archive path is rejected without terminal injection' fi -expect_display_unsafe_path_rejected() { - local label=$1 name=$2 entry=$3 +file_contains_hex_bytes() { + python3 - "$1" "$2" <<'PY' +import pathlib +import sys + +data = pathlib.Path(sys.argv[1]).read_bytes() +needle = bytes.fromhex(sys.argv[2]) +raise SystemExit(0 if needle in data else 1) +PY +} + +expect_display_unsafe_hex_path_rejected() { + local label=$1 name=$2 entry_hex=$3 forbidden_hex=$4 local archive=$TEST_ROOT/$name.zupt log=$TEST_ROOT/$name.log status - MSYS2_ARG_CONV_EXCL='--entry=' "$FIXTURE" "$archive" "--entry=$entry" + MSYS2_ARG_CONV_EXCL='--entry-hex=' \ + "$FIXTURE" "$archive" "--entry-hex=$entry_hex" + if ! file_contains_hex_bytes "$archive" "$entry_hex"; then + printf ' fixture did not preserve the requested path bytes: %s\n' \ + "$entry_hex" >&2 + fail "$label" + return + fi set +e "$ZUPT_BIN" list "$archive" > "$log" 2>&1 status=$? set -e - if ((status != 0)) && ! LC_ALL=C grep -Fq -- "$entry" "$log"; then + if ((status != 0)) && ! file_contains_hex_bytes "$log" "$forbidden_hex"; then pass "$label" else fail "$label" fi } -expect_display_unsafe_path_rejected \ +if MSYS2_ARG_CONV_EXCL='--entry-hex=' \ + "$FIXTURE" "$TEST_ROOT/invalid-hex.zupt" '--entry-hex=0' \ + >/dev/null 2>&1 || + MSYS2_ARG_CONV_EXCL='--entry-hex=' \ + "$FIXTURE" "$TEST_ROOT/invalid-hex.zupt" '--entry-hex=GG' \ + >/dev/null 2>&1; then + fail 'archive path fixture rejects malformed hex input' +else + pass 'archive path fixture rejects malformed hex input' +fi + +expect_display_unsafe_hex_path_rejected \ 'raw C1 archive path is rejected without terminal injection' \ - raw-c1 $'safe\23331m.txt' -expect_display_unsafe_path_rejected \ + raw-c1 736166659b33316d2e747874 9b +expect_display_unsafe_hex_path_rejected \ 'UTF-8 C1 archive path is rejected without terminal injection' \ - utf8-c1 $'safe\302\23331m.txt' -expect_display_unsafe_path_rejected \ + utf8-c1 73616665c29b33316d2e747874 c29b +expect_display_unsafe_hex_path_rejected \ 'Unicode bidi-control archive path is rejected without display spoofing' \ - bidi $'safe\342\200\256exe.txt' -expect_display_unsafe_path_rejected \ + bidi 73616665e280ae6578652e747874 e280ae +expect_display_unsafe_hex_path_rejected \ 'invalid UTF-8 archive path is rejected without raw display' \ - invalid-utf8 $'safe\300\257.txt' + invalid-utf8 73616665c0af2e747874 c0af make_fixture "$TEST_ROOT/leaf.zupt" 'innocent.txt' printf '%s\n' DO_NOT_OVERWRITE > "$TEST_ROOT/sentinel" From 6d9f740a2bc55bd539bc19facca2ac77767d2379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 18:13:54 -0300 Subject: [PATCH 02/18] packaging: pin 5.2.6 source archive checksums [skip ci] --- packaging/aur/PKGBUILD | 4 ++-- packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index fdc8189..f88ecb6 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -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. Pin after final generation. -sha256sums=('REPLACE_AFTER_FINAL_ARCHIVE_SHA256') +# Byte-reproducible upstream v5.2.6 source archive. +sha256sums=('e64bd92aac1b50018efd3c8b678c70771320a2069d47f32c03349c569100b27c') build() { cd "${pkgname}-${pkgver}" diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index 5d5af79..5a68396 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -72,7 +72,7 @@ "/releases/download/v" %zupt-version "/zupt-" %zupt-version ".tar.gz")) (sha256 - (base32 "REPLACE_AFTER_FINAL_ARCHIVE_GUIX_BASE32")))) + (base32 "0z5j028md71l0cng6iwx0si204vpf266g2rwzn702l0vmhmdjjz6")))) (define-public zupt (package diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index d147aed..11c34bf 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -24,7 +24,7 @@ class Zupt < Formula 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 "REPLACE_AFTER_FINAL_ARCHIVE_SHA256" + sha256 "e64bd92aac1b50018efd3c8b678c70771320a2069d47f32c03349c569100b27c" 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 From cb794e100cfceb0d6f9f4930dca7d5139014d196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 19:05:55 -0300 Subject: [PATCH 03/18] release: prepare ZUPT 5.2.7 native test gates --- .github/workflows/promote-release.yml | 2 +- AUDIT.md | 44 ++++++---- CHANGELOG.md | 22 +++++ DISTRIBUTION.md | 47 ++++++----- INSTALL.md | 24 +++--- README.md | 104 +++++++++++++----------- SECURITY.md | 30 ++++--- THIRD-PARTY-NOTICES.md | 4 +- THREAT_MODEL.md | 26 +++--- doc/zupt-gui.1 | 4 +- doc/zupt.1 | 8 +- gui/README.md | 14 ++-- gui/packaging/deb/control | 4 +- gui/packaging/windows/build-windows.bat | 2 +- include/zupt.h | 3 +- install.sh | 2 +- packaging/aur/PKGBUILD | 4 +- packaging/debian/changelog | 10 +++ packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 4 +- packaging/nix/flake.nix | 4 +- packaging/opensuse/README.md | 36 +++++--- packaging/opensuse/_service | 2 +- packaging/opensuse/zupt.changes | 9 ++ packaging/opensuse/zupt.spec | 2 +- packaging/portable/README.txt | 4 +- packaging/rpm/zupt.spec | 7 +- tests/test_path_traversal.sh | 21 ++++- tests/test_sha256_shani.c | 2 + 29 files changed, 278 insertions(+), 169 deletions(-) diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml index 7b7d843..d05b427 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/promote-release.yml @@ -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.7 required: true type: string diff --git a/AUDIT.md b/AUDIT.md index 70cb378..62eaa08 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,12 +1,12 @@ -# ZUPT 5.2.6 audit guide and finding history +# ZUPT 5.2.7 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.7 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.7 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.7 # 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.7.tar.gz ``` The scanner checks extensions and magic bytes, nested archives, symlink targets, @@ -109,7 +109,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.7 result, or evidence that release assets were published. | Gate | Result | Recorded evidence | @@ -141,7 +141,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.7. ## Prior 5.2.5 exact-tag native-gate evidence @@ -157,17 +157,31 @@ 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.7 hosted CI, package, native-platform, or promotion evidence. -The exact 5.2.6 candidate must repeat the required suite. Native Windows and +## 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. They are not proof +that any v5.2.7 native or hosted gate has passed. + +The exact 5.2.7 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. @@ -189,7 +203,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.7 test result. | First corrected | Severity | Finding | Resolution recorded at the time | |---|---|---|---| @@ -230,12 +244,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.7 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.7. 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index eefcaf5..e28a59f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # ZUPT Changelog +## [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 diff --git a/DISTRIBUTION.md b/DISTRIBUTION.md index 851ad90..13dd44e 100644 --- a/DISTRIBUTION.md +++ b/DISTRIBUTION.md @@ -1,4 +1,4 @@ -# Distributing ZUPT 5.2.6 +# Distributing ZUPT 5.2.7 This document describes the packaging material maintained in the ZUPT source repository. A recipe in `packaging/` is not evidence that a package has @@ -14,7 +14,7 @@ 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 +The `v5.2.2`, `v5.2.3`, `v5.2.4`, `v5.2.5`, and `v5.2.6` 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 @@ -24,9 +24,12 @@ 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. +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. Corrective packages and release assets must use +`v5.2.7`; 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. @@ -58,7 +61,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.7.tar.gz ``` The scanner reports paths, not file contents, and exits nonzero on a violation. @@ -72,8 +75,8 @@ 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.7.tar.gz dist +sha256sum /tmp/zupt-5.2.7.tar.gz ``` With identical committed input and `SOURCE_DATE_EPOCH`, repeated exports must @@ -119,12 +122,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.7_all.deb` after payload/dependency and installed integration gates | +| GUI RPM | `packaging/build-gui-rpm.sh` | `zupt-gui-5.2.7-1.noarch.rpm` and matching `.src.rpm` after package and installed integration gates | +| Linux CLI archive | `.github/workflows/ci.yml` | `zupt-5.2.7-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.7-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.7 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 +203,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.7-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.7-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.7 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 +229,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.7 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 +244,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.7 release evidence. Extended-length/device namespace paths, raw UNC output +roots, and mapped/network-drive output are not supported in 5.2.7. 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 +253,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.7 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 +272,7 @@ build. For every published artifact: -1. start from the immutable `v5.2.6` tag; +1. start from the immutable `v5.2.7` 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 @@ -287,7 +290,7 @@ 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.7` 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. diff --git a/INSTALL.md b/INSTALL.md index d3af0a4..17e0088 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,4 +1,4 @@ -# Installing ZUPT 5.2.6 +# Installing ZUPT 5.2.7 This guide covers the ZUPT command-line program and the optional Python GUI. The canonical source repository is @@ -27,17 +27,19 @@ 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 corrects those test-harness +boundaries, but still requires fresh gates. Do not treat any prior candidate's +artifacts or evidence as 5.2.7 packages or validation. -The 5.2.6 package set eligible for promotion after each target gate succeeds is: +The 5.2.7 package set eligible for promotion after each target gate succeeds is: | 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` | +| CLI | `zupt-5.2.7.tar.gz`, `zupt_5.2.7_amd64.deb`, openSUSE x86_64 binary/source RPMs, `zupt-5.2.7-linux-x86_64.tar.xz`, `zupt-5.2.7-windows-x86_64.zip`, and `ZUPT-5.2.7-macOS-*.dmg` | +| GUI | `zupt-gui_5.2.7_all.deb`, `zupt-gui-5.2.7-1.noarch.rpm`, `zupt-gui-5.2.7-1.src.rpm`, and `zupt-gui-5.2.7-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 +47,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.7. 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 +84,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.7 has been accepted into each distribution repository. ## Build and test from source @@ -102,7 +104,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.7.tar.gz ``` The default build provides the native password, ML-KEM-768 + X25519 hybrid diff --git a/README.md b/README.md index 97197f3..3d62ca2 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,41 @@ -# ZUPT 5.2.6 +# ZUPT 5.2.7 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.7 corrects two test-harness portability defects exposed after the +immutable `v5.2.6` tag. Exact-tag GitHub Actions run `33442264243` completed 13 +jobs successfully, but the native macOS job failed because x86 SHA-NI test +helper declarations were unused on arm64 under `-Werror`, and the native +Windows job aborted while argv transcoding a safe UTF-8 fixture before its +intended assertions. No 5.2.6 assets were promoted. These are release/test +integration corrections; they do not change the archive format, cryptography, +bundled codec, or SDK ABI. No v5.2.6 evidence transfers automatically to +v5.2.7. 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.7 -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. +The SHA-NI regression now 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. All current release paths move to 5.2.7 and require +fresh exact-tag hosted CI, package, native-platform, source-only, checksum, OBS, +and promotion evidence. The `v5.2.6` tag remains immutable and 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 @@ -145,9 +151,9 @@ 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.7 release artifacts -The 5.2.6 release workflow is defined to produce the following files only after +The 5.2.7 release workflow is defined to produce the following files only after the corresponding target gate succeeds. `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 @@ -156,23 +162,23 @@ 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.7.tar.gz` | Reproducible, source-only archive; scanned twice-built input plus SHA-256. | +| `zupt_5.2.7_amd64.deb` | Ubuntu 24.04 amd64 package; install, functional round trip, and uninstall gate. | +| `zupt-5.2.7-*.x86_64.rpm` and `.src.rpm` | openSUSE Tumbleweed x86_64 source/binary RPM gate; package inspection, install, round trip, and uninstall. | +| `zupt-5.2.7-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.7_all.deb` | Architecture-independent Python/Qt GUI package; exact dependency/payload checks plus installed off-screen GUI/CLI integration gate. | +| `zupt-gui-5.2.7-1.noarch.rpm` | Architecture-independent Python/Qt GUI RPM; package inspection plus installed off-screen GUI/CLI integration gate. | +| `zupt-gui-5.2.7-1.src.rpm` | Source RPM corresponding exactly to the gated noarch GUI RPM. | +| `zupt-gui-5.2.7-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.7-windows-x86_64.zip` | Native Windows x86_64 executable with notices; extracted-ZIP round-trip gate. | +| `ZUPT-5.2.7-macOS-*.dmg` | Native macOS image; mounted packaged executable round-trip gate, with the actual architecture in the filename. | 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.7. 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 @@ -202,8 +208,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.7 +bash scripts/check-source-only.sh --archive /path/to/zupt-5.2.7.tar.gz ~~~ Unknown `.bin` files fail the scan. A necessary binary data fixture may be @@ -339,7 +345,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.7 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 +353,20 @@ 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.7 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, +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 exact 5.2.7 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`. -On Windows, 5.2.6 scopes output handling to normal local Win32 paths. A MinGW +On Windows, 5.2.7 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 +388,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.7 archive. Compression and disk backup never create a no-AIT archive. `info` is deliberately different: it reports unauthenticated framing metadata, @@ -398,7 +406,7 @@ 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 +The candidate commands and outcome fields for 5.2.7 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 @@ -410,9 +418,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.7.tar.gz bash scripts/check-source-only.sh \ - --archive /tmp/zupt-5.2.6.tar.gz + --archive /tmp/zupt-5.2.7.tar.gz ~~~ Archive ordering, ownership and timestamps are normalized. The default epoch is @@ -428,7 +436,7 @@ final digest before the tag is published. ## 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 +immutable v5.2.7 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 +479,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.7 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 +489,13 @@ 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 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, or 5.2.7 changes. ## License diff --git a/SECURITY.md b/SECURITY.md index ffa42ff..aa2118e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,4 @@ -# Security Policy — ZUPT 5.2.6 +# Security Policy — ZUPT 5.2.7 ## Reporting vulnerabilities @@ -66,7 +66,7 @@ partially accepted. ### Optional integrations -The 5.2.6 default is `WITH_SDK=0 WITH_PQBOX=0`: +The 5.2.7 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. @@ -133,7 +133,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.7 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 @@ -204,7 +204,7 @@ media before proceeding. 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.7. 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 +247,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.7.tar.gz ``` Nested archive inspection is required to enforce bounded recursion, member @@ -257,13 +257,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.7. 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.7 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, @@ -303,7 +303,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.7. 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,14 +313,18 @@ 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.7. 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 +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. The exact 5.2.7 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 diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 31529e1..0d8091d 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -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.7 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.7 upstream release gates. ## Reporting attribution issues diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index c355cdd..4d45192 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,4 +1,4 @@ -# ZUPT 5.2.6 threat model +# ZUPT 5.2.7 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.7 source with: ```sh make WITH_SDK=0 WITH_PQBOX=0 @@ -175,7 +175,7 @@ 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. +The Windows handle-relative boundary in 5.2.7 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 @@ -250,9 +250,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.7; 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.7, 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 +265,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.7 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,13 +275,17 @@ 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.7 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 +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. The corresponding 5.2.7 test-harness 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 +rpmlint `no-binary` finding remain pending until an exact 5.2.7 candidate records them. ## Historical compatibility notes @@ -309,7 +313,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.7 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 +324,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.7, 2026-08-31. diff --git a/doc/zupt-gui.1 b/doc/zupt-gui.1 index bd132dd..2c37b87 100644 --- a/doc/zupt-gui.1 +++ b/doc/zupt-gui.1 @@ -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.7" "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.7 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 diff --git a/doc/zupt.1 b/doc/zupt.1 index 94a8670..ed48a17 100644 --- a/doc/zupt.1 +++ b/doc/zupt.1 @@ -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.7" "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.7; 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.7. 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.7 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. diff --git a/gui/README.md b/gui/README.md index 4177574..1937c1d 100644 --- a/gui/README.md +++ b/gui/README.md @@ -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.7 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.7_all.deb`; +- `zupt-gui-5.2.7-1.noarch.rpm`; +- `zupt-gui-5.2.7-1.src.rpm`; +- `zupt-gui-5.2.7-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.7 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.7 release asset. The promoted Windows ZIP and macOS DMG are CLI-only. Packaging recipes and scripts under `gui/packaging/` and `packaging/` are build diff --git a/gui/packaging/deb/control b/gui/packaging/deb/control index 3c50f66..a3632d2 100644 --- a/gui/packaging/deb/control +++ b/gui/packaging/deb/control @@ -1,9 +1,9 @@ Package: zupt-gui -Version: 5.2.6 +Version: 5.2.7 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.7) Maintainer: Cristian Cezar Moisés Homepage: https://github.com/cristiancmoises/zupt Description: Qt graphical interface for the ZUPT backup utility diff --git a/gui/packaging/windows/build-windows.bat b/gui/packaging/windows/build-windows.bat index 8d8eec9..544286f 100644 --- a/gui/packaging/windows/build-windows.bat +++ b/gui/packaging/windows/build-windows.bat @@ -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.7" 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%" diff --git a/include/zupt.h b/include/zupt.h index f16e441..c9923ab 100644 --- a/include/zupt.h +++ b/include/zupt.h @@ -121,7 +121,8 @@ 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. */ +#define ZUPT_VERSION_STRING "5.2.7" /* 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" diff --git a/install.sh b/install.sh index 717562e..88a8221 100644 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ set -Eeuo pipefail umask 077 -VERSION=${VERSION:-5.2.6} +VERSION=${VERSION:-5.2.7} PREFIX=${PREFIX:-/usr/local} echo "🔧 Installing ZUPT..." diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index f88ecb6..d9654f1 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -11,7 +11,7 @@ # Test locally with `makepkg -s` after the release archive is published. pkgname=zupt -pkgver=5.2.6 +pkgver=5.2.7 pkgrel=1 pkgdesc='Pure-C11 post-quantum backup compression utility (AES-256-CTR + HMAC-SHA256 + ML-KEM-768 + X25519)' arch=('x86_64') @@ -22,7 +22,7 @@ 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. +# Byte-reproducible upstream v5.2.7 source archive. sha256sums=('e64bd92aac1b50018efd3c8b678c70771320a2069d47f32c03349c569100b27c') build() { diff --git a/packaging/debian/changelog b/packaging/debian/changelog index 0376ac0..fcc4b75 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,3 +1,13 @@ +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 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 diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index 5a68396..8673b94 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -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.7") (define %zupt-source (origin diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index 11c34bf..34ed50f 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -22,8 +22,8 @@ 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" + url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.7/zupt-5.2.7.tar.gz" + version "5.2.7" sha256 "e64bd92aac1b50018efd3c8b678c70771320a2069d47f32c03349c569100b27c" license all_of: ["AGPL-3.0-or-later", "GPL-3.0-or-later", "BSD-2-Clause", "BSD-3-Clause", "CC0-1.0"] diff --git a/packaging/nix/flake.nix b/packaging/nix/flake.nix index 6caf616..5ccfdbf 100644 --- a/packaging/nix/flake.nix +++ b/packaging/nix/flake.nix @@ -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.7"; # ...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.7"; # When publishing, replace this with `fetchurl` against the # release tarball. For local development the flake assumes it diff --git a/packaging/opensuse/README.md b/packaging/opensuse/README.md index 09ed96e..c29f480 100644 --- a/packaging/opensuse/README.md +++ b/packaging/opensuse/README.md @@ -1,4 +1,4 @@ -# ZUPT 5.2.6 for openSUSE Build Service +# ZUPT 5.2.7 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 +attribute upstream code or the 5.2.2/5.2.3/5.2.4/5.2.5/5.2.6/5.2.7 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.7` 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.7.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.7 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.7.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.7, 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 @@ -194,7 +194,7 @@ 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 +automatically to v5.2.7. The exact v5.2.7 candidate must repeat every applicable gate, and the automatic openSUSE `debugsource` rpmlint `no-binary` finding remains unresolved and unsuppressed. @@ -206,8 +206,18 @@ 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.7 hosted, native, OBS, or promotion +evidence. ## Prior openSUSE packaging validation @@ -246,11 +256,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.7` 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.7`. 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 +272,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.7.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. diff --git a/packaging/opensuse/_service b/packaging/opensuse/_service index 82a8624..226388a 100644 --- a/packaging/opensuse/_service +++ b/packaging/opensuse/_service @@ -4,7 +4,7 @@ https://github.com/cristiancmoises/zupt.git git - refs/tags/v5.2.6 + refs/tags/v5.2.7 @PARENT_TAG@ ^v(.*)$ \1 diff --git a/packaging/opensuse/zupt.changes b/packaging/opensuse/zupt.changes index 228b91c..f73d7e7 100644 --- a/packaging/opensuse/zupt.changes +++ b/packaging/opensuse/zupt.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Mon Aug 31 23:00:00 UTC 2026 - Cristian Cezar Moisés + +- 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 diff --git a/packaging/opensuse/zupt.spec b/packaging/opensuse/zupt.spec index aea660a..def34ff 100644 --- a/packaging/opensuse/zupt.spec +++ b/packaging/opensuse/zupt.spec @@ -18,7 +18,7 @@ # Name: zupt -Version: 5.2.6 +Version: 5.2.7 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 diff --git a/packaging/portable/README.txt b/packaging/portable/README.txt index 2f02f53..815f3aa 100644 --- a/packaging/portable/README.txt +++ b/packaging/portable/README.txt @@ -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.7, 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.7 or place its command beside the launcher. * Set ZUPT_DEBUG=1 to print command-discovery diagnostics to stderr. diff --git a/packaging/rpm/zupt.spec b/packaging/rpm/zupt.spec index 10f1a82..0feb67a 100644 --- a/packaging/rpm/zupt.spec +++ b/packaging/rpm/zupt.spec @@ -20,7 +20,7 @@ # installed smoke test. Name: zupt -Version: 5.2.6 +Version: 5.2.7 Release: 1%{?dist} Summary: Backup compression with authenticated and post-quantum encryption @@ -101,6 +101,11 @@ comments. Plain archives use non-cryptographic checksums. %endif %changelog +* Mon Aug 31 2026 Cristian Cezar Moisés - 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 - 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 diff --git a/tests/test_path_traversal.sh b/tests/test_path_traversal.sh index 195b54d..feadad7 100755 --- a/tests/test_path_traversal.sh +++ b/tests/test_path_traversal.sh @@ -248,11 +248,26 @@ 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=73616665206469722f61c3a7c3a36f2e747874 +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" 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 + 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 UTF-8 path extracts normally' else fail 'safe nested UTF-8 path extracts normally' diff --git a/tests/test_sha256_shani.c b/tests/test_sha256_shani.c index b850bb0..58b6581 100644 --- a/tests/test_sha256_shani.c +++ b/tests/test_sha256_shani.c @@ -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); From 8cca84125886ad19ce23b46c16ab45707b33f4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 19:07:55 -0300 Subject: [PATCH 04/18] packaging: pin 5.2.7 source archive checksums [skip ci] --- packaging/aur/PKGBUILD | 2 +- packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index d9654f1..1f0e406 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -23,7 +23,7 @@ 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.7 source archive. -sha256sums=('e64bd92aac1b50018efd3c8b678c70771320a2069d47f32c03349c569100b27c') +sha256sums=('4db1badbb9a62676176829326b6fd4bab5fe67be6631a7f6d341bdc6c7265c04') build() { cd "${pkgname}-${pkgver}" diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index 8673b94..456451a 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -72,7 +72,7 @@ "/releases/download/v" %zupt-version "/zupt-" %zupt-version ".tar.gz")) (sha256 - (base32 "0z5j028md71l0cng6iwx0si204vpf266g2rwzn702l0vmhmdjjz6")))) + (base32 "012w4v3wdga1sgvafcb6prkzxddssipnnci9d0bpc9m6p7dvmcad")))) (define-public zupt (package diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index 34ed50f..887bccc 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -24,7 +24,7 @@ class Zupt < Formula homepage "https://github.com/cristiancmoises/zupt" url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.7/zupt-5.2.7.tar.gz" version "5.2.7" - sha256 "e64bd92aac1b50018efd3c8b678c70771320a2069d47f32c03349c569100b27c" + sha256 "4db1badbb9a62676176829326b6fd4bab5fe67be6631a7f6d341bdc6c7265c04" 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 From d8668e6d6466cfc3987c5d241d38da52bdae3293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 20:27:14 -0300 Subject: [PATCH 05/18] security: close filesystem race findings --- .github/workflows/ci.yml | 2 + Makefile | 1 + sdk/src/zuptsdk.c | 46 +++- sdk/tests/test_sdk_roundtrip.c | 117 +++++++- src/zupt_disk.c | 123 ++++----- src/zupt_main.c | 397 ++++++++++++++++++++++++---- tests/test_benchmark_temp_safety.sh | 82 +++++- tests/test_password_sources.sh | 23 +- tests/test_source_only.sh | 21 +- tests/test_static_analysis.sh | 25 ++ 10 files changed, 701 insertions(+), 136 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 730731b..c4e1ba4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,6 +121,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" diff --git a/Makefile b/Makefile index da95b79..5b6ec4e 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/sdk/src/zuptsdk.c b/sdk/src/zuptsdk.c index d886aa0..f90245e 100644 --- a/sdk/src/zuptsdk.c +++ b/sdk/src/zuptsdk.c @@ -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; } diff --git a/sdk/tests/test_sdk_roundtrip.c b/sdk/tests/test_sdk_roundtrip.c index 2176a60..8ef87a2 100644 --- a/sdk/tests/test_sdk_roundtrip.c +++ b/sdk/tests/test_sdk_roundtrip.c @@ -12,6 +12,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #include static int g_pass = 0, g_fail = 0; @@ -47,6 +50,79 @@ 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) || stat(path, &info) != 0 || + info.st_size < 0 || (uint64_t)info.st_size != (uint64_t)expected_size) + return 0; + FILE *stream = fopen(path, "rb"); + if (!stream) return 0; + size_t got = fread(observed, 1, expected_size, stream); + int read_error = ferror(stream); + int close_rc = fclose(stream); + return got == expected_size && !read_error && close_rc == 0 && + memcmp(observed, expected, expected_size) == 0; +} + +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) || + stat(target, &target_st) != 0 || lstat(symlink_path, &output_st) != 0 || + (target_st.st_dev == output_st.st_dev && + target_st.st_ino == output_st.st_ino) || + !S_ISREG(output_st.st_mode) || 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) || + stat(target, &target_st) != 0 || stat(hardlink_path, &output_st) != 0 || + (target_st.st_dev == output_st.st_dev && + target_st.st_ino == output_st.st_ino) || + !S_ISREG(output_st.st_mode) || 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 +326,15 @@ cleanup: static void test_keypair_pq(void) { TEST("keypair_generate + compress_pq + extract_pq"); + char saved_priv[160]; + char saved_pub[160]; + 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); + zuptsdk_ctx_t *ctx = NULL; CHECK(zuptsdk_ctx_create(&ctx), "ctx"); @@ -257,17 +342,35 @@ 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 (!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 (stat(saved_priv, &private_st) != 0 || + stat(saved_pub, &public_st) != 0 || + (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 +398,8 @@ static void test_keypair_pq(void) { zuptsdk_privkey_destroy(priv); zuptsdk_options_destroy(opts); - unlink("/tmp/_zsdk_priv.key"); - unlink("/tmp/_zsdk_pub.key"); + unlink(saved_priv); + unlink(saved_pub); if (!ok) { FAIL("byte mismatch or rc != OK"); zuptsdk_keypair_destroy(kp); zuptsdk_ctx_destroy(ctx); return; } zuptsdk_keypair_destroy(kp); @@ -305,6 +408,8 @@ static void test_keypair_pq(void) { return; err: + unlink(saved_priv); + unlink(saved_pub); zuptsdk_keypair_destroy(kp); zuptsdk_ctx_destroy(ctx); } diff --git a/src/zupt_disk.c b/src/zupt_disk.c index b7a2570..d6bc090 100644 --- a/src/zupt_disk.c +++ b/src/zupt_disk.c @@ -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; diff --git a/src/zupt_main.c b/src/zupt_main.c index f2b5b3b..a0d4a00 100644 --- a/src/zupt_main.c +++ b/src/zupt_main.c @@ -25,7 +25,9 @@ #ifdef _WIN32 #include #include + #include #else + #include #include #include #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, ¤t) && + (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(); diff --git a/tests/test_benchmark_temp_safety.sh b/tests/test_benchmark_temp_safety.sh index 470f545..954b5fb 100755 --- a/tests/test_benchmark_temp_safety.sh +++ b/tests/test_benchmark_temp_safety.sh @@ -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' diff --git a/tests/test_password_sources.sh b/tests/test_password_sources.sh index 014ad1c..2261eb6 100644 --- a/tests/test_password_sources.sh +++ b/tests/test_password_sources.sh @@ -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 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 \ + "$prompt_log" 2>&1 + prompt_status=$? + set -e +else + set +e + "$binary" test --password-prompt archive.zupt \ + "$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 diff --git a/tests/test_source_only.sh b/tests/test_source_only.sh index 9243386..3ed42fb 100755 --- a/tests/test_source_only.sh +++ b/tests/test_source_only.sh @@ -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) diff --git a/tests/test_static_analysis.sh b/tests/test_static_analysis.sh index 49af6ba..690898e 100755 --- a/tests/test_static_analysis.sh +++ b/tests/test_static_analysis.sh @@ -187,6 +187,31 @@ 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 + echo "" echo " ───────────────────────────────────────" echo " Static analysis: $PASS passed, $FAIL failed" From 7687cfa5776a17e74068ec73003811b02c9de160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 20:32:42 -0300 Subject: [PATCH 06/18] release: prepare ZUPT 5.2.8 --- .github/workflows/promote-release.yml | 2 +- AUDIT.md | 75 ++++++++++++--- CHANGELOG.md | 81 ++++++++++++---- DISTRIBUTION.md | 55 ++++++----- INSTALL.md | 29 ++++-- README.md | 118 +++++++++++++++--------- SECURITY.md | 70 ++++++++++---- THIRD-PARTY-NOTICES.md | 4 +- THREAT_MODEL.md | 73 ++++++++++----- doc/zupt-gui.1 | 4 +- doc/zupt.1 | 8 +- gui/README.md | 14 +-- gui/packaging/deb/control | 4 +- gui/packaging/windows/build-windows.bat | 2 +- include/zupt.h | 3 +- install.sh | 2 +- packaging/aur/PKGBUILD | 4 +- packaging/debian/changelog | 14 +++ packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 4 +- packaging/nix/flake.nix | 4 +- packaging/opensuse/README.md | 40 +++++--- packaging/opensuse/_service | 2 +- packaging/opensuse/zupt.changes | 15 +++ packaging/opensuse/zupt.spec | 2 +- packaging/portable/README.txt | 4 +- packaging/rpm/zupt.spec | 11 ++- src/zupt_format.c | 2 +- tests/test_audit_flake.sh | 4 +- 29 files changed, 454 insertions(+), 198 deletions(-) diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml index d05b427..e28d6c1 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/promote-release.yml @@ -11,7 +11,7 @@ on: required: true type: number tag: - description: Existing annotated release tag, for example v5.2.7 + description: Existing annotated release tag, for example v5.2.8 required: true type: string diff --git a/AUDIT.md b/AUDIT.md index 62eaa08..7f46f35 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,12 +1,12 @@ -# ZUPT 5.2.7 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.7 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.7 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.7 +scripts/check-source-only.sh --tag v5.2.8 # generated source archive -scripts/check-source-only.sh --archive /path/to/zupt-5.2.7.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.7 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.7. +the skipped native jobs into passes or transfer any result to 5.2.8. ## Prior 5.2.5 exact-tag native-gate evidence @@ -164,7 +166,7 @@ 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 or -v5.2.7 hosted CI, package, native-platform, or promotion evidence. +v5.2.8 hosted CI, package, native-platform, or promotion evidence. ## Prior 5.2.6 exact-tag native-gate evidence @@ -178,10 +180,52 @@ 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. They are not proof -that any v5.2.7 native or hosted gate has passed. +not archive-format, cryptographic, codec, or SDK ABI changes. -The exact 5.2.7 candidate must repeat the required suite. Native Windows and +## 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 || 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 +330,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.7. 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,7 +340,7 @@ 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.7. 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 were corrected for 5.2.6. A targeted clean-clone @@ -324,8 +351,15 @@ 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. The exact 5.2.7 candidate must -repeat the required suite. Native Windows and macOS, hosted GitHub CI/release +changes; v5.2.6 remained unpromoted. The exact 5.2.8 candidate must +repeat the required 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, but this is not exact-candidate evidence. 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`. diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 0d8091d..025f76d 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -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.7 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.7 upstream release gates. +not covered by the 5.2.8 upstream release gates. ## Reporting attribution issues diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 4d45192..413269b 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,4 +1,4 @@ -# ZUPT 5.2.7 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.7 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,37 @@ 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.7 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 +are project evidence, not independent certification or proof that the exact +5.2.8 hosted/native gates passed. For an untrusted archive: @@ -250,9 +274,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.7; bare Linux and Windows executables are also excluded. +promoted for 5.2.8; bare Linux and Windows executables are also excluded. -For 5.2.7, 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 +289,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.7 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,17 +299,24 @@ 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.7 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 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. The corresponding 5.2.7 test-harness corrections do not establish -their own test result. Hosted GitHub CI and release promotion, native +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 do not establish their +own test result. CI now exercises `sdk-test`, but its inclusion is not a pass. +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.7 candidate +rpmlint `no-binary` finding remain pending until an exact 5.2.8 candidate records them. ## Historical compatibility notes @@ -313,7 +344,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.7 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. @@ -324,4 +355,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.7, 2026-08-31. +Document version: 5.2.8, 2026-08-31. diff --git a/doc/zupt-gui.1 b/doc/zupt-gui.1 index 2c37b87..cecb070 100644 --- a/doc/zupt-gui.1 +++ b/doc/zupt-gui.1 @@ -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.7" "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.7 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 diff --git a/doc/zupt.1 b/doc/zupt.1 index ed48a17..d871e25 100644 --- a/doc/zupt.1 +++ b/doc/zupt.1 @@ -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.7" "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.7; 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.7. 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.7 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. diff --git a/gui/README.md b/gui/README.md index 1937c1d..378fda5 100644 --- a/gui/README.md +++ b/gui/README.md @@ -1,6 +1,6 @@ # ZUPT GUI -The ZUPT GUI is a Python/Qt front end for the ZUPT 5.2.7 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.7_all.deb`; -- `zupt-gui-5.2.7-1.noarch.rpm`; -- `zupt-gui-5.2.7-1.src.rpm`; -- `zupt-gui-5.2.7-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.7 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.7 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 diff --git a/gui/packaging/deb/control b/gui/packaging/deb/control index a3632d2..65765c6 100644 --- a/gui/packaging/deb/control +++ b/gui/packaging/deb/control @@ -1,9 +1,9 @@ Package: zupt-gui -Version: 5.2.7 +Version: 5.2.8 Section: utils Priority: optional Architecture: all -Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= 5.2.7) +Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= 5.2.8) Maintainer: Cristian Cezar Moisés Homepage: https://github.com/cristiancmoises/zupt Description: Qt graphical interface for the ZUPT backup utility diff --git a/gui/packaging/windows/build-windows.bat b/gui/packaging/windows/build-windows.bat index 544286f..36c23d6 100644 --- a/gui/packaging/windows/build-windows.bat +++ b/gui/packaging/windows/build-windows.bat @@ -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.7" +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%" diff --git a/include/zupt.h b/include/zupt.h index c9923ab..4136770 100644 --- a/include/zupt.h +++ b/include/zupt.h @@ -122,7 +122,8 @@ static inline int zupt_win_mkdir_utf8(const char *path) { /* 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. */ /* v5.2.7 corrects native test-harness portability; format remains v1.6. */ -#define ZUPT_VERSION_STRING "5.2.7" +/* 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" diff --git a/install.sh b/install.sh index 88a8221..d7e22d5 100644 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ set -Eeuo pipefail umask 077 -VERSION=${VERSION:-5.2.7} +VERSION=${VERSION:-5.2.8} PREFIX=${PREFIX:-/usr/local} echo "🔧 Installing ZUPT..." diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 1f0e406..e833860 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -11,7 +11,7 @@ # Test locally with `makepkg -s` after the release archive is published. pkgname=zupt -pkgver=5.2.7 +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,7 +22,7 @@ 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.7 source archive. +# Byte-reproducible upstream v5.2.8 source archive. sha256sums=('4db1badbb9a62676176829326b6fd4bab5fe67be6631a7f6d341bdc6c7265c04') build() { diff --git a/packaging/debian/changelog b/packaging/debian/changelog index fcc4b75..cbed093 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,3 +1,17 @@ +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 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 diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index 456451a..32e24ad 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -62,7 +62,7 @@ xcb-util-renderutil xcb-util-wm xcb-util-cursor libinput-minimal mtdev libevdev eudev)) -(define %zupt-version "5.2.7") +(define %zupt-version "5.2.8") (define %zupt-source (origin diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index 887bccc..bedf037 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -22,8 +22,8 @@ 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.7/zupt-5.2.7.tar.gz" - version "5.2.7" + url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.8/zupt-5.2.8.tar.gz" + version "5.2.8" sha256 "4db1badbb9a62676176829326b6fd4bab5fe67be6631a7f6d341bdc6c7265c04" license all_of: ["AGPL-3.0-or-later", "GPL-3.0-or-later", "BSD-2-Clause", "BSD-3-Clause", "CC0-1.0"] diff --git a/packaging/nix/flake.nix b/packaging/nix/flake.nix index 5ccfdbf..aba67e8 100644 --- a/packaging/nix/flake.nix +++ b/packaging/nix/flake.nix @@ -9,7 +9,7 @@ # nix flake check # lint the flake # # To consume from another flake: -# inputs.zupt.url = "github:cristiancmoises/zupt/v5.2.7"; +# 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.7"; + version = "5.2.8"; # When publishing, replace this with `fetchurl` against the # release tarball. For local development the flake assumes it diff --git a/packaging/opensuse/README.md b/packaging/opensuse/README.md index c29f480..1236273 100644 --- a/packaging/opensuse/README.md +++ b/packaging/opensuse/README.md @@ -1,4 +1,4 @@ -# ZUPT 5.2.7 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/5.2.7 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.7` 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.7.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.7 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.7.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.7, 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 @@ -194,7 +194,7 @@ 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.7. The exact v5.2.7 candidate must repeat every applicable +automatically to v5.2.8. The exact v5.2.8 candidate must repeat every applicable gate, and the automatic openSUSE `debugsource` rpmlint `no-binary` finding remains unresolved and unsuppressed. @@ -216,9 +216,23 @@ 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.7 hosted, native, OBS, or promotion +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. + ## Prior openSUSE packaging validation The local results below were produced on 2026-08-24 from the 5.2.2 candidate @@ -256,11 +270,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.7` 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.7`. 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. @@ -272,7 +286,7 @@ gate. ``` 4. From the extracted handoff root, run - `packaging/opensuse/source-audit.sh --archive /path/to/zupt-5.2.7.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. diff --git a/packaging/opensuse/_service b/packaging/opensuse/_service index 226388a..7fc42da 100644 --- a/packaging/opensuse/_service +++ b/packaging/opensuse/_service @@ -4,7 +4,7 @@ https://github.com/cristiancmoises/zupt.git git - refs/tags/v5.2.7 + refs/tags/v5.2.8 @PARENT_TAG@ ^v(.*)$ \1 diff --git a/packaging/opensuse/zupt.changes b/packaging/opensuse/zupt.changes index f73d7e7..476816b 100644 --- a/packaging/opensuse/zupt.changes +++ b/packaging/opensuse/zupt.changes @@ -1,3 +1,18 @@ +------------------------------------------------------------------- +Mon Aug 31 23:30:00 UTC 2026 - Cristian Cezar Moisés + +- 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 diff --git a/packaging/opensuse/zupt.spec b/packaging/opensuse/zupt.spec index def34ff..f4a6573 100644 --- a/packaging/opensuse/zupt.spec +++ b/packaging/opensuse/zupt.spec @@ -18,7 +18,7 @@ # Name: zupt -Version: 5.2.7 +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 diff --git a/packaging/portable/README.txt b/packaging/portable/README.txt index 815f3aa..05b06ae 100644 --- a/packaging/portable/README.txt +++ b/packaging/portable/README.txt @@ -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.7, 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.7 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. diff --git a/packaging/rpm/zupt.spec b/packaging/rpm/zupt.spec index 0feb67a..cdefc2a 100644 --- a/packaging/rpm/zupt.spec +++ b/packaging/rpm/zupt.spec @@ -20,7 +20,7 @@ # installed smoke test. Name: zupt -Version: 5.2.7 +Version: 5.2.8 Release: 1%{?dist} Summary: Backup compression with authenticated and post-quantum encryption @@ -101,6 +101,15 @@ comments. Plain archives use non-cryptographic checksums. %endif %changelog +* Mon Aug 31 2026 Cristian Cezar Moisés - 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 - 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. diff --git a/src/zupt_format.c b/src/zupt_format.c index ef54a6c..f127d98 100644 --- a/src/zupt_format.c +++ b/src/zupt_format.c @@ -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; diff --git a/tests/test_audit_flake.sh b/tests/test_audit_flake.sh index f6ce7ec..b48724a 100755 --- a/tests/test_audit_flake.sh +++ b/tests/test_audit_flake.sh @@ -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}" From 69fc26bfec8560f6f456be28f6cb6ad1710e5f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 20:33:52 -0300 Subject: [PATCH 07/18] packaging: pin 5.2.8 source archive checksums --- packaging/aur/PKGBUILD | 2 +- packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index e833860..b5cffea 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -23,7 +23,7 @@ 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.8 source archive. -sha256sums=('4db1badbb9a62676176829326b6fd4bab5fe67be6631a7f6d341bdc6c7265c04') +sha256sums=('07e213a246bb77ccb77be9814ce2c8505ce446eb4eef12b0d768dad532991ed1') build() { cd "${pkgname}-${pkgver}" diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index 32e24ad..e4f7660 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -72,7 +72,7 @@ "/releases/download/v" %zupt-version "/zupt-" %zupt-version ".tar.gz")) (sha256 - (base32 "012w4v3wdga1sgvafcb6prkzxddssipnnci9d0bpc9m6p7dvmcad")))) + (base32 "1l8yk4rdbnk8syq15vsfxd3f8p2hr3i4r0g9gfvwqxxv8si17qh7")))) (define-public zupt (package diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index bedf037..6f324b5 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -24,7 +24,7 @@ class Zupt < Formula homepage "https://github.com/cristiancmoises/zupt" url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.8/zupt-5.2.8.tar.gz" version "5.2.8" - sha256 "4db1badbb9a62676176829326b6fd4bab5fe67be6631a7f6d341bdc6c7265c04" + sha256 "07e213a246bb77ccb77be9814ce2c8505ce446eb4eef12b0d768dad532991ed1" 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 From 65bc5bd427a2b2dfd172f173982ad091803bef12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 20:51:59 -0300 Subject: [PATCH 08/18] test: close follow-up security gate findings --- .github/workflows/ci.yml | 1 + AUDIT.md | 9 +++++ CHANGELOG.md | 13 +++++- README.md | 13 ++++-- SECURITY.md | 7 ++++ scripts/check-source-only.sh | 10 +++++ sdk/tests/test_sdk_roundtrip.c | 71 +++++++++++++++++++++++++-------- tests/test_key_files.sh | 73 ++++++++++++++++++++++++++++++---- tests/test_static_analysis.sh | 13 ++++++ 9 files changed, 183 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4e1ba4..45abe02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 \ diff --git a/AUDIT.md b/AUDIT.md index 7f46f35..dfe887f 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -225,6 +225,14 @@ regression injects a directory symlink and verifies that its external sentinel survives. These are reviewed fixes and regression coverage, not independent certification or proof that a 5.2.8 hosted gate passed. +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. A fresh +default-branch scan remains the authoritative closure evidence. + The exact 5.2.8 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 @@ -270,6 +278,7 @@ should be rerun, but the historical resolution does not itself constitute a | 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c6cdbef..b36e681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,12 +26,23 @@ unchanged. `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. - 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. + 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. - 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 diff --git a/README.md b/README.md index 252a1c4..a845804 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,15 @@ 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`, and brings `sdk-test` into the +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. These +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 of commit `69fc26b` +closed #5, #6, and #7 and exposed test-only #8, #9, and #10 in the new SDK +regression. Their follow-up uses no-follow descriptors plus `fstat`/descriptor +reads and requires a fresh default-branch scan for closure. These corrections do not change archive format v1.6, cryptography, the bundled codec release, or the SDK ABI. @@ -36,7 +42,8 @@ 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, native redirected-prompt regression, and `sdk-test` CI step cover +fixture with Bash 3.2 unsigned-byte normalization, native redirected-prompt +and protected-DACL regressions, and `sdk-test` CI step cover these boundaries. All current release paths move to 5.2.8 and require fresh exact-tag hosted CI, package, native-platform, source-only, checksum, OBS, and promotion evidence. diff --git a/SECURITY.md b/SECURITY.md index e227e39..44c52ff 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -228,6 +228,13 @@ 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 or a claim that exact-v5.2.8 CI has passed. +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. A +subsequent default-branch scan is required as closure evidence. + 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.8. Cross-build and diff --git a/scripts/check-source-only.sh b/scripts/check-source-only.sh index 723cb4c..62c933f 100755 --- a/scripts/check-source-only.sh +++ b/scripts/check-source-only.sh @@ -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) || diff --git a/sdk/tests/test_sdk_roundtrip.c b/sdk/tests/test_sdk_roundtrip.c index 8ef87a2..f855bde 100644 --- a/sdk/tests/test_sdk_roundtrip.c +++ b/sdk/tests/test_sdk_roundtrip.c @@ -13,6 +13,7 @@ #include #include #ifndef _WIN32 +#include #include #endif #include @@ -55,18 +56,35 @@ 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) || stat(path, &info) != 0 || - info.st_size < 0 || (uint64_t)info.st_size != (uint64_t)expected_size) + if (expected_size > sizeof(observed)) return 0; - FILE *stream = fopen(path, "rb"); - if (!stream) return 0; - size_t got = fread(observed, 1, expected_size, stream); - int read_error = ferror(stream); - int close_rc = fclose(stream); - return got == expected_size && !read_error && close_rc == 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"; @@ -95,20 +113,22 @@ static int private_key_save_avoids_link_targets(const zuptsdk_keypair_t *kp) { if (symlink(target, symlink_path) != 0 || zuptsdk_keypair_save_private(kp, symlink_path) != ZUPTSDK_OK || !file_matches(target, sentinel, sizeof(sentinel) - 1) || - stat(target, &target_st) != 0 || lstat(symlink_path, &output_st) != 0 || + !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) || - !S_ISREG(output_st.st_mode) || output_st.st_size <= 0 || + 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) || - stat(target, &target_st) != 0 || stat(hardlink_path, &output_st) != 0 || + !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) || - !S_ISREG(output_st.st_mode) || output_st.st_size <= 0 || + output_st.st_size <= 0 || (output_st.st_mode & 0777) != 0600) goto cleanup; @@ -328,12 +348,16 @@ 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"); @@ -343,6 +367,16 @@ static void test_keypair_pq(void) { 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; @@ -357,8 +391,8 @@ static void test_keypair_pq(void) { #ifndef _WIN32 struct stat private_st; struct stat public_st; - if (stat(saved_priv, &private_st) != 0 || - stat(saved_pub, &public_st) != 0 || + 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"); @@ -398,8 +432,10 @@ static void test_keypair_pq(void) { zuptsdk_privkey_destroy(priv); zuptsdk_options_destroy(opts); - unlink(saved_priv); - unlink(saved_pub); + 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); @@ -410,6 +446,9 @@ static void test_keypair_pq(void) { err: unlink(saved_priv); unlink(saved_pub); +#ifndef _WIN32 + rmdir(saved_workspace); +#endif zuptsdk_keypair_destroy(kp); zuptsdk_ctx_destroy(ctx); } diff --git a/tests/test_key_files.sh b/tests/test_key_files.sh index ec4be02..8c232ff 100644 --- a/tests/test_key_files.sh +++ b/tests/test_key_files.sh @@ -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 +} + 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 diff --git a/tests/test_static_analysis.sh b/tests/test_static_analysis.sh index 690898e..4c13747 100755 --- a/tests/test_static_analysis.sh +++ b/tests/test_static_analysis.sh @@ -212,6 +212,19 @@ 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" From 7a8e5c5fbd64d42a5181e1ba6ae54105943c21b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 20:52:54 -0300 Subject: [PATCH 09/18] packaging: refresh 5.2.8 source checksums --- packaging/aur/PKGBUILD | 2 +- packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index b5cffea..241270a 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -23,7 +23,7 @@ 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.8 source archive. -sha256sums=('07e213a246bb77ccb77be9814ce2c8505ce446eb4eef12b0d768dad532991ed1') +sha256sums=('3a2a87948a02a3b97a60306e5a1987612dff2edb345a5628b9998461532539ad') build() { cd "${pkgname}-${pkgver}" diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index e4f7660..bb8a470 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -72,7 +72,7 @@ "/releases/download/v" %zupt-version "/zupt-" %zupt-version ".tar.gz")) (sha256 - (base32 "1l8yk4rdbnk8syq15vsfxd3f8p2hr3i4r0g9gfvwqxxv8si17qh7")))) + (base32 "1b9r4m9n314rp4l5cnilvcpgybb1hwcmlvihc1xbk8q2iaa8fais")))) (define-public zupt (package diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index 6f324b5..402a592 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -24,7 +24,7 @@ class Zupt < Formula homepage "https://github.com/cristiancmoises/zupt" url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.8/zupt-5.2.8.tar.gz" version "5.2.8" - sha256 "07e213a246bb77ccb77be9814ce2c8505ce446eb4eef12b0d768dad532991ed1" + sha256 "3a2a87948a02a3b97a60306e5a1987612dff2edb345a5628b9998461532539ad" 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 From af87e67a871058589d32a6b6f8426f18655e5b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 20:55:32 -0300 Subject: [PATCH 10/18] docs: finalize 5.2.8 checksum record --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b36e681..962b156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,8 @@ unchanged. 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. AUR/Homebrew SHA-256 and Guix content pins remain - pending until the final reproducible archive is generated. + references to 5.2.8, and pin the AUR/Homebrew SHA-256 and Guix content hash + to the final reproducible source archive before tagging. - Require fresh exact-`v5.2.8` source, checksum, hosted CI, native-platform, package, OBS, and promotion evidence. This entry does not claim those gates passed, and no v5.2.7 result transfers automatically. From cb60cf0218b00c1f7cd00c3defe46acf499dac8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 20:56:23 -0300 Subject: [PATCH 11/18] packaging: pin final 5.2.8 source archive --- packaging/aur/PKGBUILD | 2 +- packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 241270a..10aa698 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -23,7 +23,7 @@ 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.8 source archive. -sha256sums=('3a2a87948a02a3b97a60306e5a1987612dff2edb345a5628b9998461532539ad') +sha256sums=('0868bccf61e6375454b370545c0052ab3911d19d03032d688aedbec9ad1df0e9') build() { cd "${pkgname}-${pkgver}" diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index bb8a470..1c294ca 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -72,7 +72,7 @@ "/releases/download/v" %zupt-version "/zupt-" %zupt-version ".tar.gz")) (sha256 - (base32 "1b9r4m9n314rp4l5cnilvcpgybb1hwcmlvihc1xbk8q2iaa8fais")))) + (base32 "1sgh3nnwkgpdi9l2s0q3kp8i2fdba805qm3hnda58dz6c77vqs08")))) (define-public zupt (package diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index 402a592..923edf1 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -24,7 +24,7 @@ class Zupt < Formula homepage "https://github.com/cristiancmoises/zupt" url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.8/zupt-5.2.8.tar.gz" version "5.2.8" - sha256 "3a2a87948a02a3b97a60306e5a1987612dff2edb345a5628b9998461532539ad" + sha256 "0868bccf61e6375454b370545c0052ab3911d19d03032d688aedbec9ad1df0e9" 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 From 6fd78e0515e8a229d5e1d1f1034ed60c728ecff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 21:25:10 -0300 Subject: [PATCH 12/18] test: make Windows Unicode gate byte-exact --- .github/workflows/cross-platform.yml | 21 +++++++++++++++++---- tests/test_path_traversal.sh | 10 +++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cross-platform.yml b/.github/workflows/cross-platform.yml index 0c10766..fc21650 100644 --- a/.github/workflows/cross-platform.yml +++ b/.github/workflows/cross-platform.yml @@ -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 diff --git a/tests/test_path_traversal.sh b/tests/test_path_traversal.sh index feadad7..7982673 100755 --- a/tests/test_path_traversal.sh +++ b/tests/test_path_traversal.sh @@ -248,13 +248,17 @@ else fail 'backslash separators are normalized within the extraction root' fi -legitimate_entry_hex=73616665206469722f61c3a7c3a36f2e747874 +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 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 && python3 - "$TEST_ROOT/legitimate-out" "$legitimate_entry_hex" <<'PY' @@ -268,9 +272,9 @@ 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 UTF-8 path extracts normally' + 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" From a6e330827e3a3341a424ce9c13782a30cab9fb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 21:25:16 -0300 Subject: [PATCH 13/18] docs: record final security gate evidence --- AUDIT.md | 33 +++++++++++++++++++++++++-------- CHANGELOG.md | 8 +++++++- DISTRIBUTION.md | 7 ++++++- README.md | 28 +++++++++++++++++++--------- SECURITY.md | 24 ++++++++++++++++-------- 5 files changed, 73 insertions(+), 27 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index dfe887f..d5d6516 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -222,21 +222,38 @@ 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 or proof that a 5.2.8 hosted gate passed. +survives. These are reviewed fixes and regression coverage, not independent +certification or proof that an exact-tag 5.2.8 gate passed. 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. A fresh -default-branch scan remains the authoritative closure evidence. +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. -The exact 5.2.8 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. +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 is diagnostic evidence rather than release +approval. The exact 5.2.8 candidate must repeat the required suite. Exact-tag +native Windows/macOS, hosted CI, authenticated OBS service execution, and +release promotion remain pending until recorded otherwise. The pre-tag +openSUSE Tumbleweed job did build source and binary RPMs, pass `rpmlint` without +suppressions, and pass install/round-trip/uninstall checks. ## Cryptographic review boundary diff --git a/CHANGELOG.md b/CHANGELOG.md index 962b156..a1576d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,8 @@ unchanged. 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. + 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 @@ -43,6 +44,11 @@ unchanged. 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 diff --git a/DISTRIBUTION.md b/DISTRIBUTION.md index 30962e9..0acdb76 100644 --- a/DISTRIBUTION.md +++ b/DISTRIBUTION.md @@ -31,7 +31,12 @@ 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`. +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. Corrective packages and release assets must 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 diff --git a/README.md b/README.md index a845804..0d33c50 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,12 @@ 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 of commit `69fc26b` -closed #5, #6, and #7 and exposed test-only #8, #9, and #10 in the new SDK -regression. Their follow-up uses no-follow descriptors plus `fstat`/descriptor -reads and requires a fresh default-branch scan for closure. These -corrections do not change archive format v1.6, cryptography, the bundled codec -release, or the SDK ABI. +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. 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 @@ -29,6 +29,16 @@ 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. + 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 @@ -43,9 +53,9 @@ 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, and `sdk-test` CI step cover -these boundaries. All current release paths move to 5.2.8 and require fresh -exact-tag hosted CI, package, +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 move to 5.2.8 and require fresh exact-tag hosted CI, package, native-platform, source-only, checksum, OBS, and promotion evidence. ## Corrective changes introduced in 5.2.7 diff --git a/SECURITY.md b/SECURITY.md index 44c52ff..7b6c3e3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -232,8 +232,10 @@ 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. A -subsequent default-branch scan is required as closure evidence. +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 Windows handle-relative implementation is scoped to normal local Win32 paths. Win32 extended-length and device-namespace paths, raw UNC output roots, @@ -364,12 +366,18 @@ 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, but this is not exact-candidate evidence. 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`. +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 native gates, hosted CI, authenticated OBS service +execution, and release promotion remain pending. An unavailable or unexecuted +environment remains `SKIP`, never `PASS`. Run target-native static analyzers and package checks as additional evidence. Do not infer x86_64, aarch64, ppc64le, s390x, riscv64, macOS, Windows, Leap, or From 4741d34783965fa41b42373c3df340e3b8581a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 21:26:32 -0300 Subject: [PATCH 14/18] packaging: pin audited 5.2.8 source archive --- packaging/aur/PKGBUILD | 2 +- packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 10aa698..a212869 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -23,7 +23,7 @@ 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.8 source archive. -sha256sums=('0868bccf61e6375454b370545c0052ab3911d19d03032d688aedbec9ad1df0e9') +sha256sums=('7dcc72ae4fe287ac2c033070f959510998a9714aef9fde5b0f6c27365e3c88e7') build() { cd "${pkgname}-${pkgver}" diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index 1c294ca..9917b53 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -72,7 +72,7 @@ "/releases/download/v" %zupt-version "/zupt-" %zupt-version ".tar.gz")) (sha256 - (base32 "1sgh3nnwkgpdi9l2s0q3kp8i2fdba805qm3hnda58dz6c77vqs08")))) + (base32 "1rw87ig3c9vc1xdxx7zg99qsk609a5czjw1h0cnar1z29yp75k3x")))) (define-public zupt (package diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index 923edf1..219c4a2 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -24,7 +24,7 @@ class Zupt < Formula homepage "https://github.com/cristiancmoises/zupt" url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.8/zupt-5.2.8.tar.gz" version "5.2.8" - sha256 "0868bccf61e6375454b370545c0052ab3911d19d03032d688aedbec9ad1df0e9" + sha256 "7dcc72ae4fe287ac2c033070f959510998a9714aef9fde5b0f6c27365e3c88e7" 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 From f6bbb95eeec93d1d6a01577a110e8a279786b9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 21:36:27 -0300 Subject: [PATCH 15/18] docs: use canonical release epoch --- DISTRIBUTION.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/DISTRIBUTION.md b/DISTRIBUTION.md index 0acdb76..24b0c3b 100644 --- a/DISTRIBUTION.md +++ b/DISTRIBUTION.md @@ -84,14 +84,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.8.tar.gz dist +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 From ebb9ab3aa1d42c50030ca02883f6162dc4771fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 21:37:11 -0300 Subject: [PATCH 16/18] packaging: repin canonical 5.2.8 archive --- packaging/aur/PKGBUILD | 2 +- packaging/guix/zupt.scm | 2 +- packaging/homebrew/zupt.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index a212869..5cec646 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -23,7 +23,7 @@ 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.8 source archive. -sha256sums=('7dcc72ae4fe287ac2c033070f959510998a9714aef9fde5b0f6c27365e3c88e7') +sha256sums=('378b9506211545b9594cf0d38ac8955d9b1cac34eb6b379ae0ec26b84edb65f7') build() { cd "${pkgname}-${pkgver}" diff --git a/packaging/guix/zupt.scm b/packaging/guix/zupt.scm index 9917b53..ec60fa5 100644 --- a/packaging/guix/zupt.scm +++ b/packaging/guix/zupt.scm @@ -72,7 +72,7 @@ "/releases/download/v" %zupt-version "/zupt-" %zupt-version ".tar.gz")) (sha256 - (base32 "1rw87ig3c9vc1xdxx7zg99qsk609a5czjw1h0cnar1z29yp75k3x")))) + (base32 "1xv5vd7bh9pcw2d3fszb6jn1r6sxjp48mlzh9icvji8m4439b2rp")))) (define-public zupt (package diff --git a/packaging/homebrew/zupt.rb b/packaging/homebrew/zupt.rb index 219c4a2..5b2164e 100644 --- a/packaging/homebrew/zupt.rb +++ b/packaging/homebrew/zupt.rb @@ -24,7 +24,7 @@ class Zupt < Formula homepage "https://github.com/cristiancmoises/zupt" url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.8/zupt-5.2.8.tar.gz" version "5.2.8" - sha256 "7dcc72ae4fe287ac2c033070f959510998a9714aef9fde5b0f6c27365e3c88e7" + 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 From 33eb90454d9f1ece028a8dd96df4315aa4457061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 22:12:31 -0300 Subject: [PATCH 17/18] packaging: validate genuine source RPM metadata --- .github/workflows/ci.yml | 6 ++++++ .github/workflows/promote-release.yml | 16 ++++++++++++-- packaging/build-gui-rpm.sh | 31 ++++++++++++++++++++++++--- packaging/build-rpm.sh | 23 ++++++++++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45abe02..a04c599 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -469,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) diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml index e28d6c1..0dc0ec4 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/promote-release.yml @@ -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' diff --git a/packaging/build-gui-rpm.sh b/packaging/build-gui-rpm.sh index 372ae69..cd7e8c8 100755 --- a/packaging/build-gui-rpm.sh +++ b/packaging/build-gui-rpm.sh @@ -124,20 +124,45 @@ 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 '%{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 '%{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 - </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 From f8844f806a85856c793d5c9e0da7dd8e92f82930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Mon, 31 Aug 2026 22:28:57 -0300 Subject: [PATCH 18/18] docs: record audited 5.2.8 release evidence --- AUDIT.md | 26 +++++++++++----- CHANGELOG.md | 10 ++++-- DISTRIBUTION.md | 15 ++++++--- INSTALL.md | 8 ++--- README.md | 60 +++++++++++++++++++++++------------- SECURITY.md | 20 ++++++++---- THREAT_MODEL.md | 20 +++++++----- packaging/build-gui-rpm.sh | 4 +++ packaging/build-rpm.sh | 4 +++ packaging/opensuse/README.md | 30 +++++++++++++++--- 10 files changed, 140 insertions(+), 57 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index d5d6516..76b87d3 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -223,7 +223,7 @@ 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 or proof that an exact-tag 5.2.8 gate passed. +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: @@ -248,12 +248,24 @@ 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 is diagnostic evidence rather than release -approval. The exact 5.2.8 candidate must repeat the required suite. Exact-tag -native Windows/macOS, hosted CI, authenticated OBS service execution, and -release promotion remain pending until recorded otherwise. The pre-tag -openSUSE Tumbleweed job did build source and binary RPMs, pass `rpmlint` without -suppressions, and pass install/round-trip/uninstall checks. +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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a1576d2..3371176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,9 +57,13 @@ unchanged. - 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. -- Require fresh exact-`v5.2.8` source, checksum, hosted CI, native-platform, - package, OBS, and promotion evidence. This entry does not claim those gates - passed, and no v5.2.7 result transfers automatically. +- 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 diff --git a/DISTRIBUTION.md b/DISTRIBUTION.md index 24b0c3b..7cff1ce 100644 --- a/DISTRIBUTION.md +++ b/DISTRIBUTION.md @@ -37,7 +37,13 @@ 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. -Corrective packages and release assets must use `v5.2.8`; never move or +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 @@ -295,9 +301,10 @@ 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 diff --git a/INSTALL.md b/INSTALL.md index 298378d..76a320e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -38,11 +38,11 @@ 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. It still requires fresh exact-version validation. Do not treat any prior -candidate's artifacts or evidence as 5.2.8 packages or validation. +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.8 package set eligible for promotion after each target gate succeeds is -exactly these 13 assets: +The published 5.2.8 package set is exactly these 13 gated assets: | Component | Gated artifacts | |---|---| diff --git a/README.md b/README.md index 0d33c50..905bbad 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ 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. These corrections do not change archive format v1.6, +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 @@ -39,6 +41,16 @@ 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 @@ -55,8 +67,9 @@ 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 move to 5.2.8 and require fresh exact-tag hosted CI, package, -native-platform, source-only, checksum, OBS, and promotion evidence. +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 @@ -177,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 @@ -194,8 +211,8 @@ built and tested is not presented as supported. ## 5.2.8 release artifacts -The 5.2.8 release workflow is defined to produce exactly the following 13 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 @@ -238,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 -. 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. +. The canonical release is +. 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: @@ -410,11 +428,12 @@ not promoted. The immutable v5.2.7 tag was also not promoted: exact-tag run 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 ...