@@ -3,7 +3,7 @@
**Issue**: [#5](https://github.com/notifd/ex_git_objectstore/issues/5)
**Fix Issue**: [#6](https://github.com/notifd/ex_git_objectstore/issues/6)
**Date**: 2026-02-10
**Status**: Fix cycles complete — all Critical/High addressed
**Status**: 5 red team / fix cycles complete — all Critical/High addressed
**Team**: security-hunter, perf-hunter, correctness-auditor, interop-tester, reliability-auditor
## Executive Summary
@@ -443,3 +443,36 @@
| NEW-HIGH-2 | Added `if base_offset < 0` guard after computing `offset - neg_offset` in OFS_DELTA handler. Returns `{:error, :invalid_ofs_delta_offset}` instead of crashing on negative binary size. | cycle4_fixes_test.exs (2 tests) |
| NEW-C4-3 | Added `@max_varint_bytes 10` limit to `read_varint` in delta.ex. Consumed counter tracks continuation bytes; exceeding 10 returns `{:error, :varint_too_long}`. | cycle4_fixes_test.exs (4 tests) |
| NEW-C4-2 | Added `@sha_hex_pattern ~r/\A[0-9a-f]{40}\z/` validation to `parse_command_line` in receive_pack.ex. Invalid SHAs in push commands return `{:error, {:invalid_sha_format, cmd_str}}`. | cycle4_fixes_test.exs (4 tests) |
---
## Red Team Cycle 5
**Auditors**: 2 parallel auditors verified all cycle 4 fixes correct.
### New findings from cycle 5:
| ID | Severity | Finding | Disposition |
|----|----------|---------|-------------|
| C5-A-C2 | High | Writer `zlib_compress` returns error tuple that gets embedded in iodata, causing confusing crash | **Fixed in cycle 5** — removed rescue, let exceptions propagate |
| C5-A-C3 | High | Writer `Base.decode16` crash via `elem(:error, 1)` on invalid SHA | **Fixed in cycle 5** — added `decode16!/1` with clear error message |
| C5-A-C1 | High | PktLine decoder accepts packets > 65520 bytes (spec violation) | **Fixed in cycle 5** — added `len <= @max_pkt_len` guard in decoder |
| C5-B-H1 | High | Ref `sha?/1` uses `^`/`$` anchors instead of `\A`/`\z` (inconsistent with protocol modules) | **Fixed in cycle 5** — changed to `\A`/`\z` anchors |
| C5-A-H1 | Medium | OFS offset arithmetic produces large integers (2^70) | **False positive** — Elixir bigints are ~9 bytes, not a DoS |
| C5-A-H2 | Medium | Pack writer doesn't validate object count fits in 32 bits | **False positive** — can't have >2^32 list elements in practice |
| C5-A-H3 | Medium | packed-refs file read with no size limit | **Accepted** — filesystem protections already in place |
| C5-B-C5-2 | Medium | Ref resolution depth off-by-one (11 vs 10) | **Accepted** — trivial, within acceptable range |
| C5-B-H5-4 | Medium | Walk timestamp parser returns 0 on malformed input | **Accepted** — edge case, doesn't affect normal operation |
---
## Fix Cycle 5 (this commit)
**4 fixes.** 482 tests, 0 failures.
| Finding | Fix Summary | Tests |
|---------|-------------|-------|
| C5-A-C2 | Removed `rescue` from `zlib_compress` — exceptions propagate directly instead of returning error tuples that corrupt iodata. `after` block still ensures `:zlib.close`. | cycle5_fixes_test.exs (2 tests) |
| C5-A-C3 | Added `decode16!/1` helper that uses `case Base.decode16(...)` with pattern matching. Returns binary on success, raises `ArgumentError` with clear message on invalid SHA hex. Used in both `generate_index` and `build_fanout`. | cycle5_fixes_test.exs (3 tests) |
| C5-A-C1 | Added `len <= @max_pkt_len` guard to `decode_one/1`. Packets with length > 65520 now fall through to `{:error, {:invalid_pkt_len, hex_len}}`. Matches the limit already enforced by `encode/1` and `encode_raw/1`. | cycle5_fixes_test.exs (5 tests) |
| C5-B-H1 | Changed `sha?/1` regex from `~r/^[0-9a-f]{40}$/` to `~r/\A[0-9a-f]{40}\z/` for consistency with `@sha_hex_pattern` in upload_pack.ex and receive_pack.ex. | cycle5_fixes_test.exs (3 tests) |