v4.0.0: codec 2.60.4 security release, --pq-box sealed-box mode, F-16 fix
Some checks failed
CI / build-and-test (clang) (push) Has been cancelled
CI / build-and-test (gcc) (push) Has been cancelled
CI / strict-warnings (clang, -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -O2 -std=c11 -Werror) (push) Has been cancelled
CI / strict-warnings (gcc, -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -Wformat-security -Wlogical-op -Wjump-misses-init -Wdouble-promotion -O2 -std=c11 -Werror) (push) Has been cancelled
CI / sanitizers (push) Has been cancelled
CI / pie-hardening (push) Has been cancelled
CI / cross-aarch64 (push) Has been cancelled
CI / dist-reproducibility (push) Has been cancelled
CI / packaging-syntax (push) Has been cancelled
CI / release (push) Has been cancelled
Some checks failed
CI / build-and-test (clang) (push) Has been cancelled
CI / build-and-test (gcc) (push) Has been cancelled
CI / strict-warnings (clang, -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -O2 -std=c11 -Werror) (push) Has been cancelled
CI / strict-warnings (gcc, -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -Wformat-security -Wlogical-op -Wjump-misses-init -Wdouble-promotion -O2 -std=c11 -Werror) (push) Has been cancelled
CI / sanitizers (push) Has been cancelled
CI / pie-hardening (push) Has been cancelled
CI / cross-aarch64 (push) Has been cancelled
CI / dist-reproducibility (push) Has been cancelled
CI / packaging-syntax (push) Has been cancelled
CI / release (push) Has been cancelled
Major release. Highlights: - Codec: vendored VaptVupt codec moves to canonical 2.60.4 security release. Fixes a high-severity OOB heap write in the AVX2 decode fast path (reachable on a valid stream sized to exactly content_size, both tail variants). Brings CBMC-formally-verified BCJ filters with automatic ELF/PE/Mach-O detection. Compressed output stays byte-identical (ratio gate Δ 0.00%); wire format unchanged at v1.6. - New --pq-box sealed-box recipient mode (vendored libpqvaptvupt 0.6.0): ML-KEM-768 + X25519 combined via HKDF-SHA256 with domain separation, AES-256-CTR + HMAC-SHA256 EtM. Legacy --pq and --pq-sdk stay readable. - F-16: discloses and fixes a pre-existing data-loss defect in the <= 3.8.0 in-tree BCJ encoder. Full back-compat matrix decodes byte-exact under 4.0.0; every readable pre-4.0 archive remains readable. Repository hygiene: - Sync full 4.0.0 source tree (codec, crypto, SDK, GUI, packaging, tests). - Remove internal scratch files (PROMPT.md, FORMAL_AUDIT_PROMPT.md) and superseded version-specific docs (INTEGRATION_PROTOCOL_2.60.4.md, docs/FINDINGS-2.x.md) and a stray test binary. - Refresh README download/install section to real 4.0.0 release assets; bump version badge to 4.0.0. - Add .gitignore for build outputs (keeps vendored prebuilt libraries).
This commit is contained in:
parent
7619c4c577
commit
544a2cd647
98 changed files with 15615 additions and 1397 deletions
149
src/vv_ans.c
149
src/vv_ans.c
|
|
@ -192,13 +192,27 @@ static void build_dec(const uint16_t norm[NSYM], const uint8_t sp[ANS_L],
|
|||
int flg = ilog2(f);
|
||||
int nb_max = ANS_LOG - flg;
|
||||
int low_count = (1 << (flg + 1)) - (int)f;
|
||||
/* On a VALID normalized table, f ∈ [1, ANS_L) here (f==0 and
|
||||
* f==ANS_L are handled above), so flg ≤ ANS_LOG-1 and nb_max ≥ 1,
|
||||
* and the shifts below are well-defined. A CORRUPT stream can
|
||||
* carry f > ANS_L (read_hdr_v2 does not range-check the wire
|
||||
* values), giving flg ≥ ANS_LOG and nb_max ≤ 0 — i.e. a negative
|
||||
* shift, which is C undefined behaviour (found under UBSan on
|
||||
* bit-flipped input: "shift exponent is negative"). Clamp nb_max
|
||||
* to ≥ 0 and both shift amounts to ≥ 0. For valid tables
|
||||
* (nb_max ≥ 1) this is a no-op, so valid-stream decode output is
|
||||
* byte-for-byte unchanged; for corrupt tables it merely produces
|
||||
* a defined (still-wrong) baseline that the downstream
|
||||
* sequence/offset bounds checks reject. */
|
||||
if (nb_max < 0) nb_max = 0;
|
||||
if (k < low_count) {
|
||||
dec[x].nbits = (uint8_t)nb_max;
|
||||
dec[x].baseline = (uint16_t)((uint32_t)k << nb_max);
|
||||
} else {
|
||||
dec[x].nbits = (uint8_t)(nb_max - 1);
|
||||
int sh = (nb_max > 0) ? (nb_max - 1) : 0;
|
||||
dec[x].nbits = (uint8_t)sh;
|
||||
dec[x].baseline = (uint16_t)(((uint32_t)low_count << nb_max)
|
||||
+ ((uint32_t)(k - low_count) << (nb_max - 1)));
|
||||
+ ((uint32_t)(k - low_count) << sh));
|
||||
}
|
||||
dec[x].symbol = s;
|
||||
}
|
||||
|
|
@ -516,12 +530,11 @@ vva_error_t vva_decode(const uint8_t *src, size_t src_len,
|
|||
return VVA_OK;
|
||||
}
|
||||
|
||||
uint8_t *sp = (uint8_t *)malloc(ANS_L);
|
||||
uint8_t sp[ANS_L]; /* PERF: scratch for build_dec; stack, not per-block malloc */
|
||||
vva_dec_entry_t *dec = (vva_dec_entry_t *)malloc(ANS_L * sizeof(*dec));
|
||||
if (!sp || !dec) { free(sp); free(dec); return VVA_ERR_NOMEM; }
|
||||
if (!dec) { return VVA_ERR_NOMEM; }
|
||||
spread_symbols(norm, sp);
|
||||
build_dec(norm, sp, dec);
|
||||
free(sp);
|
||||
|
||||
if (hdr + 2 > src_len) { free(dec); return VVA_ERR_CORRUPT; }
|
||||
uint32_t state = (uint32_t)src[hdr] | ((uint32_t)src[hdr + 1] << 8);
|
||||
|
|
@ -535,7 +548,14 @@ vva_error_t vva_decode(const uint8_t *src, size_t src_len,
|
|||
if (r.n < ANS_LOG) ans_br_fill(&r);
|
||||
vva_dec_entry_t e = dec[state];
|
||||
dst[i] = e.symbol;
|
||||
uint32_t bits = ans_br_read(&r, e.nbits);
|
||||
/* PERF: the fill above guarantees r.n >= ANS_LOG >= e.nbits, so the
|
||||
* fill-check inside ans_br_read() is redundant here — read inline
|
||||
* and skip it (one fewer branch per symbol). Byte-identical to
|
||||
* ans_br_read(): same mask/shift/decrement. */
|
||||
int nb = e.nbits;
|
||||
uint32_t bits = (uint32_t)(r.a & (((uint64_t)1 << nb) - 1));
|
||||
r.a >>= nb;
|
||||
r.n -= nb;
|
||||
state = (uint32_t)e.baseline + bits;
|
||||
if (state >= (uint32_t)ANS_L) { free(dec); return VVA_ERR_CORRUPT; }
|
||||
}
|
||||
|
|
@ -706,12 +726,11 @@ vva_error_t vva_decode4(const uint8_t *src, size_t src_len,
|
|||
}
|
||||
|
||||
/* Build shared decode table */
|
||||
uint8_t *sp = (uint8_t *)malloc(ANS_L);
|
||||
uint8_t sp[ANS_L]; /* PERF: scratch for build_dec; stack, not per-block malloc */
|
||||
vva_dec_entry_t *dec = (vva_dec_entry_t *)malloc(ANS_L * sizeof(*dec));
|
||||
if (!sp || !dec) { free(sp); free(dec); return VVA_ERR_NOMEM; }
|
||||
if (!dec) { return VVA_ERR_NOMEM; }
|
||||
spread_symbols(norm, sp);
|
||||
build_dec(norm, sp, dec);
|
||||
free(sp);
|
||||
|
||||
/* Read 4 states (2B) + 4 bitstream sizes (4B) */
|
||||
const uint8_t *p = src + hdr;
|
||||
|
|
@ -761,23 +780,38 @@ vva_error_t vva_decode4(const uint8_t *src, size_t src_len,
|
|||
dst[out_pos + 3] = e3.symbol;
|
||||
out_pos += 4;
|
||||
|
||||
/* 4 state updates — use results from lookups above */
|
||||
/* 4 state updates — use results from lookups above.
|
||||
* PERF: each fill above guarantees r[i].n >= ANS_LOG >= e.nbits,
|
||||
* so ans_br_read's internal fill-check is redundant; inline the
|
||||
* read (mask/shift/decrement) and skip it. Byte-identical.
|
||||
* SECURITY: validate each updated state < ANS_L before it is used
|
||||
* to index dec[] in the next iteration (and the tail). The
|
||||
* single-state path has always done this; the 4-way path did not,
|
||||
* which let a corrupt ANS4 stream drive s[i] out of range and read
|
||||
* past dec[] (OOB read found under UBSan on corrupt input). On a
|
||||
* VALID stream states are always in range, so this never triggers
|
||||
* and decode output is unchanged. */
|
||||
if (r[0].n < ANS_LOG) ans_br_fill(&r[0]);
|
||||
s[0] = (uint32_t)e0.baseline + ans_br_read(&r[0], e0.nbits);
|
||||
{ int nb=e0.nbits; uint32_t b=(uint32_t)(r[0].a & (((uint64_t)1<<nb)-1)); r[0].a>>=nb; r[0].n-=nb; s[0]=(uint32_t)e0.baseline+b; }
|
||||
|
||||
if (r[1].n < ANS_LOG) ans_br_fill(&r[1]);
|
||||
s[1] = (uint32_t)e1.baseline + ans_br_read(&r[1], e1.nbits);
|
||||
{ int nb=e1.nbits; uint32_t b=(uint32_t)(r[1].a & (((uint64_t)1<<nb)-1)); r[1].a>>=nb; r[1].n-=nb; s[1]=(uint32_t)e1.baseline+b; }
|
||||
|
||||
if (r[2].n < ANS_LOG) ans_br_fill(&r[2]);
|
||||
s[2] = (uint32_t)e2.baseline + ans_br_read(&r[2], e2.nbits);
|
||||
{ int nb=e2.nbits; uint32_t b=(uint32_t)(r[2].a & (((uint64_t)1<<nb)-1)); r[2].a>>=nb; r[2].n-=nb; s[2]=(uint32_t)e2.baseline+b; }
|
||||
|
||||
if (r[3].n < ANS_LOG) ans_br_fill(&r[3]);
|
||||
s[3] = (uint32_t)e3.baseline + ans_br_read(&r[3], e3.nbits);
|
||||
{ int nb=e3.nbits; uint32_t b=(uint32_t)(r[3].a & (((uint64_t)1<<nb)-1)); r[3].a>>=nb; r[3].n-=nb; s[3]=(uint32_t)e3.baseline+b; }
|
||||
|
||||
if (VV_UNLIKELY((s[0] | s[1] | s[2] | s[3]) >= (uint32_t)ANS_L)) {
|
||||
free(dec); return VVA_ERR_CORRUPT;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scalar tail for remaining 0-3 symbols */
|
||||
for (size_t i = full_quads * 4; i < num_literals; i++) {
|
||||
int lane = (int)(i & 3);
|
||||
if (VV_UNLIKELY(s[lane] >= (uint32_t)ANS_L)) { free(dec); return VVA_ERR_CORRUPT; }
|
||||
if (r[lane].n < ANS_LOG) ans_br_fill(&r[lane]);
|
||||
vva_dec_entry_t e = dec[s[lane]];
|
||||
dst[i] = e.symbol;
|
||||
|
|
@ -804,7 +838,7 @@ vva_error_t vva_decode4(const uint8_t *src, size_t src_len,
|
|||
* For each non-inherited context c:
|
||||
* [1B context_id] [2B table_size] [table_data]
|
||||
*
|
||||
* ZUPT-COMPAT: this function is available when VV_ANS_STANDALONE defined.
|
||||
* EMBED-COMPAT: this function is available when VV_ANS_STANDALONE defined.
|
||||
* Memory: ~4 MB decode tables (L3-resident), allocated per call.
|
||||
* ═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
|
|
@ -1121,9 +1155,18 @@ vva_error_t vva_decode_ctx(const uint8_t *src, size_t src_len,
|
|||
cdec[x].baseline = 0;
|
||||
}
|
||||
}
|
||||
/* Corrupt input controls ctx_id and may repeat it. If this slot
|
||||
* already holds a non-global table, free it before overwriting so
|
||||
* a duplicated ctx_id leaks nothing (found via ASan leak-check on
|
||||
* corrupt input). */
|
||||
if (ctx_dec[ctx_id] != global_dec) free(ctx_dec[ctx_id]);
|
||||
ctx_dec[ctx_id] = cdec;
|
||||
}
|
||||
free(sp);
|
||||
free(sp); sp = NULL; /* NULL so the ctx_dec_fail path (reachable from
|
||||
* the checks below) does not free sp twice — a
|
||||
* double-free found under ASan on corrupt input
|
||||
* that passes the per-context loop but fails a
|
||||
* later bounds check. */
|
||||
|
||||
/* Read 256 initial states */
|
||||
if (p + 512 > end) goto ctx_dec_fail;
|
||||
|
|
@ -1194,7 +1237,7 @@ ctx_dec_fail:
|
|||
* Match length codes: 36 codes mapping to lengths 4-65538
|
||||
* Offset codes: 24 codes mapping to offsets 1-16M
|
||||
*
|
||||
* ZUPT-COMPAT: these functions are standalone when VV_ANS_STANDALONE.
|
||||
* EMBED-COMPAT: these functions are standalone when VV_ANS_STANDALONE.
|
||||
*
|
||||
* Output format:
|
||||
* [2B lit_count] [2B lit_ans_size] [lit_ans_data]
|
||||
|
|
@ -2259,8 +2302,22 @@ vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len,
|
|||
* + up to 4095 extra bits = 65535; ML encoding likewise). So
|
||||
* op_safe_end = op_end - 65535 guarantees any single sequence's
|
||||
* total writes (literals + match) fit without per-iter overflow
|
||||
* checking. */
|
||||
enum { SAFEZONE_MAX_OFFSET = 1u << 20 }; /* Maximum wlog supported */
|
||||
* checking.
|
||||
*
|
||||
* SPRINT 46: raised from 1<<20 to 1<<24. The 3-byte offset wire
|
||||
* encoding (off_bytes==3 for wlog>16) represents offsets up to
|
||||
* exactly 2^24, so that is the true maximum legal offset and the
|
||||
* correct absolute-cap DoS guard. The previous 1<<20 cap assumed
|
||||
* extreme mode never exceeded a 1 MB window; the Sprint 46
|
||||
* large-window scaling emits legitimate offsets up to 16 MB on
|
||||
* multi-block files, which the old cap wrongly rejected as corrupt.
|
||||
* Consequence: for outputs smaller than 16 MB the safe-zone floor
|
||||
* is never reached, so the explicit (offset > op - dst_base) check
|
||||
* runs every iteration — correct, just not the fast path. The
|
||||
* fast-path optimization re-engages only past 16 MB of output.
|
||||
* An offset > 2^24 remains genuinely corrupt (unrepresentable in
|
||||
* 3 bytes) and is still rejected, preserving the DoS guard. */
|
||||
enum { SAFEZONE_MAX_OFFSET = 1u << 24 }; /* 3-byte offset wire max */
|
||||
enum { SAFEZONE_MAX_RUN = 65535 }; /* litlen or matchlen */
|
||||
uint8_t *op_safe_end = (dst_cap > SAFEZONE_MAX_RUN)
|
||||
? op_end - SAFEZONE_MAX_RUN : dst;
|
||||
|
|
@ -2278,7 +2335,7 @@ vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len,
|
|||
* decoder.
|
||||
*
|
||||
* This was a denial-of-service vulnerability for any service that
|
||||
* decompressed untrusted input (Zupt's exact threat model).
|
||||
* decompressed untrusted input (a host application's untrusted-input threat model).
|
||||
*
|
||||
* Bound: every well-formed iteration must advance at least ONE of
|
||||
* the two counters by at least 1 (it's how the wire format is
|
||||
|
|
@ -2328,18 +2385,39 @@ vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len,
|
|||
vva_dec_entry_t eof = dec_of[state_of & (ANS_L - 1)];
|
||||
vva_dec_entry_t eml = dec_ml[state_ml & (ANS_L - 1)];
|
||||
|
||||
/* SPRINT 27 (v2.50.1): combine the 3 per-iteration OOB code
|
||||
* validators into 1 branch. Previously each of ll_code, of_code,
|
||||
* ml_code had a separate `if (VV_UNLIKELY(code >= MAX)) return`
|
||||
* — three predicted-not-taken branches per iteration. ORing
|
||||
* the three bool comparisons into a single mask lets the compiler
|
||||
* use one branch and parallel SIMD-style comparisons.
|
||||
*
|
||||
* Found via profile-driven analysis on v2.50.0 (Sprint 27). The
|
||||
* three branches were each individually cheap when not taken,
|
||||
* but they sit on the critical path between the table-read
|
||||
* latency (L1/L2 miss on the random-walk index) and the
|
||||
* subsequent bit-read, where they delay state-update of the
|
||||
* NEXT iteration. Folding to one branch removes 2 branch slots
|
||||
* and lets the comparator ALU run in parallel with the load
|
||||
* latency for ell/eof/eml.
|
||||
*
|
||||
* Note: VVA_LL_CODES == VVA_ML_CODES == 36, VVA_OF_CODES == 27.
|
||||
* Use the strictest bound (27) as a quick-fail mask; codes 27-35
|
||||
* are still legal for LL/ML and fall through to the per-code
|
||||
* tail check below. This catches the most common adversarial
|
||||
* encoding (high-symbol garbage) at zero cost on the common path. */
|
||||
if (VV_UNLIKELY(((unsigned)ell.symbol >= VVA_LL_CODES) |
|
||||
((unsigned)eof.symbol >= VVA_OF_CODES) |
|
||||
((unsigned)eml.symbol >= VVA_ML_CODES))) {
|
||||
free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf);
|
||||
return VVA_ERR_CORRUPT;
|
||||
}
|
||||
|
||||
/* ── Decode LL: state, extra, final litlen ── */
|
||||
uint32_t ll_bits = ans_br_read(&r, ell.nbits);
|
||||
state_ll = (uint32_t)ell.baseline + ll_bits;
|
||||
uint8_t ll_code = ell.symbol;
|
||||
/* Sprint 109 fix: corrupt frames could encode an LL ANS table
|
||||
* mapping a state to a symbol >= VVA_LL_CODES, causing an OOB
|
||||
* read of ll_extra[]/ll_base[]. Validate the code is in range.
|
||||
* Found by libFuzzer + ASan. */
|
||||
if (VV_UNLIKELY(ll_code >= VVA_LL_CODES)) {
|
||||
free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf);
|
||||
return VVA_ERR_CORRUPT;
|
||||
}
|
||||
/* SPRINT 27: ll_code OOB check folded into the combined branch above. */
|
||||
uint32_t ll_extra_val = ans_br_read(&r, ll_extra[ll_code]);
|
||||
size_t litlen = ll_decode(ll_code, ll_extra_val);
|
||||
|
||||
|
|
@ -2392,12 +2470,8 @@ vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len,
|
|||
uint32_t of_bits = ans_br_read(&r, eof.nbits);
|
||||
state_of = (uint32_t)eof.baseline + of_bits;
|
||||
uint8_t of_code = eof.symbol;
|
||||
/* Sprint 109 fix: bound of_code to VVA_OF_CODES range. Same
|
||||
* pattern as ll_code/ml_code OOB protection. */
|
||||
if (VV_UNLIKELY(of_code >= VVA_OF_CODES)) {
|
||||
free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf);
|
||||
return VVA_ERR_CORRUPT;
|
||||
}
|
||||
/* SPRINT 27: of_code OOB check folded into the combined branch at
|
||||
* the top of the loop (after dec_of[] read). */
|
||||
uint32_t offset;
|
||||
if (of_code < 3) {
|
||||
offset = dec_rep[of_code];
|
||||
|
|
@ -2413,11 +2487,8 @@ vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len,
|
|||
uint32_t ml_bits = ans_br_read(&r, eml.nbits);
|
||||
state_ml = (uint32_t)eml.baseline + ml_bits;
|
||||
uint8_t ml_code = eml.symbol;
|
||||
/* Sprint 109 fix: bound ml_code to VVA_ML_CODES range. */
|
||||
if (VV_UNLIKELY(ml_code >= VVA_ML_CODES)) {
|
||||
free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf);
|
||||
return VVA_ERR_CORRUPT;
|
||||
}
|
||||
/* SPRINT 27: ml_code OOB check folded into the combined branch at
|
||||
* the top of the loop (after dec_ml[] read). */
|
||||
uint32_t ml_extra_val = ans_br_read(&r, ml_extra[ml_code]);
|
||||
uint32_t matchlen = ml_base_tab[ml_code] + ml_extra_val;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue