- Corrected LZHP prediction encoding in disk backups to prevent data corruption - Disabled spurious SOLID flag for per-block disk archives - Shared write_enc_header() across all encryption paths to eliminate format mismatches - Enabled solid compression with PQ encryption support - Updated block device restore to use O_SYNC + fsync/sync - Improved Termux/Android host detection for safer builds - Made zupt_w8(), zupt_w16le(), zupt_w64le() non-static for shared use
21 KiB
21 KiB
Changelog
All notable changes to Zupt are documented in this file. Format follows Keep a Changelog.
[2.1.3] — 2026-04-11
Fixed — LZHP Prediction Encoding Missing in Disk Backup (data corruption)
- Root cause:
zupt_disk_backup()LZHP compression path skipped thezupt_predict_encode()step. When byte prediction was active (pred_active=1), it stored the prediction table and wrotecbuf[0] = 0x01, but then compressed the raw block data instead of the prediction-encoded data. On restore,decompress_block()correctly appliedzupt_predict_decode()to the decompressed output, producing corrupted data. Checksum mismatch on block 0 for any block with structured content (ext4 metadata, NTFS headers, partition tables). - Impact: ALL disk backups using LZHP codec (default on CPUs without AVX2) on non-random data were silently corrupted. VaptVupt codec was unaffected (no prediction path). Random/incompressible data was unaffected (prediction benefit < threshold →
pred_active=0). - Fix: Added
zupt_predict_encode(rbuf, transformed, nread, pred)beforezupt_lzh_compress(), matching the correct path inzupt_format.c(lines 557–563). Allocated temporary buffer for prediction-encoded data, freed after compression.
Fixed — Spurious SOLID Flag on Disk Archives
- Disk backup no longer sets
ZUPT_FLAG_SOLIDin the archive header. Disk images are independent per-block archives, not solid streams. The SOLID flag causedzupt_extract_archive()to take the wrong code path if a disk archive was ever parsed by the extract function.
Fixed — Shared Encryption Header (eliminates all format mismatches)
- Extracted
write_enc_header()fromzupt_format.cas a shared non-static function. ALL three encryption write paths —zupt_compress_files(),zupt_compress_solid(), andzupt_disk_backup()— now call the same function. - Solid compress now supports PQ encryption.
zupt_w8(),zupt_w16le(),zupt_w64le()made non-static and declared inzupt.h.
Fixed — Block Device Restore I/O
- Restore uses POSIX raw I/O (
open()+write()loop) withO_SYNCfor block devices,fsync()+sync()before close.
Fixed — Termux/Android Build
- Arch-safety guard uses
$(CC) -dumpmachinefor host detection. Falls back touname -m.
Tests
- 78 total: 70 core + 8 disk (including LZHP+PQ+password on ext4 — the exact failing case). ASAN + UBSan clean.
[2.1.2] — 2026-04-06
Added — Full-Disk Backup/Restore
zupt disk backup— streams a raw block device or file in 4MB chunks, compresses each block with the selected codec (VaptVupt default), detects all-zero (sparse) blocks and stores them with near-zero overhead. Supports password encryption (-p), post-quantum encryption (--pq), compression level override (-l 1-9), and codec selection (--vv,--lzhp). Real-time progress bar with throughput on stderr.zupt disk restore— reads a disk image archive block-by-block, decrypts + decompresses each block, validates per-block XXH64 checksums, and writes sequentially to the target device or file. Rejects wrong passwords/keys immediately on first block failure.ZUPT_FLAG_DISK_IMAGE (1u << 6)— new global flag in the archive header.zupt disk restorevalidates this flag and rejects non-disk archives. Standardzupt extractrejects disk archives with a clear error message.src/zupt_disk.c— 530 lines. Portable device size detection:BLKGETSIZE64on Linux,DKIOCGETBLOCKCOUNTon macOS,lseek(SEEK_END)fallback on FreeBSD/generic. 8-byte-wide sparse block detection.- CLI:
zupt disk backup [OPTIONS] <output.zupt> <device_or_file>/zupt disk restore [OPTIONS] <archive.zupt> <target>
Tests
- 77 tests total: 11 VV unit + 13 NIST/RFC vectors + 22 regression + 14 multi-threaded + 10 post-quantum + 7 disk backup (normal, encrypted, PQ, sparse, LZHP, extreme, wrong-password rejection). ASAN + UBSan clean across all paths.
[2.1.1] — 2026-04-06
Fixed — Multi-Architecture Build
- Stale object files removed from distribution. Previous tarballs shipped pre-compiled x86_64
.ofiles. On aarch64 (Termux, Raspberry Pi, etc.) the linker failed withld.lld: error: src/zupt_xxh.o is incompatible with aarch64linux. All.ofiles now excluded from release tarballs. - Arch-safety guard in Makefile. Detects pre-compiled
.ofiles from a different architecture viafile(1)and auto-removes them before linking. Prevents silent link failures if stale objects are accidentally present. - Termux/Android compatibility. Default compiler changed from
gcctocc(Termux ships clang).-lpthreadskipped on Android/Termux (bionic libc has pthreads built-in, detected viauname -o). sys/syscall.hinclude moved to file top inzupt_crypto.c. Was inside function body (non-standard C, rejected by some compilers).
Fixed — Undefined Behavior
- Keccak ROL64 shift-by-64 UB.
ROL64(x, 0)expanded to(x >> 64)which is undefined behavior in C. The Keccak rotation table hasKECCAK_ROT[0] = 0, triggering this on every Keccak-f[1600] call (SHA3-256, SHA3-512, SHAKE-128, SHAKE-256, ML-KEM-768). Fix:ROL64now returnsxunchanged whenn == 0. Confirmed zero UBSan violations across all PQ paths.
Tests
- 70/70: 11 VV + 13 NIST + 22 regression + 14 MT + 10 PQ. ASAN + UBSan clean (zero violations).
[2.1.0] — 2026-04-05
Upgraded — VaptVupt 1.4.0 Codec
- Cross-block dictionary carry — hash chain now spans block boundaries. The encoder passes absolute positions to
compress_block()so matches can reference data from previous blocks. Large structured files (7MB logs) compress 5.73:1 instead of per-block independent ratios. The decoder accepts cross-block offsets via adst_baseparameter threaded through all decode functions. - Context model decode prefetch —
__builtin_prefetchin the order-1 context ANS decode loop hides L2/L3 latency for the 4MB context tables. Extreme-mode decode throughput improved significantly on cache-constrained systems. - Faster adaptive window trial — greedy depth=4 on 256KB sample instead of full lazy parse on entire first block. Encode speed improved 2.6× with same ratio decisions.
- Zupt integration API — new
vvz_compress/vvz_decompress/vvz_compress_boundwrappers (vaptvupt_api.h/vaptvupt_api.c) simplify codec dispatch with backup-optimized defaults.
Changed
zupt_format.ccompress paths (normal, solid) now usevvz_compress()API instead of rawvv_compress()with manual option setup.zupt_format.cdecompress path now usesvvz_decompress()API.- Version bumped to 2.1.0.
Performance (balanced mode, vs gzip-9)
| File Type | v2.1.0 | gzip-9 | vs gzip |
|---|---|---|---|
| Source code (531K) | 59.5:1 | 51.7:1 | +15% better |
| JSON (232K) | 10.7:1 | 8.8:1 | +21% better |
| XML markup (641K) | 18.1:1 | 14.6:1 | +24% better |
| Long-range (800K) | 5.7:1 | 1.4:1 | +307% better |
| Logs 7MB (7.5MB) | 5.7:1 | 7.5:1 | gap 24% |
Tests
- 70/70: 11 VV + 13 NIST + 22 regression + 14 MT + 10 PQ. ASAN clean.
[2.0.0] — 2026-04-05
Added — VaptVupt 1.1.0 Codec Integration
- VaptVupt codec integrated as
0x0010— LZ77 + tANS entropy + AVX2 SIMD decode. - Three compression modes: Ultra-Fast (greedy), Balanced (lazy + 4-way ANS), Extreme (lazy-2 + order-1 context).
- Rep-match offset coding — 3 recent offsets tracked (like zstd), saves 10–15 bits per repeated match.
- Adaptive window selection — trial-compresses at wlog=16 vs wlog=20, picks larger window only if ≥3% improvement.
- CLI flags
--vv/--vaptvuptto select VaptVupt codec. - CLI flag
--lzhpto explicitly select Zupt-LZHP codec. - VaptVupt source files with dual MIT + Apache-2.0 headers.
vv_xxh64aliased tozupt_xxh64via macro (no duplicate symbol).- Wired into compress (single-thread, multi-thread, solid) and decompress paths.
- 11 VaptVupt unit tests + 6 regression tests (T13–T18).
Added — Auto Codec Detection
ZUPT_CODEC_AUTO— hardware-aware default codec selection:- x86_64 with AVX2: VaptVupt (inline AVX2 SIMD decode, ~2–3 GB/s).
- aarch64 with NEON: VaptVupt (NEON SIMD decode path).
- All other architectures: Zupt-LZHP (scalar decoder, no SIMD dependency).
zupt_resolve_auto_codec()checks compile-time flags (__AVX2__,__ARM_NEON) and runtime CPUID.- Decompression is universal — any archive extracts on any architecture regardless of codec.
- Users can override with
--vv(force VaptVupt) or--lzhp(force LZHP).
Fixed — Jasmin Assembly
- AES-NI stack offset bug fixed: replaced
stack u128[15]with 15 individualstack u128variables to avoid jasminc byte-offset indexing. Round keys now at correct 16-byte aligned offsets. - X25519 fe_cswap wired: Jasmin swaps first 4 limbs (32 bytes), C handles 5th limb.
- All 5 Jasmin functions now active:
zupt_mac_verify_ct,zupt_ct_select_32,zupt_fe_cswap,zupt_aes256_blk,zupt_aes256_ctr4. - SIGILL fix: AVX detection with OSXSAVE/XCR0 check. The Jasmin AES assembly uses VEX-encoded instructions (
vaesenc,vmovdqu,vpxor) which require AVX — not just AES-NI. Previous dispatch only checkedhas_aesni, causing SIGILL on CPUs with AES-NI but without AVX or without OS XSAVE support. Now checkshas_aesni && has_avxwith proper XGETBV XCR0 validation. - Added
has_avxfield tozupt_cpu_features_twith correct detection: CPUID ECX[28] (AVX) + ECX[27] (OSXSAVE) + XCR0 bits 1+2.
Fixed — VaptVupt Codec Bugs
copy_match_scalaroverlap corruption (vv_simd.c): 8-byte bulk copy was used for offsets 4–7, where source overlaps destination by more than the copy stride. Thememcpyread-then-write semantics don't correctly replicate the overlapping pattern. Fix: byte-by-byte for offsets < 8 (was < 4). This caused silent data corruption on inputs with short-offset matches near the output buffer tail.vva_encode_sequencesheap overflow (vv_ans.c): litlen varint buffer allocated asnseq * 5 + 1bytes, but individual literal lengths in solid mode can reach 1 MB, requiring up toceil(litlen/255) + 1bytes per varint. Fix: compute exact bound from actual litlen values. This caused heap corruption and abort (malloc(): invalid size) on large solid-mode archives.
Added — ACSL Formal Annotations
- 19 security-critical functions annotated with complete
requires/ensures/assignsACSL contracts. - Covers: SHA-256, HMAC, PBKDF2, AES-256-CTR, key derivation, encrypt/decrypt, hybrid KEM, SHA3, SHAKE, ML-KEM-768, X25519, secure_wipe.
- Target:
frama-c -wp -wp-rte -wp-model Typed+Cast.
Added — Security Hardening
- mlock() for key material — prevents swap to disk (Linux/BSD/Windows).
- Buffer canaries on
zupt_keyring_t—canary_head/canary_taildetect overflow, abort on corruption. - Always-decrypt timing mitigation —
zupt_decrypt_buffer()always decrypts even on MAC failure (then wipes), preventing timing oracle. - AFL++ fuzzing harnesses —
fuzz_decompress.c(archive format) andfuzz_vv_decompress.c(VaptVupt codec).make fuzz-build.
Added — Performance
- AES-NI 4-block pipeline —
zupt_aes256_ctr4interleaves 4 counter blocks per AES round for pipeline saturation. - Multi-threaded decompression — non-solid extract dispatches blocks to N worker threads via
zpar_ctx_tinfrastructure. - Adaptive compression —
zupt_detect_filetype()identifies 16+ file formats by magic bytes; already-compressed files get STORE. - Benchmark harness —
zupt bench --comparetests all codecs + auto-detects gzip/lz4/zstd.
Changed — Multi-Architecture Support
- Makefile rewritten for full multi-arch builds: x86_64, aarch64, armhf, ppc64le, s390x, riscv64.
- Jasmin CT assembly: x86_64 only (C fallback on all others).
- AVX2 SIMD decode: x86_64 only. NEON decode: aarch64. Scalar fallback: everywhere.
LDFLAGShonored on link line for PIE linking (-pie -Wl,-z,relro,-z,now).LDLIBSplaced after objects (correct rpmlint/OBS link order).DESTDIRsupport for staged packaging installs.- Man page
doc/zupt.1compressed and installed to$(MANDIR)/man1/zupt.1.gz. - Verbose build with
make V=1. make helpshows available targets and detected architecture capabilities.- AVX2 detection gates
has_avx2onhas_avx(OS XSAVE must be enabled).
Tests
- 70 tests total: 11 VV unit + 13 NIST/RFC vectors + 22 regression + 14 multi-threaded + 10 post-quantum.
- ASAN clean across all modes (normal, encrypted, solid, threaded, PQ).
- All 5 Jasmin symbols linked (confirmed via
nm).
[1.5.5] — 2026-04-01
Fixed — Makefile & Packaging
- Added man page installation (
doc/zupt.1→zupt.1.gzin$(MAN1DIR)). - Enabled verbose build output with
V=1support. - Fixed Makefile to honor
LDFLAGSand support PIE linking. - Improved rpmlint compliance for OBS/openSUSE packaging.
- Jasmin assembly gated to x86_64 only (
ifeq ($(ARCH),x86_64)) — clean build on aarch64/armhf/ppc64le. - Object files excluded from distribution tarballs.
install.shconvenience installer restored.
[1.5.0] — 2026-03-28
Added — Jasmin Assembly Integration (Sprint 1)
zupt_mac_verify_ctJasmin assembly linked intozupt_decrypt_buffer(). Replaces the C XOR accumulation loop for HMAC-SHA256 comparison. 4×u64 unrolled XOR, proven constant-time by Jasmin type system. Symbol confirmed active vianm:T zupt_mac_verify_ct.zupt_ct_select_32Jasmin assembly linked intozupt_mlkem768_decaps(). Replaces the Ccmov()function for Fujisaki-Okamoto implicit rejection key selection. 4×u64 masked select, proven constant-time. Symbol confirmed active vianm:T zupt_ct_select_32.include/zupt_jasmin.h— extern declarations for all Jasmin functions with ABI documentation.#ifdef ZUPT_USE_JASMINdispatch guards inzupt_crypto.candzupt_mlkem.cwith clean C fallback.- Makefile auto-detects
jasmin/*.sfiles, assembles to.o, links into binary, sets-DZUPT_USE_JASMIN.
Not Wired (documented, requires upstream fixes)
zupt_fe_cswap(X25519): Jasmin uses 4×u64 limbs, C uses 5×u51-bit — incompatible layout. C fallback active.zupt_aes256_blk(AES-NI): Assembly has stack offset bug ([rsp+1]instead of[rsp+16]). C table-based AES active.
Changed
- Version: 1.4.0 → 1.5.0.
cmov()inzupt_mlkem.cguarded with#ifndef ZUPT_USE_JASMIN.- MAC comparison return type widened from
uint8_ttouint64_tto match Jasmin signature.
Security
- 53/53 tests pass with Jasmin linked. 13/13 NIST vectors. ASAN clean. Zero warnings.
[1.4.0] — 2026-03-28
Fixed — Jasmin Parse Errors (jasminc 2026.03.0)
All 4 .jazz files rewritten to fix compilation errors:
zupt_mac_verify.jazz:diff |= a ^ b— compound XOR+OR not a single x86-64 op. Split intotmp = a; tmp ^= b; diff |= tmp.zupt_mlkem_select.jazz:out.[i] = (8u)sel—reg ptris read-only. Changed toreg u64 out_ptrwith raw pointer writes.zupt_x25519_fe.jazz:a.[i] = ta ^ diff— same const-ptr write. Changed toreg u64 a_ptr.zupt_aes_ctr.jazz: Memory syntax(u128)[ptr]→u128[ptr]→[ptr]— all wrong. Correct:key.[0]viareg ptr u128[N]for reads;stack u128[15]for writes; bare[ptr + 0]for u64-width.- Uninitialized variable warning:
#VPXOR(zero, zero)→wipe = rk.[z]; wipe ^= wipe; rk.[z] = wipe.
Changed
- Removed all
-CTflag references (does not exist in jasminc 2026.03.0). - CT enforced by Jasmin type system during normal compilation.
- Safety:
jasminc -arch x86-64 -checksafety. - All compound expressions split into separate register operations.
- All output parameters changed from
reg ptrtoreg u64raw pointers. - Byte-level access avoided: 4×u64 instead of 32×u8.
[1.3.0] — 2026-03-28
Added
include/zupt_acsl.h— ACSL predicates:ValidBuffer,ValidWriteBuffer,Separated2,KeyWiped,ValidKey.SECURITY_REVIEW.md— 8-section security review with per-function CT analysis table.jasmin/README.jazz.md— build instructions, CT verification explanation, error history.
Fixed
- First round of Jasmin syntax fixes (partial — completed in v1.4.0).
[1.2.0] — 2026-03-28
Added — CPUID Runtime Detection
src/zupt_cpuid.c+include/zupt_cpuid.h— runtime detection of AES-NI, PCLMUL, AVX2, SSE4.1 via CPUID. Supports GCC/Clang, MSVC, and inline assembly fallback.zupt_detect_cpu()called at program start. Globalzupt_cpustruct for dispatch.
Added — Jasmin Source Files (initial)
- 4
.jazzfiles created for AES-CTR, MAC verify, X25519, ML-KEM select. - Note: All had parse errors — fixed in v1.3.0–v1.4.0.
[1.1.0] — 2026-03-28
Fixed — Critical Cryptographic Bugs
- X25519 Montgomery formula (
zupt_x25519.c):AA + 121666*E→BB + 121666*E. The doubling formula was algebraically wrong. DH exchanges produced consistently wrong but matching values, so PQ archives worked. RFC 7748 test vectors exposed the bug. All X25519 in v0.7.0–v1.0.0 was not interoperable with any other implementation. - Dead
match_cost()(zupt_lzh.c): Defined but never called. Removed (Clang-Wunused-function). - ML-KEM
const polyvecwarnings: C11 doesn't support multi-level const for arrays-of-arrays. Removedconst(matches pqcrystals reference). __int128pedantic warning: Wrapped with#pragma GCC diagnostic push/pop.
Added
tests/test_vectors.c— 13 NIST/RFC test vectors: SHA-256 (3), HMAC-SHA256 (2), SHA3-256 (2), SHAKE-128 (1), X25519 (2), ML-KEM-768 (2), XXH64 (1).
Changed
- Zero warnings on GCC + Clang with
-Wall -Wextra -Wpedantic.
[1.0.0] — 2026-03-21
Stable Release
- Archive format frozen at v1.4.
FORMAT_STABLEflag set. Future changes require v2.0. - Documentation: FORMAT.md, AUDIT.md, FUZZING.md, SECURITY.md.
- License: GPL-3.0 → MIT.
Fixed — ML-KEM-768 Bugs (5 critical)
poly_basemulOOB:zetas[64+i]accessed past 128-entry array. Fixed to 64 iterations.- Missing
poly_tomont()in keygen: Public key in wrong Montgomery domain. - Inverted
cmovin FO decaps: C integer promotion caused rejection key selected on valid ciphertext. Fixed:(-(int64_t)diff) >> 63. inv_nttwrong zetas table: Separate wrong table. Fixed: reusezetas[], k counts 127→0.- PQ nonce mismatch: Encrypt/decrypt independently generated nonces. Fixed: store in header.
Added — Post-Quantum Hybrid Encryption (v0.7.0)
- ML-KEM-768 (FIPS 203): ~658 lines pure C11. NTT, Barrett/Montgomery, CBD, FO transform.
- X25519 (RFC 7748): ~270 lines. Montgomery ladder, constant-time fe_cswap.
- Keccak-f[1600]: SHA3-256/512, SHAKE-128/256. ~215 lines.
- Hybrid KEM:
SHA3-512(ml_ss XOR x25519_ss ‖ transcript). Secure if EITHER holds. zupt keygensubcommand,--pq <keyfile>flag.- Key file format: ZKEY magic, ML-KEM pk(1184B) + X25519 pk(32B) + optional sk + XXH64.
- 10-test PQ suite.
- Format v1.3 → v1.4 with
enc_typedispatch byte.
Added — Multi-Threaded Compression (v0.6.0)
-t <N>flag. Batch-parallel pipeline. 14-test MT suite.- Solid mode falls back to N=1 (shared LZ context).
Added — Security Hardening (v0.5.1)
- 16 bug fixes: Huffman Kraft violation (data corruption), heap-buffer-overflows, removed
rand()fallback, constant-time MAC, secure key wipe, LE serialization, realloc checks, empty file checksum.
Core Features (v0.1.0–v0.4.0)
- LZ77+Huffman compression (1MB window, near-optimal parsing).
- AES-256-CTR + HMAC-SHA256 authenticated encryption.
- PBKDF2-SHA256 (600,000 iterations).
- Per-block XXH64 integrity. Recursive directory backup. Solid mode.
Summary
| Version | Key Change | Tests |
|---|---|---|
| 2.1.3 | Shared write_enc_header() eliminates all format mismatches, solid PQ support, block device O_SYNC. Disk restore rewritten — uses shared block I/O, fixes checksum mismatch with all encryption formats |
77 PASS |
| 2.1.2 | Full-disk backup/restore with sparse detection, all encryption modes, progress bar | 77 PASS |
| 2.1.1 | Termux/Android build fix, arch-safety guard, Keccak UB fix, no stale .o in tarballs | 70 PASS |
| 2.1.0 | VaptVupt 1.4.0: cross-block dictionary, context prefetch, faster adaptive window, integration API | 70 PASS |
| 2.0.0 | VaptVupt 1.1.0 codec, auto codec detection, all 5 Jasmin wired, AVX SIGILL fix, multi-arch, copy_match fix, litlen overflow fix | 70 PASS |
| 1.5.5 | Man page install, V=1 verbose, LDFLAGS/PIE, rpmlint, multi-arch Makefile | 53+13 PASS |
| 1.5.0 | Jasmin assembly linked: MAC verify + ML-KEM select active in binary | 53+13 PASS |
| 1.4.0 | All 4 .jazz files compile on jasminc 2026.03.0 |
53+13 PASS |
| 1.3.0 | ACSL predicates, security review, partial Jasmin fixes | 53+13 PASS |
| 1.2.0 | CPUID detection, Jasmin source files (with errors) | 53+13 PASS |
| 1.1.0 | X25519 BB formula fix, 13 NIST/RFC test vectors | 53+13 PASS |
| 1.0.0 | Format frozen v1.4, ML-KEM bugs fixed, MIT license | 40 PASS |
© 2026 Cristian Cezar Moisés — MIT License