Skip to content

fix(identity): reject a truncated WAV fmt chunk instead of indexing past the end - #901

Open
minion1227 wants to merge 2 commits into
GeniePod:mainfrom
minion1227:minion_wav_fmt_bounds
Open

fix(identity): reject a truncated WAV fmt chunk instead of indexing past the end#901
minion1227 wants to merge 2 commits into
GeniePod:mainfrom
minion1227:minion_wav_fmt_bounds

Conversation

@minion1227

@minion1227 minion1227 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

read_wav_mono_f32 panics with an out-of-bounds index on a WAV whose fmt chunk declares 16 bytes but is truncated by the end of the file. The guard meant to catch that can never fire.

The dead guard

let chunk_end = chunk_start.saturating_add(chunk_size).min(data.len());
...
b"fmt " => {
    if chunk_size < 16 || chunk_end > data.len() {   // second test is unreachable
        anyhow::bail!("invalid WAV fmt chunk");
    }
    ... data[chunk_start + 2] ... data[chunk_start + 14] ... data[chunk_start + 15]

chunk_end is clamped with .min(data.len()) two lines earlier, so chunk_end > data.len() is always false. A fmt chunk that declares 16 bytes near the end of the file therefore passes the guard and the fixed-offset reads below index past the end.

Replaced with a check against the bytes actually present:

let present = data.len() - chunk_start;
let chunk_end = chunk_start + chunk_size.min(present);
...
if chunk_size < 16 || present < 16 { bail!("invalid WAV fmt chunk") }

The data chunk keeps its tolerant clamping — a slightly short recording is still usable, and the sample loop is already bounded by cursor + frame_bytes <= end.

Real Behavior Proof

  • I have built and run the affected code locally (or noted why I could not).
  • I have verified the change end-to-end on Jetson hardware.
  • I have NOT verified on Jetson hardware, and I explain the equivalent verification path or validation gap below.

Tested profile / hardware (check all that apply):

  • jetson
  • raspberry_pi
  • portable_sbc
  • laptop
  • mac
  • CI-only / docs-only
  • Not run locally

What I ran

Linux x86_64 laptop. This is pure byte parsing with no device dependency, so the laptop exercises it fully.

I confirmed the panic before fixing it, by writing the regression test first and running it against unmodified main:

cargo test -p genie-core --lib truncated_fmt_chunk   # against main: panics
cargo test -p genie-core --lib voice::identity
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check

What I observed

Against unmodified main, a 44-byte input built from RIFF/WAVE + a 10-byte JUNK chunk + a fmt header declaring 16 bytes with only 6 present:

thread 'voice::identity::tests::probe_truncated_fmt_chunk' panicked at
crates/genie-core/src/voice/identity.rs:373:25:
index out of bounds: the len is 44 but the index is 44

Note the input is exactly 44 bytes, so it clears the data.len() < 44 gate at the top of the function — the length check does not protect these reads.

With the fix, the same input returns Err("invalid WAV fmt chunk"). I re-verified the test is load-bearing by restoring the old chunk_end > data.len() condition, confirming FAILED with the same out-of-bounds message, then restoring the fix and confirming pass.

  • cargo test -p genie-core --lib voice::identity — 8 passed.
  • cargo test --workspace — green, 0 failures.
  • clippy clean under -D warnings (workspace and --no-default-features); fmt clean.

Reachability

enroll_speaker_file and identify_speaker_file both parse a WAV from a path, so this is reachable via genie-ctl speaker enroll <file> with a corrupt file, and by the daemon on any recording truncated by an interrupted write or a full disk. A panic there aborts the enrolling/identifying task rather than returning the error the caller already handles.

Test plan

cargo test -p genie-core --lib truncated_fmt_chunk

Restoring chunk_end > data.len() in place of present < 16 reproduces the panic.

Notes for reviewers

  • Small fix, deliberately. I checked the two other places that parse WAV headers — voice/vad.rs::trim_wav and voice/aec.rs both guard data.len() <= 44 before slicing, so neither has this problem and I left them alone rather than widen the diff on paths that are already correct.
  • The fmt reads are at fixed offsets, so requiring 16 present bytes is the whole precondition; I did not make the chunk walk strict in general, because the tolerant data-chunk clamping is load-bearing for real recordings.
  • No filed issue; found while auditing WAV parsing. Happy to open one first if you prefer that order.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of malformed or truncated WAV audio files by safely interpreting available chunk data.
    • Prevented crashes for incomplete fmt chunks; these files now return a clear invalid WAV fmt chunk error.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 63b0630b-e674-41ac-8f13-a70ad21bee0f

📥 Commits

Reviewing files that changed from the base of the PR and between 74c47f1 and 6ed6f6a.

📒 Files selected for processing (1)
  • crates/genie-core/src/voice/identity.rs

📝 Walkthrough

Walkthrough

The WAV reader now detects fmt chunks with insufficient declared or physically present bytes and returns an error instead of risking an out-of-bounds panic. A regression test verifies the specific error.

Changes

WAV truncation validation

Layer / File(s) Summary
Validate truncated fmt chunks
crates/genie-core/src/voice/identity.rs
Chunk parsing accounts for bytes physically present in the file, rejects incomplete fmt chunks, and verifies that truncated input returns the expected error.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: rejecting truncated WAV fmt chunks before out-of-bounds indexing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/genie-core/src/voice/identity.rs`:
- Around line 864-867: Update the test around read_wav_mono_f32 to inspect the
returned error rather than only asserting result.is_err(). Verify that the error
text or equivalent context identifies the truncated fmt chunk as an invalid WAV
fmt chunk, while preserving the existing cleanup and failure behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e46d6edd-c59e-4194-9502-f83e3b8c578e

📥 Commits

Reviewing files that changed from the base of the PR and between 02a577d and 74c47f1.

📒 Files selected for processing (1)
  • crates/genie-core/src/voice/identity.rs

Comment thread crates/genie-core/src/voice/identity.rs Outdated
Review follow-up: the regression test accepted any `Err`, so a change that
skipped the malformed chunk instead of rejecting it would still pass — the
later "expected 16-bit PCM" bail would satisfy it without exercising this
guard. Assert the error text instead. Re-verified it still fails against the
old `chunk_end > data.len()` condition with the original out-of-bounds panic.
@minion1227

Copy link
Copy Markdown
Contributor Author

Fair point — taken in 6ed6f6a.

You're right that is_err() didn't pin the contract. Concretely: if a later change skipped the malformed fmt chunk instead of rejecting it, audio_format would stay None and the function would bail with "expected 16-bit PCM" — satisfying the old assertion without ever exercising this guard.

Now asserts the error text:

let error = result.expect_err("a truncated fmt chunk must be an error");
assert!(error.to_string().contains("invalid WAV fmt chunk"), "expected the fmt-chunk rejection, got: {error}");

Re-verified in both directions: passes with the fix, and still FAILS against the old chunk_end > data.len() condition with the original index out of bounds: the len is 44 but the index is 44. 8 identity tests pass; fmt and clippy -D warnings clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant