Skip to content

Commit d7dd9de

Browse files
committed
0001 mux_mp4_file.rs の data_size の u32 トランケーションを修正する
- src/mux_mp4_file.rs:548 の `sample.data_size as u32` を u32::try_from() に 置き換え、u32::MAX を超える場合に MuxError::EncodeError を返すようにする - MuxError 種別は MuxError::Overflow という候補もあるが、mux_fmp4_segment.rs:744 と一貫させるため MuxError::EncodeError(Error::invalid_data(...)) を選択する - レビューで src/mux_mp4_file.rs:986 の `c.samples.len() as u32` も同型の暗黙 トランケーションを抱えていることが判明したため、同じく u32::try_from() で 防御する - Mp4FileMuxer::append_sample() の doc コメントに、エラー返却時の内部状態の 不変条件を明記する - テストモジュールに 4 テストを追加する (境界値成功・faststart 経路・境界値超過 エラー・エラー後リトライ可)
1 parent bd6cfb4 commit d7dd9de

3 files changed

Lines changed: 176 additions & 6 deletions

File tree

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
## develop
1313

14+
- [FIX] `Mp4FileMuxer::append_sample()``sample.data_size``u32::MAX` を超える場合にエラーを返すようにする
15+
- これまでは `usize` から `u32` への暗黙キャストで上位ビットが切り捨てられ、壊れた MP4 が生成される可能性があった
16+
- `u32::try_from()` で明示的にチェックし、超過時は `MuxError::EncodeError` を返すように変更した
17+
- 同様に `build_stbl_box` 内の `sample_per_chunk` でも `c.samples.len()``u32` 暗黙キャストを防御する
18+
- @voluntas
19+
1420
## 2026.3.0
1521

1622
- [ADD] `Mp4FileMuxer::advance_position()` メソッドを追加する

issues/0001-bug-mux-mp4-file-data-size-truncation.md renamed to issues/closed/0001-bug-mux-mp4-file-data-size-truncation.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# mux_mp4_file.rs で sample.data_size の usize→u32 によるトランケーションが発生する
22

33
Created: 2026-05-20
4+
Completed: 2026-05-20
45
Model: opencode mimo-v2.5-pro
6+
Branch: feature/fix-mux-mp4-file-data-size-truncation
57

68
## 概要
79

@@ -94,3 +96,28 @@ fmp4 側と一貫性を取るため `EncodeError` を採用する)。
9496

9597
C API (`crates/c-api/src/mux.rs`) の `Mp4MuxSample` でも `data_size` を受け取るが、
9698
C API 側では `u32` として扱っているため、今回の問題は発生しない。
99+
100+
## 解決方法
101+
102+
- `src/mux_mp4_file.rs:548``sample.data_size as u32`
103+
`u32::try_from(sample.data_size).map_err(|_| MuxError::EncodeError(Error::invalid_data("sample data size exceeds u32::MAX")))?`
104+
に置き換え、`u32::MAX` を超えるサイズで明示的なエラーを返すようにした。
105+
`mux_fmp4_segment.rs:744` 周辺と同じ防御パターンに揃えた。
106+
- レビューで `src/mux_mp4_file.rs:986``c.samples.len() as u32` も同型の暗黙
107+
トランケーションを抱えていることが判明したため、同じく `u32::try_from()`
108+
ベースに置き換え、`build_stbl_box``entries` 構築 closure を `Result<StscEntry, MuxError>`
109+
返却にして `collect::<Result<_, _>>()?` で伝播するようにした。エラー文面は
110+
`"samples per chunk exceeds u32::MAX"`
111+
- `Mp4FileMuxer::append_sample()` の doc コメントに、エラー返却時の内部状態の
112+
不変条件 (`MissingSampleEntry` 経路を除き `next_position` / `chunks` 等は
113+
変更されない) を明記した。
114+
- `src/mux_mp4_file.rs``#[cfg(test)] mod tests` に以下 4 テストを追加した:
115+
- `test_append_sample_data_size_u32_max_succeeds`: 境界値 `u32::MAX`
116+
`append_sample``finalize` が成功し、Co64Box 経路を通ることを検証する
117+
- `test_append_sample_data_size_u32_max_with_faststart`: faststart 有効化
118+
(`with_options`) 経路でも同じ境界値挙動を検証する
119+
- `test_append_sample_data_size_exceeds_u32_max`: `u32::MAX + 1`
120+
`MuxError::EncodeError` が返り、その Display 出力が
121+
`"sample data size exceeds u32::MAX"` を含むことを検証する (64-bit 限定)
122+
- `test_append_sample_error_keeps_muxer_state`: エラー後に同じ `data_offset`
123+
で正常サンプルを再投入できることを検証する (64-bit 限定)

src/mux_mp4_file.rs

Lines changed: 143 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,11 @@ impl Mp4FileMuxer {
531531
/// 実際のデータ追記処理自体は利用側の責務であり、
532532
/// このメソッド目的は、その追記結果などを伝えることで、
533533
/// [`Mp4FileMuxer`] が適切に、MP4ファイルの再生に必要なメタデータを構築できるようにすることである。
534+
///
535+
/// エラーを返した場合、内部状態 (`next_position` / `audio_chunks` / `video_chunks` /
536+
/// `last_sample_kind`) は変更されないため、呼び出し側は内容を補正したサンプルで再呼び出しできる。
537+
/// ただし、対象トラック種別の最初のサンプル投入直後に `MissingSampleEntry` エラーになった場合は、
538+
/// そのトラックの `audio_track_timescale` または `video_track_timescale` だけは記録済みとなる。
534539
pub fn append_sample(&mut self, sample: &Sample) -> Result<(), MuxError> {
535540
if self.finalized_boxes.is_some() {
536541
return Err(MuxError::AlreadyFinalized);
@@ -545,7 +550,9 @@ impl Mp4FileMuxer {
545550
let metadata = SampleMetadata {
546551
duration: sample.duration,
547552
keyframe: sample.keyframe,
548-
size: sample.data_size as u32,
553+
size: u32::try_from(sample.data_size).map_err(|_| {
554+
MuxError::EncodeError(Error::invalid_data("sample data size exceeds u32::MAX"))
555+
})?,
549556
composition_time_offset: sample.composition_time_offset,
550557
};
551558

@@ -973,19 +980,23 @@ impl Mp4FileMuxer {
973980
entries: chunks
974981
.iter()
975982
.enumerate()
976-
.map(|(i, c)| {
983+
.map(|(i, c)| -> Result<StscEntry, MuxError> {
977984
let sample_description_index = sample_entries
978985
.iter()
979986
.position(|entry| entry == &c.sample_entry)
980987
.map(|idx| NonZeroU32::MIN.saturating_add(idx as u32))
981988
.expect("sample_entry should exist in sample_entries");
982-
StscEntry {
989+
Ok(StscEntry {
983990
first_chunk: NonZeroU32::MIN.saturating_add(i as u32),
984-
sample_per_chunk: c.samples.len() as u32,
991+
sample_per_chunk: u32::try_from(c.samples.len()).map_err(|_| {
992+
MuxError::EncodeError(Error::invalid_data(
993+
"samples per chunk exceeds u32::MAX",
994+
))
995+
})?,
985996
sample_description_index,
986-
}
997+
})
987998
})
988-
.collect(),
999+
.collect::<Result<Vec<_>, _>>()?,
9891000
};
9901001

9911002
let stsz_box = StszBox::Variable {
@@ -1123,6 +1134,8 @@ fn build_ctts_box(chunks: &[Chunk]) -> Result<Option<CttsBox>, MuxError> {
11231134
#[cfg(test)]
11241135
mod tests {
11251136
use super::*;
1137+
use alloc::format;
1138+
11261139
use crate::{
11271140
Uint,
11281141
boxes::{
@@ -1275,6 +1288,130 @@ mod tests {
12751288
));
12761289
}
12771290

1291+
/// data_size が u32::MAX の境界値で append_sample と finalize が成功するテスト
1292+
///
1293+
/// Mp4FileMuxer はメタデータのみを管理して実バイト列は確保しないため、
1294+
/// data_size に u32::MAX を渡しても 4 GiB の確保は発生しない。
1295+
/// next_position が u32::MAX を超えるため、finalize は Co64Box 経路を通る。
1296+
#[test]
1297+
fn test_append_sample_data_size_u32_max_succeeds() {
1298+
let mut muxer = Mp4FileMuxer::new().expect("failed to create muxer");
1299+
let initial_size = muxer.initial_boxes_bytes().len() as u64;
1300+
1301+
let sample = Sample {
1302+
track_kind: TrackKind::Video,
1303+
sample_entry: Some(create_avc1_sample_entry()),
1304+
keyframe: true,
1305+
timescale: NonZeroU32::MIN.saturating_add(30 - 1),
1306+
duration: 1,
1307+
composition_time_offset: None,
1308+
data_offset: initial_size,
1309+
data_size: u32::MAX as usize,
1310+
};
1311+
muxer
1312+
.append_sample(&sample)
1313+
.expect("failed to append sample with u32::MAX data_size");
1314+
let finalized = muxer.finalize().expect("failed to finalize muxer");
1315+
assert!(!finalized.moov_box_bytes.is_empty());
1316+
}
1317+
1318+
/// faststart 有効化 (with_options) 経路でも data_size の u32 境界が同様に防御されるテスト
1319+
#[test]
1320+
fn test_append_sample_data_size_u32_max_with_faststart() {
1321+
let options = Mp4FileMuxerOptions {
1322+
reserved_moov_box_size: 8192,
1323+
..Default::default()
1324+
};
1325+
let mut muxer =
1326+
Mp4FileMuxer::with_options(options).expect("failed to create muxer with options");
1327+
let initial_size = muxer.initial_boxes_bytes().len() as u64;
1328+
1329+
let sample = Sample {
1330+
track_kind: TrackKind::Video,
1331+
sample_entry: Some(create_avc1_sample_entry()),
1332+
keyframe: true,
1333+
timescale: NonZeroU32::MIN.saturating_add(30 - 1),
1334+
duration: 1,
1335+
composition_time_offset: None,
1336+
data_offset: initial_size,
1337+
data_size: u32::MAX as usize,
1338+
};
1339+
muxer
1340+
.append_sample(&sample)
1341+
.expect("failed to append sample with u32::MAX data_size under faststart");
1342+
let finalized = muxer.finalize().expect("failed to finalize muxer");
1343+
assert!(finalized.is_faststart_enabled());
1344+
}
1345+
1346+
/// data_size が u32::MAX を超える場合のエラーテスト
1347+
// 32-bit プラットフォームでは usize で u32::MAX + 1 を表現できず構造的に到達不能なため cfg で限定する
1348+
#[test]
1349+
#[cfg(target_pointer_width = "64")]
1350+
fn test_append_sample_data_size_exceeds_u32_max() {
1351+
let mut muxer = Mp4FileMuxer::new().expect("failed to create muxer");
1352+
let initial_size = muxer.initial_boxes_bytes().len() as u64;
1353+
1354+
let sample = Sample {
1355+
track_kind: TrackKind::Video,
1356+
sample_entry: Some(create_avc1_sample_entry()),
1357+
keyframe: true,
1358+
timescale: NonZeroU32::MIN.saturating_add(30 - 1),
1359+
duration: 1,
1360+
composition_time_offset: None,
1361+
data_offset: initial_size,
1362+
data_size: u32::MAX as usize + 1,
1363+
};
1364+
let err = muxer
1365+
.append_sample(&sample)
1366+
.expect_err("expected encode error for data_size exceeding u32::MAX");
1367+
assert!(matches!(err, MuxError::EncodeError(_)));
1368+
// MuxError::EncodeError は他原因でも返るためメッセージ内容まで確認する
1369+
let message = format!("{err}");
1370+
assert!(
1371+
message.contains("sample data size exceeds u32::MAX"),
1372+
"unexpected error message: {message}",
1373+
);
1374+
}
1375+
1376+
/// append_sample がエラーを返した後にミューサ状態が変化していないことを検証するテスト
1377+
// 32-bit プラットフォームでは usize で u32::MAX + 1 を表現できず構造的に到達不能なため cfg で限定する
1378+
#[test]
1379+
#[cfg(target_pointer_width = "64")]
1380+
fn test_append_sample_error_keeps_muxer_state() {
1381+
let mut muxer = Mp4FileMuxer::new().expect("failed to create muxer");
1382+
let initial_size = muxer.initial_boxes_bytes().len() as u64;
1383+
1384+
let bad_sample = Sample {
1385+
track_kind: TrackKind::Video,
1386+
sample_entry: Some(create_avc1_sample_entry()),
1387+
keyframe: true,
1388+
timescale: NonZeroU32::MIN.saturating_add(30 - 1),
1389+
duration: 1,
1390+
composition_time_offset: None,
1391+
data_offset: initial_size,
1392+
data_size: u32::MAX as usize + 1,
1393+
};
1394+
muxer
1395+
.append_sample(&bad_sample)
1396+
.expect_err("expected encode error for data_size exceeding u32::MAX");
1397+
1398+
// エラー後でも next_position は初期値のままなので、同じ data_offset で再投入できる
1399+
let good_sample = Sample {
1400+
track_kind: TrackKind::Video,
1401+
sample_entry: Some(create_avc1_sample_entry()),
1402+
keyframe: true,
1403+
timescale: NonZeroU32::MIN.saturating_add(30 - 1),
1404+
duration: 1,
1405+
composition_time_offset: None,
1406+
data_offset: initial_size,
1407+
data_size: 1024,
1408+
};
1409+
muxer
1410+
.append_sample(&good_sample)
1411+
.expect("failed to append sample after error");
1412+
muxer.finalize().expect("failed to finalize muxer");
1413+
}
1414+
12781415
/// 音声と映像の複数トラックのテスト
12791416
#[test]
12801417
fn test_audio_and_video_tracks() {

0 commit comments

Comments
 (0)