From a4aab7a1b6840c2baf184968574b239902b9c87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Sat, 11 Apr 2026 17:24:32 -0300 Subject: [PATCH] Release: 2.1.4 - Resolved high-severity vulnerabilities by removing TOCTOU filesystem races via fd-first open()/fstat() patterns and enforcing non-optimizable secure memory zeroization for cryptographic material. --- CHANGELOG.md | 18 ++++++++++++++-- README.md | 5 ++++- ROADMAP.md | 3 ++- SECURITY.md | 2 +- include/zupt.h | 2 +- src/zupt_disk.c | 53 +++++++++++++++++++++++++++++++---------------- src/zupt_x25519.c | 7 +++++-- 7 files changed, 64 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec49d9..e1cd07c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). --- +## [v2.1.4] — 2026-04-11 + +### Fixed — CodeQL Security Alerts (4/4 resolved) + +- **Alert #1 & #2: TOCTOU filesystem race in `get_device_size()`** (High). The function called `stat(path)` to classify the file type, then `open(path)` to read it — between those two calls an attacker could swap the path to a different file. Fix: open the fd first with `open()`, then classify via `fstat(fd)`. The fd is stable and cannot be swapped. +- **Alert #3: Dead-store `memset` in `zupt_x25519()`** (High). The `memset(e, 0, 32)` call to wipe the clamped scalar was the last use of `e` before the function returned, so the compiler could legally optimize it away (and some do at `-O2`). Fix: volatile pointer loop (`volatile uint8_t *ve = e; for(...) ve[i] = 0;`) which the compiler must emit. +- **Alert #4: TOCTOU filesystem race in `zupt_disk_restore()`** (High). Restore called `stat(target_path)` to check for block devices, then `open(target_path)` — same race window as alerts #1/#2. Fix: open fd first, then `fstat(fd)` to classify, then `fcntl(fd, F_SETFL, O_SYNC)` for block devices. + +### Tests +- **78 total:** 70 core + 8 disk. ASAN + UBSan clean. + +--- + ## [2.1.3] — 2026-04-11 ### Fixed — LZHP Prediction Encoding Missing in Disk Backup (data corruption) @@ -299,8 +312,9 @@ All 4 `.jazz` files rewritten to fix compilation errors: | 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.4** |Resolved CodeQL-reported high-severity vulnerabilities by removing TOCTOU filesystem races via fd-first open()/fstat() patterns and enforcing non-optimizable secure memory zeroization for cryptographic material.| 78 PASS | +| **2.1.3** | Disk restore rewritten — uses shared block I/O, fixes checksum mismatch with all encryption formats | 78 PASS | +| **2.1.2** | Full-disk backup/restore with sparse detection, all encryption modes, progress bar. Shared `write_enc_header()` eliminates all format mismatches, solid PQ support, block device O_SYNC | 78 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 | diff --git a/README.md b/README.md index d5bcd1b..6ab455b 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,10 @@ All codecs are forward-compatible: archives created with any codec can be read b | v2.1.0 | VaptVupt 1.4.0: cross-block dictionary carry, context decode prefetch, faster adaptive window (2.6× encode), integration API | | v2.1.1 | Termux/Android build fix, arch-safety guard, Keccak ROL64 UB fix, zero UBSan violations | | v2.1.2 | Full-disk backup/restore (`zupt disk`), sparse detection, all encryption modes, progress bar | -| **v2.1.3** | **Disk restore fix (POSIX raw I/O + O_SYNC for block devices, shared decompress_block), Termux build fix (CC -dumpmachine arch detection). LZHP prediction encoding fix (data corruption on structured data), shared write_enc_header, SOLID flag removed from disk, 78 tests** | +| v2.1.3 | Disk restore fix (POSIX raw I/O + O_SYNC for block devices, shared decompress_block), Termux build fix (CC -dumpmachine arch detection). LZHP prediction encoding fix (data corruption on structured data), shared write_enc_header, SOLID flag removed from disk, 78 tests | +| **v2.1.4** | **Resolved high-severity vulnerabilities by removing TOCTOU filesystem races via fd-first open()/fstat() patterns and enforcing non-optimizable secure memory zeroization for cryptographic material.**| + + See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes. diff --git a/ROADMAP.md b/ROADMAP.md index 8680798..ce24e2a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -23,7 +23,8 @@ | **v2.1.1** | **✅** | **Termux/Android build fix, arch-safety guard, Keccak UB fix, no stale .o in tarballs** | | **v2.1.2** | **✅** | **Full-disk backup/restore with sparse detection, all encryption modes, progress bar, 77 tests** | | **v2.1.3** | **✅** | **Disk restore rewritten — shared block I/O, fixes checksum mismatch on encrypted/PQ archives** | -| **v2.1.3** | **✅ Current** | **LZHP prediction encoding fix, shared write_enc_header, SOLID flag removed from disk, block device O_SYNC, 78 tests** | +| **v2.1.3** | **✅** | **LZHP prediction encoding fix, shared write_enc_header, SOLID flag removed from disk, 78 tests** | +| **v2.1.4** | **✅ Current** | **CodeQL: 4 security fixes — TOCTOU races (fstat on fd), X25519 scalar wipe (volatile), 78 tests** | ## Planned diff --git a/SECURITY.md b/SECURITY.md index 9f78fe1..669f4ca 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ **Be free to report vulnerabilities. For high-risk send an email.** -Email: **ethicalhacker@riseup.net** +Email: **zupt@riseup.net** Include: description, reproduction steps, impact assessment. Response within 48 hours. Fix within 30 days for critical issues. diff --git a/include/zupt.h b/include/zupt.h index 7c72753..8416e0a 100644 --- a/include/zupt.h +++ b/include/zupt.h @@ -30,7 +30,7 @@ #define zupt_mkdir(p) mkdir(p, 0755) #endif -#define ZUPT_VERSION_STRING "2.1.3" +#define ZUPT_VERSION_STRING "2.1.4" #define ZUPT_FORMAT_MAJOR 1 #define ZUPT_FORMAT_MINOR 4 diff --git a/src/zupt_disk.c b/src/zupt_disk.c index c008801..f4e945a 100644 --- a/src/zupt_disk.c +++ b/src/zupt_disk.c @@ -1,5 +1,5 @@ /* - * Zupt v2.1.3 — Full-Disk Backup/Restore + * Zupt v2.1.4 — Full-Disk Backup/Restore * Copyright (c) 2026 Cristian Cezar Moisés — MIT License * * Reads a raw block device or file, compresses in streaming chunks, @@ -75,17 +75,22 @@ static int64_t get_device_size(const char *path) { CloseHandle(h); return -1; #else + /* Open first, then fstat on the fd — eliminates TOCTOU race between + * stat() and open() where the path could change between the two calls. */ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + struct stat st; - if (stat(path, &st) != 0) return -1; + if (fstat(fd, &st) != 0) { close(fd); return -1; } if (S_ISREG(st.st_mode)) { - return (int64_t)st.st_size; + int64_t sz = (int64_t)st.st_size; + close(fd); + return sz; } #ifdef __linux__ if (S_ISBLK(st.st_mode)) { - int fd = open(path, O_RDONLY); - if (fd < 0) return -1; uint64_t sz = 0; if (ioctl(fd, BLKGETSIZE64, &sz) == 0) { close(fd); @@ -98,8 +103,6 @@ static int64_t get_device_size(const char *path) { #ifdef __APPLE__ if (S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode)) { - int fd = open(path, O_RDONLY); - if (fd < 0) return -1; uint64_t bc = 0, bs = 0; if (ioctl(fd, DKIOCGETBLOCKCOUNT, &bc) == 0 && ioctl(fd, DKIOCGETBLOCKSIZE, &bs) == 0) { @@ -112,8 +115,6 @@ static int64_t get_device_size(const char *path) { #endif /* FreeBSD/generic: try seeking to end */ - int fd = open(path, O_RDONLY); - if (fd < 0) return -1; off_t end = lseek(fd, 0, SEEK_END); close(fd); return (end >= 0) ? (int64_t)end : -1; @@ -580,7 +581,10 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path * Block devices require raw POSIX I/O (open/write) because stdio * buffering can cause misaligned or partial writes that corrupt data. * O_SYNC ensures each write is flushed to the device before returning. - * For loop devices, this ensures data reaches the backing file. */ + * For loop devices, this ensures data reaches the backing file. + * + * To avoid TOCTOU races (stat then open on a path that could change), + * we open the fd first, then fstat on the fd to classify it. */ #ifdef _WIN32 FILE *tgt = fopen(target_path, "wb"); if (!tgt) { @@ -590,17 +594,13 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path return ZUPT_ERR_IO; } #else - struct stat tgt_st; int tgt_fd; int is_block_dev = 0; - /* For block/char devices: O_WRONLY | O_SYNC (no truncate, sync writes). - * For regular files: O_WRONLY | O_CREAT | O_TRUNC. */ - if (stat(target_path, &tgt_st) == 0 && - (S_ISBLK(tgt_st.st_mode) || S_ISCHR(tgt_st.st_mode))) { - tgt_fd = open(target_path, O_WRONLY | O_SYNC); - is_block_dev = 1; - } else { + /* Open the target — try without O_CREAT first (for existing devices/files), + * fall back to O_CREAT | O_TRUNC for new files. */ + tgt_fd = open(target_path, O_WRONLY); + if (tgt_fd < 0) { tgt_fd = open(target_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); } if (tgt_fd < 0) { @@ -609,6 +609,23 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path fclose(f); return ZUPT_ERR_IO; } + + /* Classify the fd (not the path) to avoid TOCTOU */ + { + struct stat tgt_st; + if (fstat(tgt_fd, &tgt_st) == 0 && + (S_ISBLK(tgt_st.st_mode) || S_ISCHR(tgt_st.st_mode))) { + is_block_dev = 1; + /* Enable synchronous I/O for block devices */ + int fl = fcntl(tgt_fd, F_GETFL); + if (fl >= 0) fcntl(tgt_fd, F_SETFL, fl | O_SYNC); + } else if (fstat(tgt_fd, &tgt_st) == 0 && S_ISREG(tgt_st.st_mode)) { + /* Regular file — truncate if we opened without O_TRUNC */ + if (ftruncate(tgt_fd, 0) != 0) { + /* Non-fatal: file may already be empty */ + } + } + } #endif fprintf(stderr, " Restoring disk image to: %s\n", target_path); diff --git a/src/zupt_x25519.c b/src/zupt_x25519.c index b96b277..f110f85 100644 --- a/src/zupt_x25519.c +++ b/src/zupt_x25519.c @@ -314,8 +314,11 @@ void zupt_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[ fe_mul(x2, x2, z2); fe_tobytes(out, x2); - /* Wipe stack */ - memset(e, 0, 32); + /* Wipe clamped scalar from stack. + * Use volatile pointer to resist dead-store elimination (CodeQL alert #3). + * Cannot use zupt_secure_wipe() here because this file does not include zupt.h. */ + volatile uint8_t *ve = (volatile uint8_t *)e; + for (int i = 0; i < 32; i++) ve[i] = 0; } /* FRAMA-C: X25519 with standard basepoint (u=9) */