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:
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"