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"))]
1112#[unsafe(no_mangle)]
1113pub unsafe extern "C" fn net_mesh_blob_adapter_store(
1114 handle: *const MeshBlobAdapterHandle,
1115 blob_ref_bytes: *const u8,
1116 blob_ref_len: usize,
1117 data: *const u8,
1118 data_len: usize,
1119) -> c_int {
1120 let null_rc: c_int = NetError::NullPointer.into();
1121 adapter_guard("net_mesh_blob_adapter_store", null_rc, || {
1122 if handle.is_null() || blob_ref_bytes.is_null() {
1123 return NetError::NullPointer.into();
1124 }
1125 if blob_ref_len > isize::MAX as usize || data_len > isize::MAX as usize {
1127 return NetError::InvalidJson.into();
1128 }
1129 let h = unsafe { &*handle };
1130 let _op = match h.guard.try_enter() {
1132 Some(op) => op,
1133 None => return NetError::NullPointer.into(),
1134 };
1135 let blob_slice = unsafe { std::slice::from_raw_parts(blob_ref_bytes, blob_ref_len) };
1136 let blob_ref = match InnerBlobRef::decode(blob_slice) {
1137 Ok(Some(b)) => b,
1138 _ => return NET_ERR_BLOB_DECODE,
1139 };
1140 let data_slice = if data.is_null() {
1141 &[]
1142 } else {
1143 unsafe { std::slice::from_raw_parts(data, data_len) }
1144 };
1145 let adapter = h.inner.clone();
1146 let data_owned = data_slice.to_vec();
1147 let result = block_on(async move { (*adapter).store(&blob_ref, &data_owned).await });
1148 match result {
1149 Ok(()) => 0,
1150 Err(e) => err_to_code(&e),
1151 }
1152 })
1153}
1154
1155#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1164#[unsafe(no_mangle)]
1165pub unsafe extern "C" fn net_mesh_blob_adapter_fetch(
1166 handle: *const MeshBlobAdapterHandle,
1167 blob_ref_bytes: *const u8,
1168 blob_ref_len: usize,
1169 out_data: *mut *mut u8,
1170 out_len: *mut usize,
1171) -> c_int {
1172 let null_rc: c_int = NetError::NullPointer.into();
1173 adapter_guard("net_mesh_blob_adapter_fetch", null_rc, || {
1174 if handle.is_null() || blob_ref_bytes.is_null() || out_data.is_null() || out_len.is_null() {
1175 return NetError::NullPointer.into();
1176 }
1177 if blob_ref_len > isize::MAX as usize {
1179 return NetError::InvalidJson.into();
1180 }
1181 let h = unsafe { &*handle };
1182 let _op = match h.guard.try_enter() {
1183 Some(op) => op,
1184 None => return NetError::NullPointer.into(),
1185 };
1186 let blob_slice = unsafe { std::slice::from_raw_parts(blob_ref_bytes, blob_ref_len) };
1187 let blob_ref = match InnerBlobRef::decode(blob_slice) {
1188 Ok(Some(b)) => b,
1189 _ => return NET_ERR_BLOB_DECODE,
1190 };
1191 let adapter = h.inner.clone();
1192 let result = block_on(async move { (*adapter).fetch(&blob_ref).await });
1193 match result {
1194 Ok(bytes) => unsafe { write_bytes_out(&bytes, out_data, out_len) },
1200 Err(e) => err_to_code(&e),
1201 }
1202 })
1203}
1204
1205#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1212#[unsafe(no_mangle)]
1213pub unsafe extern "C" fn net_mesh_blob_adapter_exists(
1214 handle: *const MeshBlobAdapterHandle,
1215 blob_ref_bytes: *const u8,
1216 blob_ref_len: usize,
1217 out_exists: *mut c_int,
1218) -> c_int {
1219 let null_rc: c_int = NetError::NullPointer.into();
1220 adapter_guard("net_mesh_blob_adapter_exists", null_rc, || {
1221 if handle.is_null() || blob_ref_bytes.is_null() || out_exists.is_null() {
1222 return NetError::NullPointer.into();
1223 }
1224 if blob_ref_len > isize::MAX as usize {
1226 return NetError::InvalidJson.into();
1227 }
1228 let h = unsafe { &*handle };
1229 let _op = match h.guard.try_enter() {
1230 Some(op) => op,
1231 None => return NetError::NullPointer.into(),
1232 };
1233 let blob_slice = unsafe { std::slice::from_raw_parts(blob_ref_bytes, blob_ref_len) };
1234 let blob_ref = match InnerBlobRef::decode(blob_slice) {
1235 Ok(Some(b)) => b,
1236 _ => return NET_ERR_BLOB_DECODE,
1237 };
1238 let adapter = h.inner.clone();
1239 let result = block_on(async move { (*adapter).exists(&blob_ref).await });
1240 match result {
1241 Ok(present) => {
1242 unsafe { *out_exists = if present { 1 } else { 0 } };
1243 0
1244 }
1245 Err(e) => err_to_code(&e),
1246 }
1247 })
1248}
1249
1250#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1258#[unsafe(no_mangle)]
1259pub unsafe extern "C" fn net_mesh_blob_adapter_prometheus_text(
1260 handle: *const MeshBlobAdapterHandle,
1261) -> *mut c_char {
1262 adapter_guard(
1263 "net_mesh_blob_adapter_prometheus_text",
1264 ptr::null_mut(),
1265 || {
1266 if handle.is_null() {
1267 return ptr::null_mut();
1268 }
1269 let h = unsafe { &*handle };
1270 let _op = match h.guard.try_enter() {
1271 Some(op) => op,
1272 None => return ptr::null_mut(),
1273 };
1274 let adapter = h.inner.clone();
1275 let body = (*adapter).prometheus_text();
1276 match std::ffi::CString::new(body) {
1277 Ok(s) => s.into_raw(),
1278 Err(_) => ptr::null_mut(),
1279 }
1280 },
1281 )
1282}
1283
1284#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1292#[unsafe(no_mangle)]
1293pub unsafe extern "C" fn net_mesh_blob_adapter_overflow_enabled(
1294 handle: *const MeshBlobAdapterHandle,
1295) -> c_int {
1296 let null_rc: c_int = NetError::NullPointer.into();
1297 adapter_guard("net_mesh_blob_adapter_overflow_enabled", null_rc, || {
1298 if handle.is_null() {
1299 return NetError::NullPointer.into();
1300 }
1301 let h = unsafe { &*handle };
1302 let _op = match h.guard.try_enter() {
1303 Some(op) => op,
1304 None => return NetError::NullPointer.into(),
1305 };
1306 let adapter = h.inner.clone();
1307 if (*adapter).overflow_enabled() {
1308 1
1309 } else {
1310 0
1311 }
1312 })
1313}
1314
1315#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1321#[unsafe(no_mangle)]
1322pub unsafe extern "C" fn net_mesh_blob_adapter_overflow_active(
1323 handle: *const MeshBlobAdapterHandle,
1324) -> c_int {
1325 let null_rc: c_int = NetError::NullPointer.into();
1326 adapter_guard("net_mesh_blob_adapter_overflow_active", null_rc, || {
1327 if handle.is_null() {
1328 return NetError::NullPointer.into();
1329 }
1330 let h = unsafe { &*handle };
1331 let _op = match h.guard.try_enter() {
1332 Some(op) => op,
1333 None => return NetError::NullPointer.into(),
1334 };
1335 let adapter = h.inner.clone();
1336 if (*adapter).overflow_active() {
1337 1
1338 } else {
1339 0
1340 }
1341 })
1342}
1343
1344#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1351#[unsafe(no_mangle)]
1352pub unsafe extern "C" fn net_mesh_blob_adapter_overflow_config(
1353 handle: *const MeshBlobAdapterHandle,
1354) -> *mut c_char {
1355 adapter_guard(
1356 "net_mesh_blob_adapter_overflow_config",
1357 ptr::null_mut(),
1358 || {
1359 if handle.is_null() {
1360 return ptr::null_mut();
1361 }
1362 let h = unsafe { &*handle };
1363 let _op = match h.guard.try_enter() {
1364 Some(op) => op,
1365 None => return ptr::null_mut(),
1366 };
1367 let adapter = h.inner.clone();
1368 let cfg = (*adapter).overflow_config();
1369 let json = overflow_to_json(cfg);
1370 match std::ffi::CString::new(json) {
1371 Ok(s) => s.into_raw(),
1372 Err(_) => ptr::null_mut(),
1373 }
1374 },
1375 )
1376}
1377
1378#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1384#[unsafe(no_mangle)]
1385pub unsafe extern "C" fn net_mesh_blob_adapter_set_overflow_enabled(
1386 handle: *const MeshBlobAdapterHandle,
1387 enabled: c_int,
1388) -> c_int {
1389 let null_rc: c_int = NetError::NullPointer.into();
1390 adapter_guard(
1391 "net_mesh_blob_adapter_set_overflow_enabled",
1392 null_rc,
1393 || {
1394 if handle.is_null() {
1395 return NetError::NullPointer.into();
1396 }
1397 let h = unsafe { &*handle };
1398 let _op = match h.guard.try_enter() {
1399 Some(op) => op,
1400 None => return NetError::NullPointer.into(),
1401 };
1402 let adapter = h.inner.clone();
1403 (*adapter).set_overflow_enabled(enabled != 0);
1404 0
1405 },
1406 )
1407}
1408
1409#[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1417#[unsafe(no_mangle)]
1418pub unsafe extern "C" fn net_mesh_blob_adapter_set_overflow_config(
1419 handle: *const MeshBlobAdapterHandle,
1420 config_json: *const c_char,
1421) -> c_int {
1422 let null_rc: c_int = NetError::NullPointer.into();
1423 adapter_guard("net_mesh_blob_adapter_set_overflow_config", null_rc, || {
1424 if handle.is_null() || config_json.is_null() {
1425 return NetError::NullPointer.into();
1426 }
1427 let s = match unsafe { c_str_to_owned(config_json) } {
1428 Some(s) => s,
1429 None => return NetError::InvalidUtf8.into(),
1430 };
1431 let cfg = match parse_overflow_json(&s) {
1432 Ok(c) => c,
1433 Err(code) => return code,
1434 };
1435 let h = unsafe { &*handle };
1436 let _op = match h.guard.try_enter() {
1437 Some(op) => op,
1438 None => return NetError::NullPointer.into(),
1439 };
1440 let adapter = h.inner.clone();
1441 (*adapter).set_overflow_config(cfg);
1442 0
1443 })
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448 #![allow(
1449 clippy::disallowed_methods,
1450 reason = "test code legitimately uses std::sync::{Mutex,RwLock} for SUT setup; tests have no real poison concern"
1451 )]
1452 use super::*;
1453 use std::ffi::CString;
1454 use std::sync::atomic::{AtomicU64, Ordering};
1455
1456 fn unique_id(prefix: &str) -> String {
1457 static N: AtomicU64 = AtomicU64::new(0);
1458 let n = N.fetch_add(1, Ordering::Relaxed);
1459 format!("{}-{}-{}", prefix, std::process::id(), n)
1460 }
1461
1462 #[test]
1465 fn ffi_publish_resolve_round_trip() {
1466 let id = unique_id("ffi-blob");
1467 let root = std::env::temp_dir().join(format!("net-ffi-blob-{}", id));
1468 let id_c = CString::new(id.clone()).unwrap();
1469 let root_c = CString::new(root.to_string_lossy().as_ref()).unwrap();
1470 let uri_c = CString::new("file:///ffi-round-trip").unwrap();
1471
1472 unsafe {
1473 assert_eq!(
1474 net_blob_register_fs_adapter(id_c.as_ptr(), root_c.as_ptr()),
1475 0
1476 );
1477 assert_eq!(net_blob_adapter_registered(id_c.as_ptr()), 1);
1478
1479 let payload = b"end-to-end ffi blob round trip";
1480 let mut out_buf: *mut u8 = std::ptr::null_mut();
1481 let mut out_len: usize = 0;
1482 let rc = net_blob_publish(
1483 id_c.as_ptr(),
1484 uri_c.as_ptr(),
1485 payload.as_ptr(),
1486 payload.len(),
1487 &mut out_buf,
1488 &mut out_len,
1489 );
1490 assert_eq!(rc, 0);
1491 assert!(!out_buf.is_null());
1492 let encoded = std::slice::from_raw_parts(out_buf, out_len);
1494 assert_eq!(
1495 &encoded[..4],
1496 &crate::adapter::net::dataforts::BLOB_REF_MAGIC,
1497 );
1498
1499 let mut content_buf: *mut u8 = std::ptr::null_mut();
1501 let mut content_len: usize = 0;
1502 let rc = net_blob_resolve(
1503 id_c.as_ptr(),
1504 out_buf,
1505 out_len,
1506 &mut content_buf,
1507 &mut content_len,
1508 );
1509 assert_eq!(rc, 0);
1510 let resolved = std::slice::from_raw_parts(content_buf, content_len);
1511 assert_eq!(resolved, payload);
1512
1513 net_blob_free_buffer(out_buf, out_len);
1514 net_blob_free_buffer(content_buf, content_len);
1515 assert_eq!(net_blob_unregister_adapter(id_c.as_ptr()), 1);
1516 }
1517 let _ = std::fs::remove_dir_all(&root);
1518 }
1519
1520 #[test]
1521 fn ffi_resolve_returns_not_registered_for_unknown_adapter() {
1522 let id_c = CString::new("never-registered").unwrap();
1523 let payload = b"any";
1524 let mut out_buf: *mut u8 = std::ptr::null_mut();
1525 let mut out_len: usize = 0;
1526 let rc = unsafe {
1527 net_blob_resolve(
1528 id_c.as_ptr(),
1529 payload.as_ptr(),
1530 payload.len(),
1531 &mut out_buf,
1532 &mut out_len,
1533 )
1534 };
1535 assert_eq!(rc, NET_ERR_BLOB_NOT_REGISTERED);
1536 assert!(out_buf.is_null());
1537 assert_eq!(out_len, 0);
1538 }
1539
1540 mod callback_adapter_round_trip {
1546 use super::*;
1547 use std::collections::HashMap;
1548 use std::sync::Mutex;
1549
1550 struct CallbackCtx {
1551 store: Mutex<HashMap<[u8; 32], Vec<u8>>>,
1552 }
1553
1554 unsafe extern "C" fn cb_store(
1555 ctx: *mut c_void,
1556 _uri: *const c_char,
1557 hash: *const u8,
1558 _size: u64,
1559 data: *const u8,
1560 data_len: usize,
1561 ) -> c_int {
1562 let ctx = &*(ctx as *const CallbackCtx);
1563 let mut h = [0u8; 32];
1564 h.copy_from_slice(std::slice::from_raw_parts(hash, 32));
1565 let buf = if data_len == 0 {
1566 Vec::new()
1567 } else {
1568 std::slice::from_raw_parts(data, data_len).to_vec()
1569 };
1570 ctx.store.lock().unwrap().insert(h, buf);
1571 0
1572 }
1573
1574 unsafe extern "C" fn cb_fetch(
1575 ctx: *mut c_void,
1576 _uri: *const c_char,
1577 hash: *const u8,
1578 _size: u64,
1579 out_data: *mut *mut u8,
1580 out_len: *mut usize,
1581 ) -> c_int {
1582 let ctx = &*(ctx as *const CallbackCtx);
1583 let mut h = [0u8; 32];
1584 h.copy_from_slice(std::slice::from_raw_parts(hash, 32));
1585 let store = ctx.store.lock().unwrap();
1586 match store.get(&h) {
1587 Some(bytes) => {
1588 let boxed = bytes.clone().into_boxed_slice();
1589 let len = boxed.len();
1590 let ptr = Box::into_raw(boxed) as *mut u8;
1591 *out_data = ptr;
1592 *out_len = len;
1593 0
1594 }
1595 None => NET_ERR_BLOB_NOT_FOUND,
1596 }
1597 }
1598
1599 unsafe extern "C" fn cb_fetch_range(
1600 ctx: *mut c_void,
1601 _uri: *const c_char,
1602 hash: *const u8,
1603 _size: u64,
1604 range_start: u64,
1605 range_end: u64,
1606 out_data: *mut *mut u8,
1607 out_len: *mut usize,
1608 ) -> c_int {
1609 let ctx = &*(ctx as *const CallbackCtx);
1610 let mut h = [0u8; 32];
1611 h.copy_from_slice(std::slice::from_raw_parts(hash, 32));
1612 let store = ctx.store.lock().unwrap();
1613 match store.get(&h) {
1614 Some(bytes) => {
1615 let s = range_start as usize;
1616 let e = range_end as usize;
1617 if s > e || e > bytes.len() {
1618 return NET_ERR_BLOB_BACKEND;
1619 }
1620 let slice = bytes[s..e].to_vec().into_boxed_slice();
1621 let len = slice.len();
1622 *out_data = Box::into_raw(slice) as *mut u8;
1623 *out_len = len;
1624 0
1625 }
1626 None => NET_ERR_BLOB_NOT_FOUND,
1627 }
1628 }
1629
1630 unsafe extern "C" fn cb_exists(
1631 ctx: *mut c_void,
1632 _uri: *const c_char,
1633 hash: *const u8,
1634 _size: u64,
1635 out_exists: *mut c_int,
1636 ) -> c_int {
1637 let ctx = &*(ctx as *const CallbackCtx);
1638 let mut h = [0u8; 32];
1639 h.copy_from_slice(std::slice::from_raw_parts(hash, 32));
1640 *out_exists = if ctx.store.lock().unwrap().contains_key(&h) {
1641 1
1642 } else {
1643 0
1644 };
1645 0
1646 }
1647
1648 unsafe extern "C" fn cb_free(_ctx: *mut c_void, data: *mut u8, len: usize) {
1649 if data.is_null() {
1650 return;
1651 }
1652 let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(data, len));
1653 }
1654
1655 #[test]
1656 fn callback_adapter_publish_resolve_round_trip() {
1657 let ctx = Box::new(CallbackCtx {
1658 store: Mutex::new(HashMap::new()),
1659 });
1660 let ctx_ptr = Box::into_raw(ctx) as *mut c_void;
1661 let vtable = NetBlobAdapterVtable {
1662 store: cb_store,
1663 fetch: cb_fetch,
1664 fetch_range: cb_fetch_range,
1665 exists: cb_exists,
1666 free_buffer: cb_free,
1667 };
1668
1669 let id_c = std::ffi::CString::new("ffi-cb-roundtrip").unwrap();
1670 let uri_c = std::ffi::CString::new("cb://round-trip").unwrap();
1671 unsafe {
1672 assert_eq!(
1673 net_blob_register_callback_adapter(id_c.as_ptr(), &vtable, ctx_ptr),
1674 0
1675 );
1676
1677 let payload = b"vtable round-trip payload";
1678 let mut out_buf: *mut u8 = std::ptr::null_mut();
1679 let mut out_len: usize = 0;
1680 let rc = net_blob_publish(
1681 id_c.as_ptr(),
1682 uri_c.as_ptr(),
1683 payload.as_ptr(),
1684 payload.len(),
1685 &mut out_buf,
1686 &mut out_len,
1687 );
1688 assert_eq!(rc, 0);
1689
1690 let mut content_buf: *mut u8 = std::ptr::null_mut();
1691 let mut content_len: usize = 0;
1692 let rc = net_blob_resolve(
1693 id_c.as_ptr(),
1694 out_buf,
1695 out_len,
1696 &mut content_buf,
1697 &mut content_len,
1698 );
1699 assert_eq!(rc, 0);
1700 let resolved = std::slice::from_raw_parts(content_buf, content_len);
1701 assert_eq!(resolved, payload);
1702
1703 net_blob_free_buffer(out_buf, out_len);
1704 net_blob_free_buffer(content_buf, content_len);
1705 assert_eq!(net_blob_unregister_adapter(id_c.as_ptr()), 1);
1706
1707 drop(Box::from_raw(ctx_ptr as *mut CallbackCtx));
1709 }
1710 }
1711 }
1712
1713 #[test]
1714 fn ffi_duplicate_registration_rejected() {
1715 let id = unique_id("ffi-dup");
1716 let root = std::env::temp_dir().join(format!("net-ffi-blob-{}", id));
1717 let id_c = CString::new(id.clone()).unwrap();
1718 let root_c = CString::new(root.to_string_lossy().as_ref()).unwrap();
1719 unsafe {
1720 assert_eq!(
1721 net_blob_register_fs_adapter(id_c.as_ptr(), root_c.as_ptr()),
1722 0
1723 );
1724 assert_eq!(
1725 net_blob_register_fs_adapter(id_c.as_ptr(), root_c.as_ptr()),
1726 NET_ERR_BLOB_DUPLICATE_ID
1727 );
1728 assert_eq!(net_blob_unregister_adapter(id_c.as_ptr()), 1);
1729 }
1730 let _ = std::fs::remove_dir_all(&root);
1731 }
1732
1733 #[cfg(all(feature = "dataforts", feature = "netdb", feature = "redex-disk"))]
1747 #[test]
1748 fn blob_adapter_ops_after_free_bail_and_double_free_is_safe() {
1749 use crate::ffi::cortex::{net_redex_free, net_redex_new};
1750
1751 let null_rc: c_int = NetError::NullPointer.into();
1752 let id_c = CString::new(unique_id("ffi-blob-adapter-uaf")).unwrap();
1753
1754 unsafe {
1755 let redex = net_redex_new(std::ptr::null());
1757 assert!(!redex.is_null());
1758
1759 let adapter = net_mesh_blob_adapter_new(redex, id_c.as_ptr(), 0, std::ptr::null());
1761 assert!(!adapter.is_null(), "adapter must construct");
1762
1763 let live = net_mesh_blob_adapter_overflow_enabled(adapter);
1765 assert!(live == 0 || live == 1, "live overflow_enabled in {{0,1}}");
1766
1767 net_mesh_blob_adapter_free(adapter);
1769
1770 assert_eq!(
1774 net_mesh_blob_adapter_overflow_enabled(adapter),
1775 null_rc,
1776 "op on freed handle must return the null-pointer bail code",
1777 );
1778 assert_eq!(net_mesh_blob_adapter_overflow_active(adapter), null_rc);
1779 assert!(
1780 net_mesh_blob_adapter_prometheus_text(adapter).is_null(),
1781 "ptr-returning op on freed handle must return null",
1782 );
1783 let blob_ref = [0u8; 4];
1784 assert_eq!(
1785 net_mesh_blob_adapter_store(
1786 adapter,
1787 blob_ref.as_ptr(),
1788 blob_ref.len(),
1789 std::ptr::null(),
1790 0,
1791 ),
1792 null_rc,
1793 "store on freed handle must bail, not run against freed inner",
1794 );
1795
1796 net_mesh_blob_adapter_free(adapter);
1798
1799 net_mesh_blob_adapter_free(std::ptr::null_mut());
1801 assert_eq!(
1802 net_mesh_blob_adapter_overflow_enabled(std::ptr::null()),
1803 null_rc,
1804 );
1805
1806 net_redex_free(redex);
1807 }
1808 }
1809}