1#![allow(clippy::missing_safety_doc)]
31#![expect(
32 clippy::undocumented_unsafe_blocks,
33 reason = "module-wide FFI safety contract documented in the # Safety preamble above"
34)]
35#![expect(
36 clippy::multiple_unsafe_ops_per_block,
37 reason = "FFI entry points routinely deref + write to multiple out-parameter fields under the same caller contract"
38)]
39
40use std::ffi::{c_char, c_int, CStr};
41use std::os::raw::c_void;
42use std::path::PathBuf;
43use std::ptr;
44use std::sync::Arc;
45
46use tokio::runtime::Runtime;
47
48#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
49use crate::adapter::net::behavior::TopologyScope;
50use crate::adapter::net::dataforts::{
51 global_blob_adapter_registry, publish_blob, resolve_payload, BlobAdapter,
52 BlobError as InnerBlobError, FileSystemAdapter,
53};
54#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
59use crate::adapter::net::dataforts::{
60 BlobRef as InnerBlobRef, MeshBlobAdapter as InnerMeshBlobAdapter,
61 OverflowConfig as InnerOverflowConfig,
62};
63
64use super::NetError;
65
66pub const NET_ERR_BLOB_DECODE: c_int = -110;
68pub const NET_ERR_BLOB_DUPLICATE_ID: c_int = -111;
70pub const NET_ERR_BLOB_NOT_REGISTERED: c_int = -112;
72pub const NET_ERR_BLOB_NOT_FOUND: c_int = -113;
74pub const NET_ERR_BLOB_HASH_MISMATCH: c_int = -114;
76pub const NET_ERR_BLOB_BACKEND: c_int = -115;
78pub const NET_ERR_BLOB_UNSUPPORTED_SCHEME: c_int = -116;
81pub const NET_ERR_BLOB_ADAPTER_NOT_CONFIGURED: c_int = -118;
83pub const NET_ERR_BLOB_ADAPTER_NOT_REGISTERED: c_int = -119;
85pub const NET_ERR_BLOB_PANIC: c_int = -117;
91pub const NET_ERR_BLOB_UNAUTHORIZED: c_int = -120;
96
97fn runtime() -> &'static Arc<Runtime> {
98 use std::sync::OnceLock;
99 static RT: OnceLock<Arc<Runtime>> = OnceLock::new();
100 RT.get_or_init(|| {
101 match tokio::runtime::Builder::new_multi_thread()
102 .enable_all()
103 .build()
104 {
105 Ok(rt) => Arc::new(rt),
106 Err(e) => {
107 eprintln!("FATAL: blob FFI tokio runtime build failure ({e:?}); aborting");
108 std::process::abort();
109 }
110 }
111 })
112}
113
114fn block_on<F: std::future::Future>(future: F) -> F::Output {
115 if tokio::runtime::Handle::try_current().is_ok() {
116 eprintln!("FATAL: blob FFI called from inside a tokio runtime context; aborting");
117 std::process::abort();
118 }
119 runtime().block_on(future)
120}
121
122unsafe fn c_str_to_owned(p: *const c_char) -> Option<String> {
123 if p.is_null() {
124 return None;
125 }
126 CStr::from_ptr(p).to_str().ok().map(|s| s.to_owned())
127}
128
129fn err_to_code(e: &InnerBlobError) -> c_int {
130 match e {
131 InnerBlobError::HashMismatch { .. } => NET_ERR_BLOB_HASH_MISMATCH,
132 InnerBlobError::NotFound(_) => NET_ERR_BLOB_NOT_FOUND,
133 InnerBlobError::Backend(_) => NET_ERR_BLOB_BACKEND,
134 InnerBlobError::Cancelled => NET_ERR_BLOB_BACKEND,
135 InnerBlobError::UnsupportedScheme(_) => NET_ERR_BLOB_UNSUPPORTED_SCHEME,
136 InnerBlobError::UnsupportedVersion(_) => NET_ERR_BLOB_DECODE,
137 InnerBlobError::Decode(_) => NET_ERR_BLOB_DECODE,
138 InnerBlobError::AdapterNotConfigured => NET_ERR_BLOB_ADAPTER_NOT_CONFIGURED,
139 InnerBlobError::AdapterNotRegistered(_) => NET_ERR_BLOB_ADAPTER_NOT_REGISTERED,
140 InnerBlobError::Unauthorized(_) => NET_ERR_BLOB_UNAUTHORIZED,
141 InnerBlobError::ShortChunk { .. } => NET_ERR_BLOB_BACKEND,
149 }
150}
151
152#[unsafe(no_mangle)]
164pub unsafe extern "C" fn net_blob_register_fs_adapter(
165 adapter_id: *const c_char,
166 root: *const c_char,
167) -> c_int {
168 let id = match c_str_to_owned(adapter_id) {
169 Some(s) => s,
170 None => return NetError::InvalidUtf8.into(),
171 };
172 let root = match c_str_to_owned(root) {
173 Some(s) => s,
174 None => return NetError::InvalidUtf8.into(),
175 };
176 let adapter: Arc<dyn BlobAdapter> =
177 Arc::new(FileSystemAdapter::new(id.clone(), PathBuf::from(root)));
178 match global_blob_adapter_registry().register(adapter) {
179 Ok(()) => 0,
180 Err(_) => NET_ERR_BLOB_DUPLICATE_ID,
181 }
182}
183
184#[unsafe(no_mangle)]
192pub unsafe extern "C" fn net_blob_unregister_adapter(adapter_id: *const c_char) -> c_int {
193 let id = match c_str_to_owned(adapter_id) {
194 Some(s) => s,
195 None => return NetError::InvalidUtf8.into(),
196 };
197 if global_blob_adapter_registry().unregister(&id).is_some() {
198 1
199 } else {
200 0
201 }
202}
203
204#[unsafe(no_mangle)]
211pub unsafe extern "C" fn net_blob_adapter_registered(adapter_id: *const c_char) -> c_int {
212 let id = match c_str_to_owned(adapter_id) {
213 Some(s) => s,
214 None => return NetError::InvalidUtf8.into(),
215 };
216 if global_blob_adapter_registry().get(&id).is_some() {
217 1
218 } else {
219 0
220 }
221}
222
223#[unsafe(no_mangle)]
240pub unsafe extern "C" fn net_blob_publish(
241 adapter_id: *const c_char,
242 uri: *const c_char,
243 data: *const u8,
244 data_len: usize,
245 out_payload: *mut *mut u8,
246 out_payload_len: *mut usize,
247) -> c_int {
248 if out_payload.is_null() || out_payload_len.is_null() {
249 return NetError::NullPointer.into();
250 }
251 *out_payload = ptr::null_mut();
252 *out_payload_len = 0;
253
254 let id = match c_str_to_owned(adapter_id) {
255 Some(s) => s,
256 None => return NetError::InvalidUtf8.into(),
257 };
258 let uri = match c_str_to_owned(uri) {
259 Some(s) => s,
260 None => return NetError::InvalidUtf8.into(),
261 };
262 if data.is_null() && data_len > 0 {
263 return NetError::NullPointer.into();
264 }
265 if data_len > isize::MAX as usize {
267 return NetError::InvalidJson.into();
268 }
269 let data_slice = if data_len == 0 {
270 &[][..]
271 } else {
272 std::slice::from_raw_parts(data, data_len)
273 };
274
275 let adapter = match global_blob_adapter_registry().get(&id) {
276 Some(a) => a,
277 None => return NET_ERR_BLOB_NOT_REGISTERED,
278 };
279 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
284 block_on(async move { publish_blob(adapter.as_ref(), uri, data_slice).await })
285 }));
286 let bytes = match result {
287 Ok(Ok(b)) => b,
288 Ok(Err(e)) => return err_to_code(&e),
289 Err(_) => return NET_ERR_BLOB_PANIC,
290 };
291
292 write_bytes_out(&bytes, out_payload, out_payload_len)
293}
294
295#[unsafe(no_mangle)]
312pub unsafe extern "C" fn net_blob_resolve(
313 adapter_id: *const c_char,
314 payload: *const u8,
315 payload_len: usize,
316 out_content: *mut *mut u8,
317 out_content_len: *mut usize,
318) -> c_int {
319 if out_content.is_null() || out_content_len.is_null() {
320 return NetError::NullPointer.into();
321 }
322 *out_content = ptr::null_mut();
323 *out_content_len = 0;
324
325 let id = match c_str_to_owned(adapter_id) {
326 Some(s) => s,
327 None => return NetError::InvalidUtf8.into(),
328 };
329 if payload.is_null() && payload_len > 0 {
330 return NetError::NullPointer.into();
331 }
332 if payload_len > isize::MAX as usize {
334 return NetError::InvalidJson.into();
335 }
336 let payload_slice = if payload_len == 0 {
337 &[][..]
338 } else {
339 std::slice::from_raw_parts(payload, payload_len)
340 };
341
342 let adapter = match global_blob_adapter_registry().get(&id) {
343 Some(a) => a,
344 None => return NET_ERR_BLOB_NOT_REGISTERED,
345 };
346 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
348 block_on(async move { resolve_payload(payload_slice, adapter.as_ref()).await })
349 }));
350 let bytes = match result {
351 Ok(Ok(b)) => b,
352 Ok(Err(e)) => return err_to_code(&e),
353 Err(_) => return NET_ERR_BLOB_PANIC,
354 };
355
356 write_bytes_out(&bytes, out_content, out_content_len)
357}
358
359unsafe fn write_bytes_out(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
373 let len = src.len();
374 if len == 0 {
375 unsafe {
376 *out_ptr = ptr::null_mut();
377 *out_len = 0;
378 }
379 return 0;
380 }
381 let layout = match std::alloc::Layout::array::<u8>(len) {
382 Ok(l) => l,
383 Err(_) => return NetError::InvalidJson.into(),
391 };
392 let alloc_ptr = unsafe { std::alloc::alloc(layout) };
393 if alloc_ptr.is_null() {
394 std::alloc::handle_alloc_error(layout);
395 }
396 unsafe {
397 std::ptr::copy_nonoverlapping(src.as_ptr(), alloc_ptr, len);
398 *out_ptr = alloc_ptr;
399 *out_len = len;
400 }
401 0
402}
403
404#[unsafe(no_mangle)]
414pub unsafe extern "C" fn net_blob_free_buffer(ptr: *mut u8, len: usize) {
415 if ptr.is_null() || len == 0 {
416 return;
417 }
418 let layout = match std::alloc::Layout::array::<u8>(len) {
424 Ok(l) => l,
425 Err(_) => return,
426 };
427 std::alloc::dealloc(ptr, layout);
428}
429
430#[allow(dead_code)]
433fn _force_use() -> *mut c_void {
434 ptr::null_mut()
435}
436
437use std::ops::Range;
445
446use async_trait::async_trait;
447use bytes::Bytes;
448
449pub type NetBlobAdapterStoreFn = unsafe extern "C" fn(
452 ctx: *mut c_void,
453 uri: *const c_char,
454 hash: *const u8, size: u64,
456 data: *const u8,
457 data_len: usize,
458) -> c_int;
459
460pub type NetBlobAdapterFetchFn = unsafe extern "C" fn(
465 ctx: *mut c_void,
466 uri: *const c_char,
467 hash: *const u8,
468 size: u64,
469 out_data: *mut *mut u8,
470 out_len: *mut usize,
471) -> c_int;
472
473pub type NetBlobAdapterFetchRangeFn = unsafe extern "C" fn(
475 ctx: *mut c_void,
476 uri: *const c_char,
477 hash: *const u8,
478 size: u64,
479 range_start: u64,
480 range_end: u64,
481 out_data: *mut *mut u8,
482 out_len: *mut usize,
483) -> c_int;
484
485pub type NetBlobAdapterExistsFn = unsafe extern "C" fn(
488 ctx: *mut c_void,
489 uri: *const c_char,
490 hash: *const u8,
491 size: u64,
492 out_exists: *mut c_int,
493) -> c_int;
494
495pub type NetBlobAdapterFreeFn = unsafe extern "C" fn(ctx: *mut c_void, data: *mut u8, len: usize);
499
500#[repr(C)]
504#[derive(Clone, Copy)]
505pub struct NetBlobAdapterVtable {
506 pub store: NetBlobAdapterStoreFn,
508 pub fetch: NetBlobAdapterFetchFn,
510 pub fetch_range: NetBlobAdapterFetchRangeFn,
512 pub exists: NetBlobAdapterExistsFn,
514 pub free_buffer: NetBlobAdapterFreeFn,
518}
519
520struct OpaqueCtx(*mut c_void);
548
549unsafe impl Send for OpaqueCtx {}
554unsafe impl Sync for OpaqueCtx {}
555
556impl OpaqueCtx {
557 fn new(ptr: *mut c_void) -> Self {
558 Self(ptr)
559 }
560 fn get(&self) -> *mut c_void {
561 self.0
562 }
563}
564
565struct CallbackBlobAdapter {
572 id: String,
573 vtable: NetBlobAdapterVtable,
574 ctx: Arc<OpaqueCtx>,
575}
576
577unsafe impl Send for CallbackBlobAdapter {}
578unsafe impl Sync for CallbackBlobAdapter {}
579
580fn code_to_err(code: c_int, label: &str) -> InnerBlobError {
581 match code {
582 NET_ERR_BLOB_NOT_FOUND => InnerBlobError::NotFound(label.into()),
583 NET_ERR_BLOB_HASH_MISMATCH => InnerBlobError::Backend(format!(
584 "{}: substrate hash mismatch (caller returned wrong bytes)",
585 label
586 )),
587 NET_ERR_BLOB_UNSUPPORTED_SCHEME => InnerBlobError::UnsupportedScheme(label.into()),
588 NET_ERR_BLOB_DECODE => InnerBlobError::Decode(label.into()),
589 _ => InnerBlobError::Backend(format!("{}: code {}", label, code)),
590 }
591}
592
593fn expect_small_for_ffi(
601 blob_ref: &crate::adapter::net::dataforts::BlobRef,
602) -> std::result::Result<(String, [u8; 32], u64), InnerBlobError> {
603 match blob_ref {
604 crate::adapter::net::dataforts::BlobRef::Small {
605 uri, hash, size, ..
606 } => Ok((uri.clone(), *hash, *size)),
607 crate::adapter::net::dataforts::BlobRef::Manifest { .. }
608 | crate::adapter::net::dataforts::BlobRef::Tree { .. } => Err(InnerBlobError::Backend(
609 "CallbackBlobAdapter operates on Small blobs only; \
610 chunked blobs are dispatched at the substrate above"
611 .to_owned(),
612 )),
613 }
614}
615
616#[async_trait]
617impl BlobAdapter for CallbackBlobAdapter {
618 fn adapter_id(&self) -> &str {
619 &self.id
620 }
621
622 async fn store(
623 &self,
624 blob_ref: &crate::adapter::net::dataforts::BlobRef,
625 bytes: &[u8],
626 ) -> std::result::Result<(), InnerBlobError> {
627 let vtable = self.vtable;
628 let ctx = self.ctx.clone();
629 let (uri_str, hash, size) = expect_small_for_ffi(blob_ref)?;
630 let uri = match std::ffi::CString::new(uri_str) {
631 Ok(c) => c,
632 Err(e) => return Err(InnerBlobError::Backend(format!("uri NUL: {}", e))),
633 };
634 let data = bytes.to_vec();
635 tokio::task::spawn_blocking(move || -> std::result::Result<(), InnerBlobError> {
636 let code = unsafe {
637 (vtable.store)(
638 ctx.get(),
639 uri.as_ptr(),
640 hash.as_ptr(),
641 size,
642 data.as_ptr(),
643 data.len(),
644 )
645 };
646 if code == 0 {
647 Ok(())
648 } else {
649 Err(code_to_err(code, "store"))
650 }
651 })
652 .await
653 .map_err(|e| InnerBlobError::Backend(format!("spawn_blocking join: {}", e)))?
654 }
655
656 async fn fetch(
657 &self,
658 blob_ref: &crate::adapter::net::dataforts::BlobRef,
659 ) -> std::result::Result<Bytes, InnerBlobError> {
660 let vtable = self.vtable;
661 let ctx = self.ctx.clone();
662 let (uri_str, hash, size) = expect_small_for_ffi(blob_ref)?;
663 let uri = match std::ffi::CString::new(uri_str) {
664 Ok(c) => c,
665 Err(e) => return Err(InnerBlobError::Backend(format!("uri NUL: {}", e))),
666 };
667 tokio::task::spawn_blocking(move || -> std::result::Result<Bytes, InnerBlobError> {
668 let mut out_data: *mut u8 = ptr::null_mut();
669 let mut out_len: usize = 0;
670 let code = unsafe {
671 (vtable.fetch)(
672 ctx.get(),
673 uri.as_ptr(),
674 hash.as_ptr(),
675 size,
676 &mut out_data,
677 &mut out_len,
678 )
679 };
680 if code != 0 {
681 return Err(code_to_err(code, "fetch"));
682 }
683 if out_data.is_null() {
684 if out_len == 0 {
685 return Ok(Bytes::new());
686 }
687 return Err(InnerBlobError::Backend(
688 "fetch: caller returned null pointer with non-zero len".into(),
689 ));
690 }
691 let buf = unsafe { std::slice::from_raw_parts(out_data, out_len).to_vec() };
700 unsafe { (vtable.free_buffer)(ctx.get(), out_data, out_len) };
701 Ok(Bytes::from(buf))
702 })
703 .await
704 .map_err(|e| InnerBlobError::Backend(format!("spawn_blocking join: {}", e)))?
705 }
706
707 async fn fetch_range(
708 &self,
709 blob_ref: &crate::adapter::net::dataforts::BlobRef,
710 range: Range<u64>,
711 ) -> std::result::Result<Bytes, InnerBlobError> {
712 let vtable = self.vtable;
713 let ctx = self.ctx.clone();
714 let (uri_str, hash, size) = expect_small_for_ffi(blob_ref)?;
715 let uri = match std::ffi::CString::new(uri_str) {
716 Ok(c) => c,
717 Err(e) => return Err(InnerBlobError::Backend(format!("uri NUL: {}", e))),
718 };
719 let start = range.start;
720 let end = range.end;
721 tokio::task::spawn_blocking(move || -> std::result::Result<Bytes, InnerBlobError> {
722 let mut out_data: *mut u8 = ptr::null_mut();
723 let mut out_len: usize = 0;
724 let code = unsafe {
725 (vtable.fetch_range)(
726 ctx.get(),
727 uri.as_ptr(),
728 hash.as_ptr(),
729 size,
730 start,
731 end,
732 &mut out_data,
733 &mut out_len,
734 )
735 };
736 if code != 0 {
737 return Err(code_to_err(code, "fetch_range"));
738 }
739 if out_data.is_null() {
740 if out_len == 0 {
741 return Ok(Bytes::new());
742 }
743 return Err(InnerBlobError::Backend(
744 "fetch_range: caller returned null pointer with non-zero len".into(),
745 ));
746 }
747 let buf = unsafe { std::slice::from_raw_parts(out_data, out_len).to_vec() };
748 unsafe { (vtable.free_buffer)(ctx.get(), out_data, out_len) };
749 Ok(Bytes::from(buf))
750 })
751 .await
752 .map_err(|e| InnerBlobError::Backend(format!("spawn_blocking join: {}", e)))?
753 }
754
755 async fn exists(
756 &self,
757 blob_ref: &crate::adapter::net::dataforts::BlobRef,
758 ) -> std::result::Result<bool, InnerBlobError> {
759 let vtable = self.vtable;
760 let ctx = self.ctx.clone();
761 let (uri_str, hash, size) = expect_small_for_ffi(blob_ref)?;
762 let uri = match std::ffi::CString::new(uri_str) {
763 Ok(c) => c,
764 Err(e) => return Err(InnerBlobError::Backend(format!("uri NUL: {}", e))),
765 };
766 tokio::task::spawn_blocking(move || -> std::result::Result<bool, InnerBlobError> {
767 let mut out_exists: c_int = 0;
768 let code = unsafe {
769 (vtable.exists)(
770 ctx.get(),
771 uri.as_ptr(),
772 hash.as_ptr(),
773 size,
774 &mut out_exists,
775 )
776 };
777 if code != 0 {
778 return Err(code_to_err(code, "exists"));
779 }
780 Ok(out_exists != 0)
781 })
782 .await
783 .map_err(|e| InnerBlobError::Backend(format!("spawn_blocking join: {}", e)))?
784 }
785}
786
787#[unsafe(no_mangle)]
827pub unsafe extern "C" fn net_blob_register_callback_adapter(
828 adapter_id: *const c_char,
829 vtable: *const NetBlobAdapterVtable,
830 ctx: *mut c_void,
831) -> c_int {
832 if vtable.is_null() {
833 return NetError::NullPointer.into();
834 }
835 let id = match c_str_to_owned(adapter_id) {
836 Some(s) => s,
837 None => return NetError::InvalidUtf8.into(),
838 };
839 {
846 let raw = vtable as *const c_void as *const *const c_void;
847 for i in 0..5 {
852 let field = unsafe { *raw.add(i) };
853 if field.is_null() {
854 return NET_ERR_BLOB_BACKEND;
855 }
856 }
857 }
858 let vtable = unsafe { *vtable };
859 let adapter: Arc<dyn BlobAdapter> = Arc::new(CallbackBlobAdapter {
860 id: id.clone(),
861 vtable,
862 ctx: Arc::new(OpaqueCtx::new(ctx)),
863 });
864 match global_blob_adapter_registry().register(adapter) {
865 Ok(()) => 0,
866 Err(_) => NET_ERR_BLOB_DUPLICATE_ID,
867 }
868}
869
870#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
893pub struct MeshBlobAdapterHandle {
894 inner: ManuallyDrop<Arc<InnerMeshBlobAdapter>>,
895 guard: HandleGuard,
896}
897
898#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
899use std::mem::ManuallyDrop;
900
901#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
902use super::handle_guard::{HandleGuard, FFI_HANDLE_FREE_DEADLINE};
903
904#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
911#[inline]
912fn adapter_guard<R>(name: &'static str, fallback: R, f: impl FnOnce() -> R) -> R {
913 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
914 Ok(v) => v,
915 Err(_) => {
916 tracing::error!(
917 ffi_function = name,
918 "panic caught in mesh blob adapter FFI; returning fallback to avoid \
919 UB across the C boundary",
920 );
921 fallback
922 }
923 }
924}
925
926#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
931#[derive(serde::Deserialize, serde::Serialize)]
932struct OverflowConfigJson {
933 #[serde(default)]
934 enabled: bool,
935 #[serde(default)]
936 high_water_ratio: Option<f64>,
937 #[serde(default)]
938 low_water_ratio: Option<f64>,
939 #[serde(default)]
940 max_pushes_per_tick: Option<u64>,
941 #[serde(default)]
942 scope: Option<String>,
943 #[serde(default)]
944 tick_interval_ms: Option<u64>,
945}
946
947#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
948fn parse_overflow_json(s: &str) -> Result<InnerOverflowConfig, c_int> {
949 if s.is_empty() {
950 return Ok(InnerOverflowConfig::default());
951 }
952 let raw: OverflowConfigJson =
953 serde_json::from_str(s).map_err(|_| -> c_int { NetError::InvalidJson.into() })?;
954 let mut cfg = InnerOverflowConfig {
955 enabled: raw.enabled,
956 ..InnerOverflowConfig::default()
957 };
958 if let Some(v) = raw.high_water_ratio {
959 cfg.high_water_ratio = v;
960 }
961 if let Some(v) = raw.low_water_ratio {
962 cfg.low_water_ratio = v;
963 }
964 if let Some(v) = raw.max_pushes_per_tick {
965 cfg.max_pushes_per_tick = v as usize;
966 }
967 if let Some(s) = raw.scope {
968 cfg.scope = match s.to_ascii_lowercase().as_str() {
969 "node" => TopologyScope::Node,
970 "zone" => TopologyScope::Zone,
971 "region" => TopologyScope::Region,
972 "mesh" => TopologyScope::Mesh,
973 _ => {
974 let code: c_int = NetError::InvalidJson.into();
975 return Err(code);
976 }
977 };
978 }
979 if let Some(v) = raw.tick_interval_ms {
980 cfg.tick_interval_ms = v;
981 }
982 Ok(cfg)
983}
984
985#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
986fn overflow_to_json(cfg: InnerOverflowConfig) -> String {
987 let scope = match cfg.scope {
988 TopologyScope::Node => "node",
989 TopologyScope::Zone => "zone",
990 TopologyScope::Region => "region",
991 TopologyScope::Mesh => "mesh",
992 };
993 let raw = OverflowConfigJson {
994 enabled: cfg.enabled,
995 high_water_ratio: Some(cfg.high_water_ratio),
996 low_water_ratio: Some(cfg.low_water_ratio),
997 max_pushes_per_tick: Some(cfg.max_pushes_per_tick as u64),
998 scope: Some(scope.to_string()),
999 tick_interval_ms: Some(cfg.tick_interval_ms),
1000 };
1001 serde_json::to_string(&raw).unwrap_or_else(|_| "{}".to_string())
1002}
1003
1004#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1026#[unsafe(no_mangle)]
1027pub unsafe extern "C" fn net_mesh_blob_adapter_new(
1028 redex: *mut super::cortex::RedexHandle,
1029 adapter_id: *const c_char,
1030 persistent: c_int,
1031 overflow_json: *const c_char,
1032) -> *mut MeshBlobAdapterHandle {
1033 if redex.is_null() {
1034 return ptr::null_mut();
1035 }
1036 let id = match unsafe { c_str_to_owned(adapter_id) } {
1037 Some(s) => s,
1038 None => return ptr::null_mut(),
1039 };
1040 let overflow_str = if overflow_json.is_null() {
1041 String::new()
1042 } else {
1043 match unsafe { c_str_to_owned(overflow_json) } {
1044 Some(s) => s,
1045 None => return ptr::null_mut(),
1046 }
1047 };
1048 let overflow_cfg = match parse_overflow_json(&overflow_str) {
1049 Ok(c) => c,
1050 Err(_) => return ptr::null_mut(),
1051 };
1052 let Some(redex_inner) = (unsafe { (*redex).redex_arc() }) else {
1056 return ptr::null_mut();
1057 };
1058 let mut builder = InnerMeshBlobAdapter::new(id, redex_inner).with_persistent(persistent != 0);
1059 if !overflow_str.is_empty() {
1060 builder = builder.with_overflow(overflow_cfg);
1061 }
1062 Box::into_raw(Box::new(MeshBlobAdapterHandle {
1063 inner: ManuallyDrop::new(Arc::new(builder)),
1064 guard: HandleGuard::new(),
1065 }))
1066}
1067
1068#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1075#[unsafe(no_mangle)]
1076pub unsafe extern "C" fn net_mesh_blob_adapter_free(handle: *mut MeshBlobAdapterHandle) {
1077 if handle.is_null() {
1078 return;
1079 }
1080 let h: &MeshBlobAdapterHandle = unsafe { &*handle };
1084 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1085 unsafe {
1088 let inner = ManuallyDrop::take(&mut (*handle).inner);
1089 drop(inner);
1090 }
1091 } else {
1092 tracing::warn!(
1093 "net_mesh_blob_adapter_free: in-flight ops did not drain within deadline; \
1094 leaking inner to avoid use-after-free"
1095 );
1096 }
1097}
1098
1099#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1107pub(super) fn blob_adapter_arc(h: &MeshBlobAdapterHandle) -> Option<Arc<InnerMeshBlobAdapter>> {
1108 let _op = h.guard.try_enter()?;
1109 Some(Arc::clone(&h.inner))
1110}
1111
1112#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1125#[unsafe(no_mangle)]
1126pub unsafe extern "C" fn net_mesh_blob_adapter_store(
1127 handle: *const MeshBlobAdapterHandle,
1128 blob_ref_bytes: *const u8,
1129 blob_ref_len: usize,
1130 data: *const u8,
1131 data_len: usize,
1132) -> c_int {
1133 let null_rc: c_int = NetError::NullPointer.into();
1134 adapter_guard("net_mesh_blob_adapter_store", null_rc, || {
1135 if handle.is_null() || blob_ref_bytes.is_null() {
1136 return NetError::NullPointer.into();
1137 }
1138 if blob_ref_len > isize::MAX as usize || data_len > isize::MAX as usize {
1140 return NetError::InvalidJson.into();
1141 }
1142 let h = unsafe { &*handle };
1143 let _op = match h.guard.try_enter() {
1145 Some(op) => op,
1146 None => return NetError::NullPointer.into(),
1147 };
1148 let blob_slice = unsafe { std::slice::from_raw_parts(blob_ref_bytes, blob_ref_len) };
1149 let blob_ref = match InnerBlobRef::decode(blob_slice) {
1150 Ok(Some(b)) => b,
1151 _ => return NET_ERR_BLOB_DECODE,
1152 };
1153 let data_slice = if data.is_null() {
1154 &[]
1155 } else {
1156 unsafe { std::slice::from_raw_parts(data, data_len) }
1157 };
1158 let adapter = h.inner.clone();
1159 let data_owned = data_slice.to_vec();
1160 let result = block_on(async move { (*adapter).store(&blob_ref, &data_owned).await });
1161 match result {
1162 Ok(()) => 0,
1163 Err(e) => err_to_code(&e),
1164 }
1165 })
1166}
1167
1168#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1177#[unsafe(no_mangle)]
1178pub unsafe extern "C" fn net_mesh_blob_adapter_fetch(
1179 handle: *const MeshBlobAdapterHandle,
1180 blob_ref_bytes: *const u8,
1181 blob_ref_len: usize,
1182 out_data: *mut *mut u8,
1183 out_len: *mut usize,
1184) -> c_int {
1185 let null_rc: c_int = NetError::NullPointer.into();
1186 adapter_guard("net_mesh_blob_adapter_fetch", null_rc, || {
1187 if handle.is_null() || blob_ref_bytes.is_null() || out_data.is_null() || out_len.is_null() {
1188 return NetError::NullPointer.into();
1189 }
1190 if blob_ref_len > isize::MAX as usize {
1192 return NetError::InvalidJson.into();
1193 }
1194 let h = unsafe { &*handle };
1195 let _op = match h.guard.try_enter() {
1196 Some(op) => op,
1197 None => return NetError::NullPointer.into(),
1198 };
1199 let blob_slice = unsafe { std::slice::from_raw_parts(blob_ref_bytes, blob_ref_len) };
1200 let blob_ref = match InnerBlobRef::decode(blob_slice) {
1201 Ok(Some(b)) => b,
1202 _ => return NET_ERR_BLOB_DECODE,
1203 };
1204 let adapter = h.inner.clone();
1205 let result = block_on(async move { (*adapter).fetch(&blob_ref).await });
1206 match result {
1207 Ok(bytes) => unsafe { write_bytes_out(&bytes, out_data, out_len) },
1213 Err(e) => err_to_code(&e),
1214 }
1215 })
1216}
1217
1218#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1225#[unsafe(no_mangle)]
1226pub unsafe extern "C" fn net_mesh_blob_adapter_exists(
1227 handle: *const MeshBlobAdapterHandle,
1228 blob_ref_bytes: *const u8,
1229 blob_ref_len: usize,
1230 out_exists: *mut c_int,
1231) -> c_int {
1232 let null_rc: c_int = NetError::NullPointer.into();
1233 adapter_guard("net_mesh_blob_adapter_exists", null_rc, || {
1234 if handle.is_null() || blob_ref_bytes.is_null() || out_exists.is_null() {
1235 return NetError::NullPointer.into();
1236 }
1237 if blob_ref_len > isize::MAX as usize {
1239 return NetError::InvalidJson.into();
1240 }
1241 let h = unsafe { &*handle };
1242 let _op = match h.guard.try_enter() {
1243 Some(op) => op,
1244 None => return NetError::NullPointer.into(),
1245 };
1246 let blob_slice = unsafe { std::slice::from_raw_parts(blob_ref_bytes, blob_ref_len) };
1247 let blob_ref = match InnerBlobRef::decode(blob_slice) {
1248 Ok(Some(b)) => b,
1249 _ => return NET_ERR_BLOB_DECODE,
1250 };
1251 let adapter = h.inner.clone();
1252 let result = block_on(async move { (*adapter).exists(&blob_ref).await });
1253 match result {
1254 Ok(present) => {
1255 unsafe { *out_exists = if present { 1 } else { 0 } };
1256 0
1257 }
1258 Err(e) => err_to_code(&e),
1259 }
1260 })
1261}
1262
1263#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1271#[unsafe(no_mangle)]
1272pub unsafe extern "C" fn net_mesh_blob_adapter_prometheus_text(
1273 handle: *const MeshBlobAdapterHandle,
1274) -> *mut c_char {
1275 adapter_guard(
1276 "net_mesh_blob_adapter_prometheus_text",
1277 ptr::null_mut(),
1278 || {
1279 if handle.is_null() {
1280 return ptr::null_mut();
1281 }
1282 let h = unsafe { &*handle };
1283 let _op = match h.guard.try_enter() {
1284 Some(op) => op,
1285 None => return ptr::null_mut(),
1286 };
1287 let adapter = h.inner.clone();
1288 let body = (*adapter).prometheus_text();
1289 match std::ffi::CString::new(body) {
1290 Ok(s) => s.into_raw(),
1291 Err(_) => ptr::null_mut(),
1292 }
1293 },
1294 )
1295}
1296
1297#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1305#[unsafe(no_mangle)]
1306pub unsafe extern "C" fn net_mesh_blob_adapter_overflow_enabled(
1307 handle: *const MeshBlobAdapterHandle,
1308) -> c_int {
1309 let null_rc: c_int = NetError::NullPointer.into();
1310 adapter_guard("net_mesh_blob_adapter_overflow_enabled", null_rc, || {
1311 if handle.is_null() {
1312 return NetError::NullPointer.into();
1313 }
1314 let h = unsafe { &*handle };
1315 let _op = match h.guard.try_enter() {
1316 Some(op) => op,
1317 None => return NetError::NullPointer.into(),
1318 };
1319 let adapter = h.inner.clone();
1320 if (*adapter).overflow_enabled() {
1321 1
1322 } else {
1323 0
1324 }
1325 })
1326}
1327
1328#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1334#[unsafe(no_mangle)]
1335pub unsafe extern "C" fn net_mesh_blob_adapter_overflow_active(
1336 handle: *const MeshBlobAdapterHandle,
1337) -> c_int {
1338 let null_rc: c_int = NetError::NullPointer.into();
1339 adapter_guard("net_mesh_blob_adapter_overflow_active", null_rc, || {
1340 if handle.is_null() {
1341 return NetError::NullPointer.into();
1342 }
1343 let h = unsafe { &*handle };
1344 let _op = match h.guard.try_enter() {
1345 Some(op) => op,
1346 None => return NetError::NullPointer.into(),
1347 };
1348 let adapter = h.inner.clone();
1349 if (*adapter).overflow_active() {
1350 1
1351 } else {
1352 0
1353 }
1354 })
1355}
1356
1357#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1364#[unsafe(no_mangle)]
1365pub unsafe extern "C" fn net_mesh_blob_adapter_overflow_config(
1366 handle: *const MeshBlobAdapterHandle,
1367) -> *mut c_char {
1368 adapter_guard(
1369 "net_mesh_blob_adapter_overflow_config",
1370 ptr::null_mut(),
1371 || {
1372 if handle.is_null() {
1373 return ptr::null_mut();
1374 }
1375 let h = unsafe { &*handle };
1376 let _op = match h.guard.try_enter() {
1377 Some(op) => op,
1378 None => return ptr::null_mut(),
1379 };
1380 let adapter = h.inner.clone();
1381 let cfg = (*adapter).overflow_config();
1382 let json = overflow_to_json(cfg);
1383 match std::ffi::CString::new(json) {
1384 Ok(s) => s.into_raw(),
1385 Err(_) => ptr::null_mut(),
1386 }
1387 },
1388 )
1389}
1390
1391#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1397#[unsafe(no_mangle)]
1398pub unsafe extern "C" fn net_mesh_blob_adapter_set_overflow_enabled(
1399 handle: *const MeshBlobAdapterHandle,
1400 enabled: c_int,
1401) -> c_int {
1402 let null_rc: c_int = NetError::NullPointer.into();
1403 adapter_guard(
1404 "net_mesh_blob_adapter_set_overflow_enabled",
1405 null_rc,
1406 || {
1407 if handle.is_null() {
1408 return NetError::NullPointer.into();
1409 }
1410 let h = unsafe { &*handle };
1411 let _op = match h.guard.try_enter() {
1412 Some(op) => op,
1413 None => return NetError::NullPointer.into(),
1414 };
1415 let adapter = h.inner.clone();
1416 (*adapter).set_overflow_enabled(enabled != 0);
1417 0
1418 },
1419 )
1420}
1421
1422#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1430#[unsafe(no_mangle)]
1431pub unsafe extern "C" fn net_mesh_blob_adapter_set_overflow_config(
1432 handle: *const MeshBlobAdapterHandle,
1433 config_json: *const c_char,
1434) -> c_int {
1435 let null_rc: c_int = NetError::NullPointer.into();
1436 adapter_guard("net_mesh_blob_adapter_set_overflow_config", null_rc, || {
1437 if handle.is_null() || config_json.is_null() {
1438 return NetError::NullPointer.into();
1439 }
1440 let s = match unsafe { c_str_to_owned(config_json) } {
1441 Some(s) => s,
1442 None => return NetError::InvalidUtf8.into(),
1443 };
1444 let cfg = match parse_overflow_json(&s) {
1445 Ok(c) => c,
1446 Err(code) => return code,
1447 };
1448 let h = unsafe { &*handle };
1449 let _op = match h.guard.try_enter() {
1450 Some(op) => op,
1451 None => return NetError::NullPointer.into(),
1452 };
1453 let adapter = h.inner.clone();
1454 (*adapter).set_overflow_config(cfg);
1455 0
1456 })
1457}
1458
1459#[cfg(test)]
1460mod tests {
1461 #![allow(
1462 clippy::disallowed_methods,
1463 reason = "test code legitimately uses std::sync::{Mutex,RwLock} for SUT setup; tests have no real poison concern"
1464 )]
1465 use super::*;
1466 use std::ffi::CString;
1467 use std::sync::atomic::{AtomicU64, Ordering};
1468
1469 fn unique_id(prefix: &str) -> String {
1470 static N: AtomicU64 = AtomicU64::new(0);
1471 let n = N.fetch_add(1, Ordering::Relaxed);
1472 format!("{}-{}-{}", prefix, std::process::id(), n)
1473 }
1474
1475 #[test]
1478 fn ffi_publish_resolve_round_trip() {
1479 let id = unique_id("ffi-blob");
1480 let root = std::env::temp_dir().join(format!("net-ffi-blob-{}", id));
1481 let id_c = CString::new(id.clone()).unwrap();
1482 let root_c = CString::new(root.to_string_lossy().as_ref()).unwrap();
1483 let uri_c = CString::new("file:///ffi-round-trip").unwrap();
1484
1485 unsafe {
1486 assert_eq!(
1487 net_blob_register_fs_adapter(id_c.as_ptr(), root_c.as_ptr()),
1488 0
1489 );
1490 assert_eq!(net_blob_adapter_registered(id_c.as_ptr()), 1);
1491
1492 let payload = b"end-to-end ffi blob round trip";
1493 let mut out_buf: *mut u8 = std::ptr::null_mut();
1494 let mut out_len: usize = 0;
1495 let rc = net_blob_publish(
1496 id_c.as_ptr(),
1497 uri_c.as_ptr(),
1498 payload.as_ptr(),
1499 payload.len(),
1500 &mut out_buf,
1501 &mut out_len,
1502 );
1503 assert_eq!(rc, 0);
1504 assert!(!out_buf.is_null());
1505 let encoded = std::slice::from_raw_parts(out_buf, out_len);
1507 assert_eq!(
1508 &encoded[..4],
1509 &crate::adapter::net::dataforts::BLOB_REF_MAGIC,
1510 );
1511
1512 let mut content_buf: *mut u8 = std::ptr::null_mut();
1514 let mut content_len: usize = 0;
1515 let rc = net_blob_resolve(
1516 id_c.as_ptr(),
1517 out_buf,
1518 out_len,
1519 &mut content_buf,
1520 &mut content_len,
1521 );
1522 assert_eq!(rc, 0);
1523 let resolved = std::slice::from_raw_parts(content_buf, content_len);
1524 assert_eq!(resolved, payload);
1525
1526 net_blob_free_buffer(out_buf, out_len);
1527 net_blob_free_buffer(content_buf, content_len);
1528 assert_eq!(net_blob_unregister_adapter(id_c.as_ptr()), 1);
1529 }
1530 let _ = std::fs::remove_dir_all(&root);
1531 }
1532
1533 #[test]
1534 fn ffi_resolve_returns_not_registered_for_unknown_adapter() {
1535 let id_c = CString::new("never-registered").unwrap();
1536 let payload = b"any";
1537 let mut out_buf: *mut u8 = std::ptr::null_mut();
1538 let mut out_len: usize = 0;
1539 let rc = unsafe {
1540 net_blob_resolve(
1541 id_c.as_ptr(),
1542 payload.as_ptr(),
1543 payload.len(),
1544 &mut out_buf,
1545 &mut out_len,
1546 )
1547 };
1548 assert_eq!(rc, NET_ERR_BLOB_NOT_REGISTERED);
1549 assert!(out_buf.is_null());
1550 assert_eq!(out_len, 0);
1551 }
1552
1553 mod callback_adapter_round_trip {
1559 use super::*;
1560 use std::collections::HashMap;
1561 use std::sync::Mutex;
1562
1563 struct CallbackCtx {
1564 store: Mutex<HashMap<[u8; 32], Vec<u8>>>,
1565 }
1566
1567 unsafe extern "C" fn cb_store(
1568 ctx: *mut c_void,
1569 _uri: *const c_char,
1570 hash: *const u8,
1571 _size: u64,
1572 data: *const u8,
1573 data_len: usize,
1574 ) -> c_int {
1575 let ctx = &*(ctx as *const CallbackCtx);
1576 let mut h = [0u8; 32];
1577 h.copy_from_slice(std::slice::from_raw_parts(hash, 32));
1578 let buf = if data_len == 0 {
1579 Vec::new()
1580 } else {
1581 std::slice::from_raw_parts(data, data_len).to_vec()
1582 };
1583 ctx.store.lock().unwrap().insert(h, buf);
1584 0
1585 }
1586
1587 unsafe extern "C" fn cb_fetch(
1588 ctx: *mut c_void,
1589 _uri: *const c_char,
1590 hash: *const u8,
1591 _size: u64,
1592 out_data: *mut *mut u8,
1593 out_len: *mut usize,
1594 ) -> c_int {
1595 let ctx = &*(ctx as *const CallbackCtx);
1596 let mut h = [0u8; 32];
1597 h.copy_from_slice(std::slice::from_raw_parts(hash, 32));
1598 let store = ctx.store.lock().unwrap();
1599 match store.get(&h) {
1600 Some(bytes) => {
1601 let boxed = bytes.clone().into_boxed_slice();
1602 let len = boxed.len();
1603 let ptr = Box::into_raw(boxed) as *mut u8;
1604 *out_data = ptr;
1605 *out_len = len;
1606 0
1607 }
1608 None => NET_ERR_BLOB_NOT_FOUND,
1609 }
1610 }
1611
1612 unsafe extern "C" fn cb_fetch_range(
1613 ctx: *mut c_void,
1614 _uri: *const c_char,
1615 hash: *const u8,
1616 _size: u64,
1617 range_start: u64,
1618 range_end: u64,
1619 out_data: *mut *mut u8,
1620 out_len: *mut usize,
1621 ) -> c_int {
1622 let ctx = &*(ctx as *const CallbackCtx);
1623 let mut h = [0u8; 32];
1624 h.copy_from_slice(std::slice::from_raw_parts(hash, 32));
1625 let store = ctx.store.lock().unwrap();
1626 match store.get(&h) {
1627 Some(bytes) => {
1628 let s = range_start as usize;
1629 let e = range_end as usize;
1630 if s > e || e > bytes.len() {
1631 return NET_ERR_BLOB_BACKEND;
1632 }
1633 let slice = bytes[s..e].to_vec().into_boxed_slice();
1634 let len = slice.len();
1635 *out_data = Box::into_raw(slice) as *mut u8;
1636 *out_len = len;
1637 0
1638 }
1639 None => NET_ERR_BLOB_NOT_FOUND,
1640 }
1641 }
1642
1643 unsafe extern "C" fn cb_exists(
1644 ctx: *mut c_void,
1645 _uri: *const c_char,
1646 hash: *const u8,
1647 _size: u64,
1648 out_exists: *mut c_int,
1649 ) -> c_int {
1650 let ctx = &*(ctx as *const CallbackCtx);
1651 let mut h = [0u8; 32];
1652 h.copy_from_slice(std::slice::from_raw_parts(hash, 32));
1653 *out_exists = if ctx.store.lock().unwrap().contains_key(&h) {
1654 1
1655 } else {
1656 0
1657 };
1658 0
1659 }
1660
1661 unsafe extern "C" fn cb_free(_ctx: *mut c_void, data: *mut u8, len: usize) {
1662 if data.is_null() {
1663 return;
1664 }
1665 let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(data, len));
1666 }
1667
1668 #[test]
1669 fn callback_adapter_publish_resolve_round_trip() {
1670 let ctx = Box::new(CallbackCtx {
1671 store: Mutex::new(HashMap::new()),
1672 });
1673 let ctx_ptr = Box::into_raw(ctx) as *mut c_void;
1674 let vtable = NetBlobAdapterVtable {
1675 store: cb_store,
1676 fetch: cb_fetch,
1677 fetch_range: cb_fetch_range,
1678 exists: cb_exists,
1679 free_buffer: cb_free,
1680 };
1681
1682 let id_c = std::ffi::CString::new("ffi-cb-roundtrip").unwrap();
1683 let uri_c = std::ffi::CString::new("cb://round-trip").unwrap();
1684 unsafe {
1685 assert_eq!(
1686 net_blob_register_callback_adapter(id_c.as_ptr(), &vtable, ctx_ptr),
1687 0
1688 );
1689
1690 let payload = b"vtable round-trip payload";
1691 let mut out_buf: *mut u8 = std::ptr::null_mut();
1692 let mut out_len: usize = 0;
1693 let rc = net_blob_publish(
1694 id_c.as_ptr(),
1695 uri_c.as_ptr(),
1696 payload.as_ptr(),
1697 payload.len(),
1698 &mut out_buf,
1699 &mut out_len,
1700 );
1701 assert_eq!(rc, 0);
1702
1703 let mut content_buf: *mut u8 = std::ptr::null_mut();
1704 let mut content_len: usize = 0;
1705 let rc = net_blob_resolve(
1706 id_c.as_ptr(),
1707 out_buf,
1708 out_len,
1709 &mut content_buf,
1710 &mut content_len,
1711 );
1712 assert_eq!(rc, 0);
1713 let resolved = std::slice::from_raw_parts(content_buf, content_len);
1714 assert_eq!(resolved, payload);
1715
1716 net_blob_free_buffer(out_buf, out_len);
1717 net_blob_free_buffer(content_buf, content_len);
1718 assert_eq!(net_blob_unregister_adapter(id_c.as_ptr()), 1);
1719
1720 drop(Box::from_raw(ctx_ptr as *mut CallbackCtx));
1722 }
1723 }
1724 }
1725
1726 #[test]
1727 fn ffi_duplicate_registration_rejected() {
1728 let id = unique_id("ffi-dup");
1729 let root = std::env::temp_dir().join(format!("net-ffi-blob-{}", id));
1730 let id_c = CString::new(id.clone()).unwrap();
1731 let root_c = CString::new(root.to_string_lossy().as_ref()).unwrap();
1732 unsafe {
1733 assert_eq!(
1734 net_blob_register_fs_adapter(id_c.as_ptr(), root_c.as_ptr()),
1735 0
1736 );
1737 assert_eq!(
1738 net_blob_register_fs_adapter(id_c.as_ptr(), root_c.as_ptr()),
1739 NET_ERR_BLOB_DUPLICATE_ID
1740 );
1741 assert_eq!(net_blob_unregister_adapter(id_c.as_ptr()), 1);
1742 }
1743 let _ = std::fs::remove_dir_all(&root);
1744 }
1745
1746 #[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1760 #[test]
1761 fn blob_adapter_ops_after_free_bail_and_double_free_is_safe() {
1762 use crate::ffi::cortex::{net_redex_free, net_redex_new};
1763
1764 let null_rc: c_int = NetError::NullPointer.into();
1765 let id_c = CString::new(unique_id("ffi-blob-adapter-uaf")).unwrap();
1766
1767 unsafe {
1768 let redex = net_redex_new(std::ptr::null());
1770 assert!(!redex.is_null());
1771
1772 let adapter = net_mesh_blob_adapter_new(redex, id_c.as_ptr(), 0, std::ptr::null());
1774 assert!(!adapter.is_null(), "adapter must construct");
1775
1776 let live = net_mesh_blob_adapter_overflow_enabled(adapter);
1778 assert!(live == 0 || live == 1, "live overflow_enabled in {{0,1}}");
1779
1780 net_mesh_blob_adapter_free(adapter);
1782
1783 assert_eq!(
1787 net_mesh_blob_adapter_overflow_enabled(adapter),
1788 null_rc,
1789 "op on freed handle must return the null-pointer bail code",
1790 );
1791 assert_eq!(net_mesh_blob_adapter_overflow_active(adapter), null_rc);
1792 assert!(
1793 net_mesh_blob_adapter_prometheus_text(adapter).is_null(),
1794 "ptr-returning op on freed handle must return null",
1795 );
1796 let blob_ref = [0u8; 4];
1797 assert_eq!(
1798 net_mesh_blob_adapter_store(
1799 adapter,
1800 blob_ref.as_ptr(),
1801 blob_ref.len(),
1802 std::ptr::null(),
1803 0,
1804 ),
1805 null_rc,
1806 "store on freed handle must bail, not run against freed inner",
1807 );
1808
1809 net_mesh_blob_adapter_free(adapter);
1811
1812 net_mesh_blob_adapter_free(std::ptr::null_mut());
1814 assert_eq!(
1815 net_mesh_blob_adapter_overflow_enabled(std::ptr::null()),
1816 null_rc,
1817 );
1818
1819 net_redex_free(redex);
1820 }
1821 }
1822}