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:
parent
28b2744c67
commit
d8de951b6d
7 changed files with 64 additions and 26 deletions
18
CHANGELOG.md
18
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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) */
|
||||
|
|
|
|||
Loading…
Reference in a new issue