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.

This commit is contained in:
Cristian Cezar Moisés 2026-04-11 17:24:32 -03:00
commit a4aab7a1b6
7 changed files with 64 additions and 26 deletions

View file

@ -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 |

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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);

View file

@ -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) */