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
288df8ea0b
commit
a4aab7a1b6
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
|
## [2.1.3] — 2026-04-11
|
||||||
|
|
||||||
### Fixed — LZHP Prediction Encoding Missing in Disk Backup (data corruption)
|
### 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 |
|
| 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.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.2** | Full-disk backup/restore with sparse detection, all encryption modes, progress bar | 77 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.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.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 |
|
| **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.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.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.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.
|
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.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.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** | **✅** | **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
|
## Planned
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
**Be free to report vulnerabilities. For high-risk send an email.**
|
**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.
|
Include: description, reproduction steps, impact assessment.
|
||||||
Response within 48 hours. Fix within 30 days for critical issues.
|
Response within 48 hours. Fix within 30 days for critical issues.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
#define zupt_mkdir(p) mkdir(p, 0755)
|
#define zupt_mkdir(p) mkdir(p, 0755)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define ZUPT_VERSION_STRING "2.1.3"
|
#define ZUPT_VERSION_STRING "2.1.4"
|
||||||
#define ZUPT_FORMAT_MAJOR 1
|
#define ZUPT_FORMAT_MAJOR 1
|
||||||
#define ZUPT_FORMAT_MINOR 4
|
#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
|
* Copyright (c) 2026 Cristian Cezar Moisés — MIT License
|
||||||
*
|
*
|
||||||
* Reads a raw block device or file, compresses in streaming chunks,
|
* 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);
|
CloseHandle(h);
|
||||||
return -1;
|
return -1;
|
||||||
#else
|
#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;
|
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)) {
|
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__
|
#ifdef __linux__
|
||||||
if (S_ISBLK(st.st_mode)) {
|
if (S_ISBLK(st.st_mode)) {
|
||||||
int fd = open(path, O_RDONLY);
|
|
||||||
if (fd < 0) return -1;
|
|
||||||
uint64_t sz = 0;
|
uint64_t sz = 0;
|
||||||
if (ioctl(fd, BLKGETSIZE64, &sz) == 0) {
|
if (ioctl(fd, BLKGETSIZE64, &sz) == 0) {
|
||||||
close(fd);
|
close(fd);
|
||||||
|
|
@ -98,8 +103,6 @@ static int64_t get_device_size(const char *path) {
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
if (S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode)) {
|
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;
|
uint64_t bc = 0, bs = 0;
|
||||||
if (ioctl(fd, DKIOCGETBLOCKCOUNT, &bc) == 0 &&
|
if (ioctl(fd, DKIOCGETBLOCKCOUNT, &bc) == 0 &&
|
||||||
ioctl(fd, DKIOCGETBLOCKSIZE, &bs) == 0) {
|
ioctl(fd, DKIOCGETBLOCKSIZE, &bs) == 0) {
|
||||||
|
|
@ -112,8 +115,6 @@ static int64_t get_device_size(const char *path) {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* FreeBSD/generic: try seeking to end */
|
/* 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);
|
off_t end = lseek(fd, 0, SEEK_END);
|
||||||
close(fd);
|
close(fd);
|
||||||
return (end >= 0) ? (int64_t)end : -1;
|
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
|
* Block devices require raw POSIX I/O (open/write) because stdio
|
||||||
* buffering can cause misaligned or partial writes that corrupt data.
|
* buffering can cause misaligned or partial writes that corrupt data.
|
||||||
* O_SYNC ensures each write is flushed to the device before returning.
|
* 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
|
#ifdef _WIN32
|
||||||
FILE *tgt = fopen(target_path, "wb");
|
FILE *tgt = fopen(target_path, "wb");
|
||||||
if (!tgt) {
|
if (!tgt) {
|
||||||
|
|
@ -590,17 +594,13 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
|
||||||
return ZUPT_ERR_IO;
|
return ZUPT_ERR_IO;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
struct stat tgt_st;
|
|
||||||
int tgt_fd;
|
int tgt_fd;
|
||||||
int is_block_dev = 0;
|
int is_block_dev = 0;
|
||||||
|
|
||||||
/* For block/char devices: O_WRONLY | O_SYNC (no truncate, sync writes).
|
/* Open the target — try without O_CREAT first (for existing devices/files),
|
||||||
* For regular files: O_WRONLY | O_CREAT | O_TRUNC. */
|
* fall back to O_CREAT | O_TRUNC for new files. */
|
||||||
if (stat(target_path, &tgt_st) == 0 &&
|
tgt_fd = open(target_path, O_WRONLY);
|
||||||
(S_ISBLK(tgt_st.st_mode) || S_ISCHR(tgt_st.st_mode))) {
|
if (tgt_fd < 0) {
|
||||||
tgt_fd = open(target_path, O_WRONLY | O_SYNC);
|
|
||||||
is_block_dev = 1;
|
|
||||||
} else {
|
|
||||||
tgt_fd = open(target_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
tgt_fd = open(target_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
}
|
}
|
||||||
if (tgt_fd < 0) {
|
if (tgt_fd < 0) {
|
||||||
|
|
@ -609,6 +609,23 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return ZUPT_ERR_IO;
|
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
|
#endif
|
||||||
|
|
||||||
fprintf(stderr, " Restoring disk image to: %s\n", target_path);
|
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_mul(x2, x2, z2);
|
||||||
fe_tobytes(out, x2);
|
fe_tobytes(out, x2);
|
||||||
|
|
||||||
/* Wipe stack */
|
/* Wipe clamped scalar from stack.
|
||||||
memset(e, 0, 32);
|
* 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) */
|
/* FRAMA-C: X25519 with standard basepoint (u=9) */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue