commit 39952362558f6e2ae1acdc78e966cc0f304ba2fa
parent 832523ed3652cec1dc9e657c84ffa22666375918
Author: triesap <tyson@radroots.org>
Date: Sun, 9 Aug 2026 19:12:33 +0000
merge: reconcile host descriptor ownership
- Preserve service-hardening and descriptor-fix histories.
- Duplicate borrowed media descriptors without consuming hosts.
- Reject out-of-range values before unsafe descriptor borrowing.
- Verify mobile checks, tests, clippy, and architecture guards.
Diffstat:
4 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3607,6 +3607,7 @@ version = "0.1.0-alpha"
dependencies = [
"async-trait",
"hex",
+ "libc",
"nostr 0.44.1",
"nostr-relay-builder",
"nostr-sdk 0.44.0",
diff --git a/Cargo.toml b/Cargo.toml
@@ -214,6 +214,7 @@ keyring = { version = "3.6.3", default-features = false, features = [
"linux-native-sync-persistent",
"vendored",
] }
+libc = "0.2"
nostr = { version = "0.44.7", default-features = false }
nostr-relay-pool = { version = "0.44.0" }
nostr-sdk = { version = "0.44.1" }
diff --git a/crates/mobile_ffi/Cargo.toml b/crates/mobile_ffi/Cargo.toml
@@ -35,6 +35,7 @@ thiserror = { workspace = true }
uniffi = { workspace = true, features = ["tokio"] }
[target.'cfg(unix)'.dependencies]
+libc = { workspace = true }
rustix = { workspace = true }
[dev-dependencies]
diff --git a/crates/mobile_ffi/src/dto.rs b/crates/mobile_ffi/src/dto.rs
@@ -1,6 +1,8 @@
//! Focused, versioned value types owned by the native boundary.
#[cfg(unix)]
+use std::os::fd::{FromRawFd, OwnedFd, RawFd};
+#[cfg(unix)]
use std::os::unix::fs::FileExt;
use radroots_blossom::{BlobDescriptor, MediaType, Sha256};
@@ -1115,8 +1117,21 @@ fn read_media_file_descriptor(
expected_size: u64,
byte_size: usize,
) -> Result<Vec<u8>, RadrootsAppError> {
- let file = std::fs::File::open(format!("/dev/fd/{file_descriptor}"))
+ let raw_file_descriptor = RawFd::try_from(file_descriptor)
.map_err(|_| RadrootsAppError::invalid_argument("media_handle_unavailable"))?;
+ // SAFETY: `fcntl(F_DUPFD_CLOEXEC)` accepts any in-range integer descriptor
+ // and reports EBADF for an unavailable one. No borrowed or owned Rust
+ // descriptor is constructed until the kernel has duplicated it.
+ let duplicated = unsafe { libc::fcntl(raw_file_descriptor, libc::F_DUPFD_CLOEXEC, 0) };
+ if duplicated < 0 {
+ return Err(RadrootsAppError::invalid_argument(
+ "media_handle_unavailable",
+ ));
+ }
+ // SAFETY: a nonnegative F_DUPFD_CLOEXEC result is a new descriptor owned by
+ // this call. The host's original descriptor remains independently owned.
+ let owned = unsafe { OwnedFd::from_raw_fd(duplicated) };
+ let file = std::fs::File::from(owned);
let metadata = file
.metadata()
.map_err(|_| RadrootsAppError::invalid_argument("media_handle_unavailable"))?;
@@ -2182,6 +2197,43 @@ mod tests {
media[0].url(),
format!("http://127.0.0.1:3000/{digest}.png")
);
+ assert_eq!(
+ file.as_file().metadata().expect("caller-owned media").len(),
+ bytes.len() as u64
+ );
+ }
+
+ #[test]
+ fn prepared_media_rejects_file_descriptors_outside_the_platform_range() {
+ let bytes = png(2, 2);
+ let blossom = blossom_slot();
+ let input = photo_input(u64::MAX, &bytes, Sha256::digest(&bytes).to_hex());
+
+ assert_eq!(
+ input
+ .command_and_media(1_800_000_000, Some(&blossom))
+ .expect_err("out-of-range descriptor")
+ .report()
+ .code,
+ "media_handle_unavailable"
+ );
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn prepared_media_rejects_unavailable_in_range_file_descriptors() {
+ let bytes = png(2, 2);
+ let blossom = blossom_slot();
+ let input = photo_input(i32::MAX as u64, &bytes, Sha256::digest(&bytes).to_hex());
+
+ assert_eq!(
+ input
+ .command_and_media(1_800_000_000, Some(&blossom))
+ .expect_err("unavailable in-range descriptor")
+ .report()
+ .code,
+ "media_handle_unavailable"
+ );
}
#[test]