field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 118fee1a7426341c98639a2d469611fecf1eaf5d
parent de87ac0c86ed27af949a8c1e3927513aa7d75c51
Author: triesap <tyson@radroots.org>
Date:   Sat, 19 Sep 2026 17:30:41 +0000

media: verify exact prepared upload bytes

- Recheck file size after exact owned reads and reject partial-read failures
- Bound submission bytes to the established prepared-media limit
- Prove altered bytes and metadata cannot persist or obtain upload authority
- Verify frozen parent binding and regenerate exact native artifacts

Diffstat:
MTeraFFI/provenance.json | 42+++++++++++++++++++++---------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 32++++++++++++++++----------------
MTeraFFI/source/aarch64-apple-ios-sim.json | 32++++++++++++++++----------------
MTeraFFI/source/aarch64-apple-ios.json | 32++++++++++++++++----------------
Mcore/crates/tera_core/src/runtime/product_surface/submission/media.rs | 4++++
Mcore/crates/tera_core/src/runtime/product_surface/submission/media_tests.rs | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_ffi/src/media_file.rs | 16++++++++++++++--
Mcore/crates/tera_ffi/src/media_file_tests.rs | 25+++++++++++++++++++++++++
Mcore/crates/tera_ffi/tests/media_ownership.rs | 43+++++++++++++++++++++++++++++++++++++++++++
Mrelease/provenance.json | 4++--
11 files changed, 219 insertions(+), 75 deletions(-)

diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76300680, + "bytes": 76201200, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "bf7519b074a67073acb95ebbdfffdb51b2e6dccfc48bbbe5f8b031ec8a814083" + "sha256": "3db5f2c892cd2f9a0fb58c67ce9ce38d368e925a34b4b030346d7be308e55d4d" }, { "bytes": 80633, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76383656, + "bytes": 76284560, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "0049118f14cae5c16228eb1e82811290c93fb43adb50d4e5672d03b228c66b60" + "sha256": "e11c1bbe578a43fc6abc7a22810a13106541927ded6df61b59a8ccc49c960ff0" }, { "bytes": 49312, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 21851872, + "bytes": 21842560, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "9bb680bb3d7cc8ff565b5099f3477f5ed927bf7ce627f7b3b69e7c4634b7d711" + "sha256": "af66b29ca68dbb3bc1db6328fed6220d2653983afce45a3f1d85fec0bb513017" }, { - "bytes": 76300680, + "bytes": 76201200, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "bf7519b074a67073acb95ebbdfffdb51b2e6dccfc48bbbe5f8b031ec8a814083" + "sha256": "3db5f2c892cd2f9a0fb58c67ce9ce38d368e925a34b4b030346d7be308e55d4d" }, { - "bytes": 76383656, + "bytes": 76284560, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "0049118f14cae5c16228eb1e82811290c93fb43adb50d4e5672d03b228c66b60" + "sha256": "e11c1bbe578a43fc6abc7a22810a13106541927ded6df61b59a8ccc49c960ff0" }, { "bytes": 100086, "path": "source/aarch64-apple-darwin.json", - "sha256": "5b023e8b835ebaba7fa1217a8ad7b230ac038cad68397d0cb57c462ccc6033a6" + "sha256": "ac6a0cb147baf73d69ba2e4727d5e57001a061f71e0478b13d6bd7703766c401" }, { "bytes": 99930, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "619a7dc9d88e0870878aa0a81d7472272d9f9b9e21f44bd5105db82af5e1b5b3" + "sha256": "8479928697fa2c03a2fcf518310905a196a8920c166eb725038fa92cab3f6d5f" }, { "bytes": 99926, "path": "source/aarch64-apple-ios.json", - "sha256": "92fc8547484ec06b456e4dd8037d9134aa232162a471aa443da1619e893db9b3" + "sha256": "e4d38c3fda702e45025f3ae9ddf1ada8a386cc636ab6293b53f7acbeab9912e9" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "abea66d625420814e307654d6777e86796424e24" + "tree": "60e714586d3acc430daee705277449abdfebacd2" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76300680, + "bytes": 76201200, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "bf7519b074a67073acb95ebbdfffdb51b2e6dccfc48bbbe5f8b031ec8a814083" + "sha256": "3db5f2c892cd2f9a0fb58c67ce9ce38d368e925a34b4b030346d7be308e55d4d" }, { "bytes": 80633, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76383656, + "bytes": 76284560, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "0049118f14cae5c16228eb1e82811290c93fb43adb50d4e5672d03b228c66b60" + "sha256": "e11c1bbe578a43fc6abc7a22810a13106541927ded6df61b59a8ccc49c960ff0" }, { "bytes": 643709, @@ -176,17 +176,17 @@ { "bytes": 100086, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "5b023e8b835ebaba7fa1217a8ad7b230ac038cad68397d0cb57c462ccc6033a6" + "sha256": "ac6a0cb147baf73d69ba2e4727d5e57001a061f71e0478b13d6bd7703766c401" }, { "bytes": 99930, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "619a7dc9d88e0870878aa0a81d7472272d9f9b9e21f44bd5105db82af5e1b5b3" + "sha256": "8479928697fa2c03a2fcf518310905a196a8920c166eb725038fa92cab3f6d5f" }, { "bytes": 99926, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "92fc8547484ec06b456e4dd8037d9134aa232162a471aa443da1619e893db9b3" + "sha256": "e4d38c3fda702e45025f3ae9ddf1ada8a386cc636ab6293b53f7acbeab9912e9" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "abea66d625420814e307654d6777e86796424e24" -manifest_sha256 = "6e587ec3129d2f90dd87d269593c2f200fd13ac29f411c5a2de850be36520698" +source_tree = "60e714586d3acc430daee705277449abdfebacd2" +manifest_sha256 = "e618e901823221365bdb1045ef6574be49333ab2a570642a838668d77e8c79e7" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -1017,10 +1017,10 @@ "sha256": "89245c05cf6717aad83658eb77ed1bd42d1adb35d466a467041f8cb3fd0b9aba" }, "core/crates/tera_core/src/runtime/product_surface/submission/media.rs": { - "bytes": 4750, - "git_blob": "a36ba548f4e977f2d83b7924078fa733b0495b04", + "bytes": 4943, + "git_blob": "add4f6673358b1d2cedc563afa27e595223500ab", "mode": "100644", - "sha256": "9793fd4e67b178e77cb344b44213a829211bb2d51101dc6a2b2e72642402e5be" + "sha256": "1ba102fa9fb5628e9137c0c7de36eae42b423ba8beeb638f6eca02b0db7cb950" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_foreground.rs": { "bytes": 3262, @@ -1053,10 +1053,10 @@ "sha256": "cfab5909c21825d09976fcd68761114d9ab21602a2ade2a722abb65198984fbb" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_tests.rs": { - "bytes": 7755, - "git_blob": "14cc026f54ebe09d4bd11fd91da1c8398830bdff", + "bytes": 9574, + "git_blob": "7c5473afac9d4a440d078e9092f8081884ac5040", "mode": "100644", - "sha256": "2f955e271cff2e20f2fc08410a32bdb62699a39814363354923714f1f3280f68" + "sha256": "97039f54d1bb6550a828a071bf2c0d165b14867d6bee72159004772e3bc28155" }, "core/crates/tera_core/src/runtime/product_surface/submission/operation.rs": { "bytes": 7701, @@ -1437,16 +1437,16 @@ "sha256": "2928947b57e21f1526f8c729f2e58c6347b061737d7fee19c2f90c32539b0dee" }, "core/crates/tera_ffi/src/media_file.rs": { - "bytes": 4731, - "git_blob": "37e5a306369e95a47559f25273a18df11c2084b6", + "bytes": 5267, + "git_blob": "5579021f7d8dd7af0c462fb74925e9f51e7c1bbf", "mode": "100644", - "sha256": "347390c081798c102926bcb412c63b760aa7bbfacbbeac41c3580677051a3416" + "sha256": "b3a9dea725cb148900000e401e3fe2a42daeab2f826f54c6db1a596e36fd81ef" }, "core/crates/tera_ffi/src/media_file_tests.rs": { - "bytes": 6109, - "git_blob": "fb7aedd53319fc37e641914bfe77667ee1057dc6", + "bytes": 7010, + "git_blob": "371c4f68d461ed6272a3a84a2ae609919173c309", "mode": "100644", - "sha256": "320601c44824df2738278a00440d0dbf802e0573465042e205dcf946011db9d4" + "sha256": "5564da1a1f0b09059a9ab86242425720c1b52f90e1388eb96da920058231f7dd" }, "core/crates/tera_ffi/src/operations.rs": { "bytes": 37016, @@ -1581,10 +1581,10 @@ "sha256": "ec87c2cc7f396ef583d3a37186f40932ac4eb42c0e80973a5c25312099f2670a" }, "core/crates/tera_ffi/tests/media_ownership.rs": { - "bytes": 5006, - "git_blob": "2ae5e29038ec37f9d67d1035a5e9ac2fb1e6c412", + "bytes": 6567, + "git_blob": "57a09e25c1db661ecacb26afe95491f462cb52f8", "mode": "100644", - "sha256": "4dc14b48a5a201fb7afd84094ba61fbce2b0d396c19e6851df13b47085c036a2" + "sha256": "6434a8258a39e9a7a58a42d6f995a313c430238c8334a5986e4e84439e3c41e9" }, "core/crates/tera_ffi/tests/recovery_contract.rs": { "bytes": 7971, @@ -2140,6 +2140,6 @@ } }, "policy": "staged_inputs", - "tree": "abea66d625420814e307654d6777e86796424e24" + "tree": "60e714586d3acc430daee705277449abdfebacd2" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -1013,10 +1013,10 @@ "sha256": "89245c05cf6717aad83658eb77ed1bd42d1adb35d466a467041f8cb3fd0b9aba" }, "core/crates/tera_core/src/runtime/product_surface/submission/media.rs": { - "bytes": 4750, - "git_blob": "a36ba548f4e977f2d83b7924078fa733b0495b04", + "bytes": 4943, + "git_blob": "add4f6673358b1d2cedc563afa27e595223500ab", "mode": "100644", - "sha256": "9793fd4e67b178e77cb344b44213a829211bb2d51101dc6a2b2e72642402e5be" + "sha256": "1ba102fa9fb5628e9137c0c7de36eae42b423ba8beeb638f6eca02b0db7cb950" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_foreground.rs": { "bytes": 3262, @@ -1049,10 +1049,10 @@ "sha256": "cfab5909c21825d09976fcd68761114d9ab21602a2ade2a722abb65198984fbb" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_tests.rs": { - "bytes": 7755, - "git_blob": "14cc026f54ebe09d4bd11fd91da1c8398830bdff", + "bytes": 9574, + "git_blob": "7c5473afac9d4a440d078e9092f8081884ac5040", "mode": "100644", - "sha256": "2f955e271cff2e20f2fc08410a32bdb62699a39814363354923714f1f3280f68" + "sha256": "97039f54d1bb6550a828a071bf2c0d165b14867d6bee72159004772e3bc28155" }, "core/crates/tera_core/src/runtime/product_surface/submission/operation.rs": { "bytes": 7701, @@ -1433,16 +1433,16 @@ "sha256": "2928947b57e21f1526f8c729f2e58c6347b061737d7fee19c2f90c32539b0dee" }, "core/crates/tera_ffi/src/media_file.rs": { - "bytes": 4731, - "git_blob": "37e5a306369e95a47559f25273a18df11c2084b6", + "bytes": 5267, + "git_blob": "5579021f7d8dd7af0c462fb74925e9f51e7c1bbf", "mode": "100644", - "sha256": "347390c081798c102926bcb412c63b760aa7bbfacbbeac41c3580677051a3416" + "sha256": "b3a9dea725cb148900000e401e3fe2a42daeab2f826f54c6db1a596e36fd81ef" }, "core/crates/tera_ffi/src/media_file_tests.rs": { - "bytes": 6109, - "git_blob": "fb7aedd53319fc37e641914bfe77667ee1057dc6", + "bytes": 7010, + "git_blob": "371c4f68d461ed6272a3a84a2ae609919173c309", "mode": "100644", - "sha256": "320601c44824df2738278a00440d0dbf802e0573465042e205dcf946011db9d4" + "sha256": "5564da1a1f0b09059a9ab86242425720c1b52f90e1388eb96da920058231f7dd" }, "core/crates/tera_ffi/src/operations.rs": { "bytes": 37016, @@ -1577,10 +1577,10 @@ "sha256": "ec87c2cc7f396ef583d3a37186f40932ac4eb42c0e80973a5c25312099f2670a" }, "core/crates/tera_ffi/tests/media_ownership.rs": { - "bytes": 5006, - "git_blob": "2ae5e29038ec37f9d67d1035a5e9ac2fb1e6c412", + "bytes": 6567, + "git_blob": "57a09e25c1db661ecacb26afe95491f462cb52f8", "mode": "100644", - "sha256": "4dc14b48a5a201fb7afd84094ba61fbce2b0d396c19e6851df13b47085c036a2" + "sha256": "6434a8258a39e9a7a58a42d6f995a313c430238c8334a5986e4e84439e3c41e9" }, "core/crates/tera_ffi/tests/recovery_contract.rs": { "bytes": 7971, @@ -2136,6 +2136,6 @@ } }, "policy": "staged_inputs", - "tree": "abea66d625420814e307654d6777e86796424e24" + "tree": "60e714586d3acc430daee705277449abdfebacd2" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -1013,10 +1013,10 @@ "sha256": "89245c05cf6717aad83658eb77ed1bd42d1adb35d466a467041f8cb3fd0b9aba" }, "core/crates/tera_core/src/runtime/product_surface/submission/media.rs": { - "bytes": 4750, - "git_blob": "a36ba548f4e977f2d83b7924078fa733b0495b04", + "bytes": 4943, + "git_blob": "add4f6673358b1d2cedc563afa27e595223500ab", "mode": "100644", - "sha256": "9793fd4e67b178e77cb344b44213a829211bb2d51101dc6a2b2e72642402e5be" + "sha256": "1ba102fa9fb5628e9137c0c7de36eae42b423ba8beeb638f6eca02b0db7cb950" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_foreground.rs": { "bytes": 3262, @@ -1049,10 +1049,10 @@ "sha256": "cfab5909c21825d09976fcd68761114d9ab21602a2ade2a722abb65198984fbb" }, "core/crates/tera_core/src/runtime/product_surface/submission/media_tests.rs": { - "bytes": 7755, - "git_blob": "14cc026f54ebe09d4bd11fd91da1c8398830bdff", + "bytes": 9574, + "git_blob": "7c5473afac9d4a440d078e9092f8081884ac5040", "mode": "100644", - "sha256": "2f955e271cff2e20f2fc08410a32bdb62699a39814363354923714f1f3280f68" + "sha256": "97039f54d1bb6550a828a071bf2c0d165b14867d6bee72159004772e3bc28155" }, "core/crates/tera_core/src/runtime/product_surface/submission/operation.rs": { "bytes": 7701, @@ -1433,16 +1433,16 @@ "sha256": "2928947b57e21f1526f8c729f2e58c6347b061737d7fee19c2f90c32539b0dee" }, "core/crates/tera_ffi/src/media_file.rs": { - "bytes": 4731, - "git_blob": "37e5a306369e95a47559f25273a18df11c2084b6", + "bytes": 5267, + "git_blob": "5579021f7d8dd7af0c462fb74925e9f51e7c1bbf", "mode": "100644", - "sha256": "347390c081798c102926bcb412c63b760aa7bbfacbbeac41c3580677051a3416" + "sha256": "b3a9dea725cb148900000e401e3fe2a42daeab2f826f54c6db1a596e36fd81ef" }, "core/crates/tera_ffi/src/media_file_tests.rs": { - "bytes": 6109, - "git_blob": "fb7aedd53319fc37e641914bfe77667ee1057dc6", + "bytes": 7010, + "git_blob": "371c4f68d461ed6272a3a84a2ae609919173c309", "mode": "100644", - "sha256": "320601c44824df2738278a00440d0dbf802e0573465042e205dcf946011db9d4" + "sha256": "5564da1a1f0b09059a9ab86242425720c1b52f90e1388eb96da920058231f7dd" }, "core/crates/tera_ffi/src/operations.rs": { "bytes": 37016, @@ -1577,10 +1577,10 @@ "sha256": "ec87c2cc7f396ef583d3a37186f40932ac4eb42c0e80973a5c25312099f2670a" }, "core/crates/tera_ffi/tests/media_ownership.rs": { - "bytes": 5006, - "git_blob": "2ae5e29038ec37f9d67d1035a5e9ac2fb1e6c412", + "bytes": 6567, + "git_blob": "57a09e25c1db661ecacb26afe95491f462cb52f8", "mode": "100644", - "sha256": "4dc14b48a5a201fb7afd84094ba61fbce2b0d396c19e6851df13b47085c036a2" + "sha256": "6434a8258a39e9a7a58a42d6f995a313c430238c8334a5986e4e84439e3c41e9" }, "core/crates/tera_ffi/tests/recovery_contract.rs": { "bytes": 7971, @@ -2136,6 +2136,6 @@ } }, "policy": "staged_inputs", - "tree": "abea66d625420814e307654d6777e86796424e24" + "tree": "60e714586d3acc430daee705277449abdfebacd2" } } diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/media.rs b/core/crates/tera_core/src/runtime/product_surface/submission/media.rs @@ -15,6 +15,9 @@ use crate::{ runtime::product_surface::{Phase1DraftError, Phase1MediaStage}, }; +/// Match the existing native prepared-derivative bound before any upload work. +const PREPARED_MEDIA_MAX_BYTES: usize = 10 * 1024 * 1024; + #[path = "media_foreground.rs"] mod foreground; #[path = "media_runtime.rs"] @@ -41,6 +44,7 @@ impl SubmissionMediaRequest { || reference.trim() != reference || reference.chars().any(char::is_control) || bytes.is_empty() + || bytes.len() > PREPARED_MEDIA_MAX_BYTES { return Err(E::InvalidMedia); } diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/media_tests.rs b/core/crates/tera_core/src/runtime/product_surface/submission/media_tests.rs @@ -206,3 +206,63 @@ fn native_media_response_bounds_include_body_and_headers() { assert!(SubmissionMediaResponse::new(200, Some("a".into()), None, vec![0; 16_384]).is_err()); assert!(SubmissionMediaResponse::new(200, None, None, vec![0; 16_385]).is_err()); } + +#[test] +fn submission_media_bounds_match_native_derivative_admission() { + for size in [0, 10 * 1024 * 1024 + 1] { + assert!( + SubmissionMediaRequest::new( + request(), + 1, + photo().0.opaque_reference, + vec![0; size].into() + ) + .is_err() + ); + } + assert!( + SubmissionMediaRequest::new( + request(), + 1, + photo().0.opaque_reference, + vec![0; 10 * 1024 * 1024].into() + ) + .is_ok() + ); +} + +#[tokio::test] +async fn captured_media_identity_rejects_same_size_mutation_and_wrong_reference() { + let signer = CountingSigner::new(); + let runtime = runtime(None, signer.clone(), "ws://127.0.0.1:19999").await; + let request = request(); + prepare(&runtime, &request, true).await; + let initial = runtime.submission_operation_status(&request).await.unwrap(); + for wrong_reference in [false, true] { + let (photo, bytes) = photo(); + let mut bytes = bytes.to_vec(); + if !wrong_reference { + bytes[23] ^= 1; + } + let reference = if wrong_reference { + "media:another-owner".into() + } else { + photo.opaque_reference + }; + let input = + SubmissionMediaRequest::new(request.clone(), 1, reference, bytes.into()).unwrap(); + assert!( + runtime + .submission_prepare_native_upload(input) + .await + .is_err() + ); + assert_eq!( + runtime.submission_operation_status(&request).await.unwrap(), + initial + ); + assert_eq!(signer.count(), 0); + assert_eq!(signer.statuses.load(Ordering::SeqCst), 0); + } + runtime.shutdown().await.unwrap(); +} diff --git a/core/crates/tera_ffi/src/media_file.rs b/core/crates/tera_ffi/src/media_file.rs @@ -50,6 +50,15 @@ impl FfiMediaFile { impl FfiMediaFile { #[cfg(unix)] pub(crate) fn read(&self, expected_size: u64) -> Result<Vec<u8>, TeraAppError> { + self.read_with(expected_size, |file, bytes| file.read_exact_at(bytes, 0)) + } + + #[cfg(unix)] + fn read_with( + &self, + expected_size: u64, + read: impl FnOnce(&File, &mut [u8]) -> std::io::Result<()>, + ) -> Result<Vec<u8>, TeraAppError> { // File contents can change through another descriptor. Recheck the // exact size and retain the existing downstream digest/image validation. if expected_size != self.byte_size { @@ -59,9 +68,12 @@ impl FfiMediaFile { let count = usize::try_from(expected_size) .map_err(|_| TeraAppError::invalid_argument("media_size_mismatch"))?; let mut bytes = vec![0; count]; - self.file - .read_exact_at(&mut bytes, 0) + read(&self.file, &mut bytes) .map_err(|_| TeraAppError::invalid_argument("media_read_failed"))?; + // A concurrent append must not turn a valid prefix into an admitted + // complete file. Downstream hashing binds the retained exact bytes; + // this second size check also rejects growth/shrink during the read. + validate_size(&self.file, expected_size)?; Ok(bytes) } diff --git a/core/crates/tera_ffi/src/media_file_tests.rs b/core/crates/tera_ffi/src/media_file_tests.rs @@ -164,3 +164,28 @@ fn exact_size_limit_is_admitted_and_changes_are_rechecked() { "media_size_mismatch" ); } + +#[test] +fn mutation_during_read_and_partial_reads_never_return_a_valid_prefix() { + for new_size in [3, 9] { + let original = fixture(b"original"); + let admitted = owner(original.as_file()); + let error = admitted + .read_with(8, |file, bytes| { + file.read_exact_at(bytes, 0)?; + original.as_file().set_len(new_size) + }) + .unwrap_err(); + assert_eq!(error.report().code, "media_size_mismatch"); + } + let original = fixture(b"original"); + let admitted = owner(original.as_file()); + let error = admitted + .read_with(8, |file, bytes| { + file.read_exact_at(&mut bytes[..3], 0)?; + Err(std::io::ErrorKind::UnexpectedEof.into()) + }) + .unwrap_err(); + assert_eq!(error.report().code, "media_read_failed"); + assert_eq!(admitted.read(8).unwrap(), b"original"); +} diff --git a/core/crates/tera_ffi/tests/media_ownership.rs b/core/crates/tera_ffi/tests/media_ownership.rs @@ -145,3 +145,46 @@ async fn cancellation_before_first_poll_releases_only_admitted_owner() { assert!(runtime.phase1_draft_status("33".repeat(16)).await.is_err()); runtime.shutdown().await.unwrap(); } + +#[tokio::test] +async fn actual_runtime_rejects_metadata_and_file_mutation_before_persistence() { + let (_root, runtime) = support::runtime().await; + runtime + .configure_blossom( + FfiBlossomHostKind::Simulator, + FfiBlossomEndpointAuthority::LoopbackDevelopment, + "http://127.0.0.1:3000".to_owned(), + vec![], + ) + .await + .unwrap(); + for mutation in 0..7 { + let (original, mut input) = photo(); + let media = &mut input.media[0]; + match mutation { + 0 => media.byte_size += 1, + 1 => media.sha256 = Sha256::digest(b"wrong").to_hex(), + 2 => media.media_type = "image/jpeg".into(), + 3 => media.width += 1, + 4 => media.height += 1, + 5 => { + use std::os::unix::fs::FileExt; + original.as_file().write_all_at(b"BAD", 0).unwrap(); + } + _ => original.as_file().set_len(3).unwrap(), + } + let id = format!("{:032x}", mutation + 100); + let error = runtime + .phase1_save_draft(id.clone(), input, 1_800_000_000, None, 1_800_000_000_000) + .await + .unwrap_err(); + let expected = if mutation == 0 || mutation == 6 { + "media_size_mismatch" + } else { + "media_verification_failed" + }; + assert_eq!(error.report().code, expected, "mutation {mutation}"); + assert!(runtime.phase1_draft_status(id).await.is_err()); + } + runtime.shutdown().await.unwrap(); +} diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "7b3f92a57ebcc7873349403243098d5dcf993b2dbdab9ed84fea4a50c20baab0", - "ffi_provenance_sha256": "6e587ec3129d2f90dd87d269593c2f200fd13ac29f411c5a2de850be36520698", + "ffi_provenance_sha256": "e618e901823221365bdb1045ef6574be49333ab2a570642a838668d77e8c79e7", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "b202075f3c34a3a70e8745ca6d6acab6c2cf5e2d6de9a2b2647475d73a8d3f28", @@ -22,7 +22,7 @@ "lib_revision": "963dd8dc99fc5f16ef7d7eea11f8d964770a696e", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "d5c9240384b50f006ee6e9eff886a3775546280e90a05cbe781eaa296632da26", - "tera_ffi_source_tree": "abea66d625420814e307654d6777e86796424e24", + "tera_ffi_source_tree": "60e714586d3acc430daee705277449abdfebacd2", "xcode_package_lock_sha256": "b6806f710734f8e628d7a7d7b97826d89eea78fba32fa9b464f513bcadd411e2" }, "version": "0.1.0-alpha"