From a9722c8c03df63821f210afdd090f4edfc14ce68 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 29 Jul 2026 18:35:42 +0200 Subject: [PATCH 1/6] feat(data-pipeline): add span event FFI Expose an owned span event builder through the C API. Native tracers can set string, boolean, integer, and double attributes, including homogeneous arrays, before consuming the event into a span. Copy string inputs and preserve event insertion order. Invalid or null handles return errors without attaching a partial event. --- libdd-data-pipeline-ffi/cbindgen.toml | 4 +- libdd-data-pipeline-ffi/src/tracer.rs | 394 +++++++++++++++++++++++++- 2 files changed, 395 insertions(+), 3 deletions(-) diff --git a/libdd-data-pipeline-ffi/cbindgen.toml b/libdd-data-pipeline-ffi/cbindgen.toml index d3e36b4945..e6be54470b 100644 --- a/libdd-data-pipeline-ffi/cbindgen.toml +++ b/libdd-data-pipeline-ffi/cbindgen.toml @@ -12,6 +12,7 @@ includes = ["common.h"] after_includes = """ typedef struct ddog_TraceExporter ddog_TraceExporter; typedef struct ddog_TracerSpan ddog_TracerSpan; +typedef struct ddog_TracerSpanEvent ddog_TracerSpanEvent; typedef struct ddog_TracerTraceChunks ddog_TracerTraceChunks; typedef struct ddog_TraceExporterCancelToken ddog_TraceExporterCancelToken; """ @@ -19,7 +20,7 @@ typedef struct ddog_TraceExporterCancelToken ddog_TraceExporterCancelToken; [export] prefix = "ddog_" renaming_overrides_prefixing = true -exclude = ["TraceExporter", "TracerSpan", "TracerTraceChunks", "TokioCancellationToken"] +exclude = ["TraceExporter", "TracerSpan", "TracerSpanEvent", "TracerTraceChunks", "TokioCancellationToken"] [export.rename] "ByteSlice" = "ddog_ByteSlice" @@ -31,6 +32,7 @@ exclude = ["TraceExporter", "TracerSpan", "TracerTraceChunks", "TokioCancellatio "ExporterErrorCode" = "ddog_TraceExporterErrorCode" "ExporterError" = "ddog_TraceExporterError" "TracerSpan" = "ddog_TracerSpan" +"TracerSpanEvent" = "ddog_TracerSpanEvent" "TracerSpanFields" = "ddog_TracerSpanFields" "TracerTraceChunks" = "ddog_TracerTraceChunks" "TokioCancellationToken" = "ddog_TraceExporterCancelToken" diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 9489527ad6..436607d408 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -13,10 +13,12 @@ use crate::error::{ExporterError, ExporterErrorCode as ErrorCode}; use crate::response::ExporterResponse; use crate::trace_exporter::TraceExporter; use crate::{catch_panic, gen_error}; -use libdd_common_ffi::slice::AsBytes; +use libdd_common_ffi::slice::{AsBytes, Slice}; use libdd_common_ffi::CharSlice; use libdd_tinybytes::BytesString; -use libdd_trace_utils::span::v04::SpanBytes; +use libdd_trace_utils::span::v04::{ + AttributeAnyValueBytes, AttributeArrayValueBytes, SpanBytes, SpanEventBytes, +}; use std::ptr::NonNull; type TokioCancellationToken = tokio_util::sync::CancellationToken; @@ -45,6 +47,9 @@ fn charslice_to_bytesstring(s: CharSlice) -> Result`. pub struct TracerSpan(SpanBytes); +/// Opaque, owned span event under construction. +pub struct TracerSpanEvent(SpanEventBytes); + /// FFI-safe bundle of scalar fields for creating a [`TracerSpan`]. /// /// Passed by reference to [`ddog_tracer_span_new`] so that adding or @@ -192,6 +197,254 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( ) } +// --------------------------------------------------------------------------- +// TracerSpanEvent +// --------------------------------------------------------------------------- + +/// Create an owned span event. +/// +/// The name is copied before this function returns. The event remains detached +/// until consumed by [`ddog_tracer_span_add_event`]. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_new( + out_handle: NonNull>, + name: CharSlice, + time_unix_nano: u64, +) -> Option> { + catch_panic!( + match charslice_to_bytesstring(name) { + Ok(name) => { + out_handle + .as_ptr() + .write(Box::new(TracerSpanEvent(SpanEventBytes { + time_unix_nano, + name, + ..Default::default() + }))); + None + } + Err(e) => Some(e), + }, + gen_error!(ErrorCode::Panic) + ) +} + +/// Free a detached span event. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_free(handle: Option>) { + drop(handle); +} + +fn set_event_attribute( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + value: AttributeAnyValueBytes, +) -> Option> { + let Some(event) = event else { + return gen_error!(ErrorCode::InvalidArgument); + }; + let key = match charslice_to_bytesstring(key) { + Ok(key) => key, + Err(e) => return Some(e), + }; + event.0.attributes.insert(key, value); + None +} + +/// Add or overwrite a string event attribute. The key and value are copied. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_set_string( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + value: CharSlice, +) -> Option> { + catch_panic!( + match charslice_to_bytesstring(value) { + Ok(value) => set_event_attribute( + event, + key, + AttributeAnyValueBytes::SingleValue(AttributeArrayValueBytes::String(value)), + ), + Err(e) => Some(e), + }, + gen_error!(ErrorCode::Panic) + ) +} + +/// Add or overwrite a boolean event attribute. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_set_bool( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + value: bool, +) -> Option> { + catch_panic!( + set_event_attribute( + event, + key, + AttributeAnyValueBytes::SingleValue(AttributeArrayValueBytes::Boolean(value)), + ), + gen_error!(ErrorCode::Panic) + ) +} + +/// Add or overwrite a signed integer event attribute. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_set_int( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + value: i64, +) -> Option> { + catch_panic!( + set_event_attribute( + event, + key, + AttributeAnyValueBytes::SingleValue(AttributeArrayValueBytes::Integer(value)), + ), + gen_error!(ErrorCode::Panic) + ) +} + +/// Add or overwrite a double event attribute. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_set_double( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + value: f64, +) -> Option> { + catch_panic!( + set_event_attribute( + event, + key, + AttributeAnyValueBytes::SingleValue(AttributeArrayValueBytes::Double(value)), + ), + gen_error!(ErrorCode::Panic) + ) +} + +/// Add or overwrite a homogeneous string array event attribute. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_set_string_array( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + values: Slice, +) -> Option> { + catch_panic!( + { + let values = values + .iter() + .map(|value| charslice_to_bytesstring(*value)) + .collect::, _>>(); + match values { + Ok(values) => set_event_attribute( + event, + key, + AttributeAnyValueBytes::Array( + values + .into_iter() + .map(AttributeArrayValueBytes::String) + .collect(), + ), + ), + Err(e) => Some(e), + } + }, + gen_error!(ErrorCode::Panic) + ) +} + +/// Add or overwrite a homogeneous boolean array event attribute. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_set_bool_array( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + values: Slice, +) -> Option> { + catch_panic!( + set_event_attribute( + event, + key, + AttributeAnyValueBytes::Array( + values + .iter() + .copied() + .map(AttributeArrayValueBytes::Boolean) + .collect(), + ), + ), + gen_error!(ErrorCode::Panic) + ) +} + +/// Add or overwrite a homogeneous signed integer array event attribute. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_set_int_array( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + values: Slice, +) -> Option> { + catch_panic!( + set_event_attribute( + event, + key, + AttributeAnyValueBytes::Array( + values + .iter() + .copied() + .map(AttributeArrayValueBytes::Integer) + .collect(), + ), + ), + gen_error!(ErrorCode::Panic) + ) +} + +/// Add or overwrite a homogeneous double array event attribute. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_event_set_double_array( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + values: Slice, +) -> Option> { + catch_panic!( + set_event_attribute( + event, + key, + AttributeAnyValueBytes::Array( + values + .iter() + .copied() + .map(AttributeArrayValueBytes::Double) + .collect(), + ), + ), + gen_error!(ErrorCode::Panic) + ) +} + +/// Atomically attach a completed event to a span, consuming the event. +/// +/// If either handle is null, the event is dropped and the span is unchanged. +#[no_mangle] +pub unsafe extern "C" fn ddog_tracer_span_add_event( + span: Option<&mut TracerSpan>, + event: Option>, +) -> Option> { + let Some(span) = span else { + return gen_error!(ErrorCode::InvalidArgument); + }; + let Some(event) = event else { + return gen_error!(ErrorCode::InvalidArgument); + }; + catch_panic!( + { + span.0.span_events.push(event.0); + None + }, + gen_error!(ErrorCode::Panic) + ) +} + // --------------------------------------------------------------------------- // TracerTraceChunks // --------------------------------------------------------------------------- @@ -430,6 +683,16 @@ mod tests { } } + fn make_event(name: &str, time_unix_nano: u64) -> Box { + unsafe { + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + let err = ddog_tracer_span_event_new(out, cs(name), time_unix_nano); + assert!(err.is_none()); + handle.assume_init() + } + } + #[test] fn new_sets_all_scalar_fields() { unsafe { @@ -545,6 +808,133 @@ mod tests { } } + #[test] + fn event_attributes_preserve_types_and_arrays() { + unsafe { + let mut event = make_event("exception", 123); + assert!( + ddog_tracer_span_event_set_string(Some(&mut event), cs("string"), cs("value")) + .is_none() + ); + assert!(ddog_tracer_span_event_set_bool(Some(&mut event), cs("bool"), true).is_none()); + assert!(ddog_tracer_span_event_set_int(Some(&mut event), cs("int"), -42).is_none()); + assert!( + ddog_tracer_span_event_set_double(Some(&mut event), cs("double"), 1.5).is_none() + ); + + let strings = [cs("one"), cs("two")]; + let bools = [true, false]; + let ints = [-1, 2]; + let doubles = [1.25, 2.5]; + assert!(ddog_tracer_span_event_set_string_array( + Some(&mut event), + cs("strings"), + Slice::from(&strings[..]) + ) + .is_none()); + assert!(ddog_tracer_span_event_set_bool_array( + Some(&mut event), + cs("bools"), + Slice::from(&bools[..]) + ) + .is_none()); + assert!(ddog_tracer_span_event_set_int_array( + Some(&mut event), + cs("ints"), + Slice::from(&ints[..]) + ) + .is_none()); + assert!(ddog_tracer_span_event_set_double_array( + Some(&mut event), + cs("doubles"), + Slice::from(&doubles[..]) + ) + .is_none()); + + assert_eq!(event.0.name.as_ref(), "exception"); + assert_eq!(event.0.time_unix_nano, 123); + assert_eq!( + event.0.attributes.get("string"), + Some(&AttributeAnyValueBytes::SingleValue( + AttributeArrayValueBytes::String(BytesString::from("value")) + )) + ); + assert_eq!( + event.0.attributes.get("bool"), + Some(&AttributeAnyValueBytes::SingleValue( + AttributeArrayValueBytes::Boolean(true) + )) + ); + assert_eq!( + event.0.attributes.get("int"), + Some(&AttributeAnyValueBytes::SingleValue( + AttributeArrayValueBytes::Integer(-42) + )) + ); + assert_eq!( + event.0.attributes.get("double"), + Some(&AttributeAnyValueBytes::SingleValue( + AttributeArrayValueBytes::Double(1.5) + )) + ); + assert_eq!( + event.0.attributes.get("strings"), + Some(&AttributeAnyValueBytes::Array(vec![ + AttributeArrayValueBytes::String(BytesString::from("one")), + AttributeArrayValueBytes::String(BytesString::from("two")), + ])) + ); + assert_eq!( + event.0.attributes.get("bools"), + Some(&AttributeAnyValueBytes::Array(vec![ + AttributeArrayValueBytes::Boolean(true), + AttributeArrayValueBytes::Boolean(false), + ])) + ); + assert_eq!( + event.0.attributes.get("ints"), + Some(&AttributeAnyValueBytes::Array(vec![ + AttributeArrayValueBytes::Integer(-1), + AttributeArrayValueBytes::Integer(2), + ])) + ); + assert_eq!( + event.0.attributes.get("doubles"), + Some(&AttributeAnyValueBytes::Array(vec![ + AttributeArrayValueBytes::Double(1.25), + AttributeArrayValueBytes::Double(2.5), + ])) + ); + + ddog_tracer_span_event_free(Some(event)); + } + } + + #[test] + fn attaching_events_is_atomic_and_preserves_order() { + unsafe { + let mut span = make_minimal_span(); + let first = make_event("first", 10); + let second = make_event("second", 20); + + assert!(ddog_tracer_span_add_event(Some(&mut span), Some(first)).is_none()); + assert!(ddog_tracer_span_add_event(Some(&mut span), Some(second)).is_none()); + assert_eq!(span.0.span_events.len(), 2); + assert_eq!(span.0.span_events[0].name.as_ref(), "first"); + assert_eq!(span.0.span_events[0].time_unix_nano, 10); + assert_eq!(span.0.span_events[1].name.as_ref(), "second"); + assert_eq!(span.0.span_events[1].time_unix_nano, 20); + + let detached = make_event("detached", 30); + let err = ddog_tracer_span_add_event(None, Some(detached)); + assert!(err.is_some()); + ddog_trace_exporter_error_free(err); + assert_eq!(span.0.span_events.len(), 2); + + ddog_tracer_span_free(span); + } + } + #[test] fn new_with_empty_strings_succeeds() { unsafe { From 127a5d1f0eb603d8bc4b9bd0412c508f8952e846 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 29 Jul 2026 19:32:10 +0200 Subject: [PATCH 2/6] test(data-pipeline): cover span event arrays Verify that every typed array setter accepts an empty array and records it as an array value rather than dropping the attribute. This preserves the caller's type information even when no elements are present. Also verify that invalid UTF-8 in scalar and array string values returns `InvalidInput` without partially mutating the detached event. --- libdd-data-pipeline-ffi/src/tracer.rs | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 436607d408..7d56a9252a 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -910,6 +910,75 @@ mod tests { } } + #[test] + fn event_attributes_accept_empty_typed_arrays() { + unsafe { + let mut event = make_event("empty-arrays", 123); + let strings: [CharSlice<'_>; 0] = []; + let bools: [bool; 0] = []; + let ints: [i64; 0] = []; + let doubles: [f64; 0] = []; + + assert!(ddog_tracer_span_event_set_string_array( + Some(&mut event), + cs("strings"), + Slice::from(&strings[..]) + ) + .is_none()); + assert!(ddog_tracer_span_event_set_bool_array( + Some(&mut event), + cs("bools"), + Slice::from(&bools[..]) + ) + .is_none()); + assert!(ddog_tracer_span_event_set_int_array( + Some(&mut event), + cs("ints"), + Slice::from(&ints[..]) + ) + .is_none()); + assert!(ddog_tracer_span_event_set_double_array( + Some(&mut event), + cs("doubles"), + Slice::from(&doubles[..]) + ) + .is_none()); + + for key in ["strings", "bools", "ints", "doubles"] { + assert_eq!( + event.0.attributes.get(key), + Some(&AttributeAnyValueBytes::Array(Vec::new())) + ); + } + + ddog_tracer_span_event_free(Some(event)); + } + } + + #[test] + fn invalid_event_strings_do_not_partially_update_the_event() { + unsafe { + let mut event = make_event("invalid", 123); + let invalid = CharSlice::from_bytes(&[0xff]); + let err = ddog_tracer_span_event_set_string(Some(&mut event), cs("scalar"), invalid); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidInput); + ddog_trace_exporter_error_free(err); + assert!(event.0.attributes.is_empty()); + + let values = [cs("valid"), invalid]; + let err = ddog_tracer_span_event_set_string_array( + Some(&mut event), + cs("array"), + Slice::from(&values[..]), + ); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidInput); + ddog_trace_exporter_error_free(err); + assert!(event.0.attributes.is_empty()); + + ddog_tracer_span_event_free(Some(event)); + } + } + #[test] fn attaching_events_is_atomic_and_preserves_order() { unsafe { From 190a5658f39dadb41a64600e5c08ff51695f25ab Mon Sep 17 00:00:00 2001 From: Julio Date: Tue, 4 Aug 2026 14:14:41 +0200 Subject: [PATCH 3/6] chore: address PR concerns - Rprevent as_slice from generating a panic by using try_as_slice, malformed slices returns ErrorCode::InvalidArgument. Invalid UTF-8 still returns ErrorCode::InvalidInput. - Accept NULL in ddog_tracer_span_free, matching ddog_tracer_span_event_free. - Deduplicate the bool/int/double array setters behind set_event_array_attribute. - Collect ddog_tracer_span_event_set_string_array once rather than building an intermediate Vec, saving one allocation per call. - Replace the match arms with Result combinators, removing the early returns that behaved differently inside and outside catch_panic!. - Cover the untested boundary paths: NULL event on every setter, invalid UTF-8 keys, ddog_tracer_span_event_new failures leaving out_handle unwritten, attribute overwrite semantics, NULL event in ddog_tracer_span_add_event, and attributes surviving attachment. --- libdd-data-pipeline-ffi/src/tracer.rs | 369 ++++++++++++++++++++------ 1 file changed, 289 insertions(+), 80 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 7d56a9252a..55a1b0b331 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -23,21 +23,18 @@ use std::ptr::NonNull; type TokioCancellationToken = tokio_util::sync::CancellationToken; -// --------------------------------------------------------------------------- -// Helper -// --------------------------------------------------------------------------- +#[inline] +fn slice_to_rust_slice<'a, T>(s: Slice<'a, T>) -> Result<&'a [T], Option>> { + s.try_as_slice() + .map_err(|_| gen_error!(ErrorCode::InvalidArgument)) +} -/// Convert a [`CharSlice`] to a [`BytesString`], copying the bytes. -/// -/// Returns an error if the slice is not valid UTF-8. #[inline] -fn charslice_to_bytesstring(s: CharSlice) -> Result> { - BytesString::from_slice(s.as_bytes()).map_err(|_| { - Box::new(ExporterError::new( - ErrorCode::InvalidInput, - &ErrorCode::InvalidInput.to_string(), - )) - }) +fn charslice_to_bytesstring(s: CharSlice) -> Result>> { + let bytes = s + .try_as_bytes() + .map_err(|_| gen_error!(ErrorCode::InvalidArgument))?; + BytesString::from_slice(bytes).map_err(|_| gen_error!(ErrorCode::InvalidInput)) } // --------------------------------------------------------------------------- @@ -88,7 +85,7 @@ pub unsafe extern "C" fn ddog_tracer_span_new( ) -> Option> { catch_panic!( if let Some(fields) = fields { - let inner = || -> Result<(), Box> { + let inner = || -> Result<(), Option>> { let service = charslice_to_bytesstring(fields.service)?; let name = charslice_to_bytesstring(fields.name)?; let resource = charslice_to_bytesstring(fields.resource)?; @@ -114,7 +111,7 @@ pub unsafe extern "C" fn ddog_tracer_span_new( out_handle.as_ptr().write(Box::new(TracerSpan(span))); Ok(()) }; - inner().err() + inner().err().flatten() } else { gen_error!(ErrorCode::InvalidArgument) }, @@ -131,7 +128,7 @@ pub unsafe extern "C" fn ddog_tracer_span_new( /// `handle` must have been created by [`ddog_tracer_span_new`] and must not /// be used after this call. #[no_mangle] -pub unsafe extern "C" fn ddog_tracer_span_free(handle: Box) { +pub unsafe extern "C" fn ddog_tracer_span_free(handle: Option>) { drop(handle); } @@ -153,11 +150,11 @@ pub unsafe extern "C" fn ddog_tracer_span_set_meta( if let Some(span) = handle { let key = match charslice_to_bytesstring(key) { Ok(s) => s, - Err(e) => return Some(e), + Err(e) => return e, }; let value = match charslice_to_bytesstring(value) { Ok(s) => s, - Err(e) => return Some(e), + Err(e) => return e, }; span.0.meta.insert(key, value); None @@ -186,7 +183,7 @@ pub unsafe extern "C" fn ddog_tracer_span_set_metric( if let Some(span) = handle { let key = match charslice_to_bytesstring(key) { Ok(s) => s, - Err(e) => return Some(e), + Err(e) => return e, }; span.0.metrics.insert(key, value); None @@ -223,7 +220,7 @@ pub unsafe extern "C" fn ddog_tracer_span_event_new( }))); None } - Err(e) => Some(e), + Err(e) => e, }, gen_error!(ErrorCode::Panic) ) @@ -235,6 +232,7 @@ pub unsafe extern "C" fn ddog_tracer_span_event_free(handle: Option, key: CharSlice, @@ -245,12 +243,29 @@ fn set_event_attribute( }; let key = match charslice_to_bytesstring(key) { Ok(key) => key, - Err(e) => return Some(e), + Err(e) => return e, }; event.0.attributes.insert(key, value); None } +fn set_event_array_attribute( + event: Option<&mut TracerSpanEvent>, + key: CharSlice, + values: Slice, + to_value: fn(T) -> AttributeArrayValueBytes, +) -> Option> { + slice_to_rust_slice(values) + .map(|values| { + set_event_attribute( + event, + key, + AttributeAnyValueBytes::Array(values.iter().copied().map(to_value).collect()), + ) + }) + .unwrap_or_else(|e| e) +} + /// Add or overwrite a string event attribute. The key and value are copied. #[no_mangle] pub unsafe extern "C" fn ddog_tracer_span_event_set_string( @@ -265,7 +280,7 @@ pub unsafe extern "C" fn ddog_tracer_span_event_set_string( key, AttributeAnyValueBytes::SingleValue(AttributeArrayValueBytes::String(value)), ), - Err(e) => Some(e), + Err(e) => e, }, gen_error!(ErrorCode::Panic) ) @@ -330,25 +345,17 @@ pub unsafe extern "C" fn ddog_tracer_span_event_set_string_array( values: Slice, ) -> Option> { catch_panic!( - { - let values = values - .iter() - .map(|value| charslice_to_bytesstring(*value)) - .collect::, _>>(); - match values { - Ok(values) => set_event_attribute( - event, - key, - AttributeAnyValueBytes::Array( - values - .into_iter() - .map(AttributeArrayValueBytes::String) - .collect(), - ), - ), - Err(e) => Some(e), - } - }, + slice_to_rust_slice(values) + .and_then(|values| { + values + .iter() + .map(|value| { + charslice_to_bytesstring(*value).map(AttributeArrayValueBytes::String) + }) + .collect::, _>>() + }) + .map(|values| set_event_attribute(event, key, AttributeAnyValueBytes::Array(values))) + .unwrap_or_else(|e| e), gen_error!(ErrorCode::Panic) ) } @@ -361,17 +368,7 @@ pub unsafe extern "C" fn ddog_tracer_span_event_set_bool_array( values: Slice, ) -> Option> { catch_panic!( - set_event_attribute( - event, - key, - AttributeAnyValueBytes::Array( - values - .iter() - .copied() - .map(AttributeArrayValueBytes::Boolean) - .collect(), - ), - ), + set_event_array_attribute(event, key, values, AttributeArrayValueBytes::Boolean), gen_error!(ErrorCode::Panic) ) } @@ -384,17 +381,7 @@ pub unsafe extern "C" fn ddog_tracer_span_event_set_int_array( values: Slice, ) -> Option> { catch_panic!( - set_event_attribute( - event, - key, - AttributeAnyValueBytes::Array( - values - .iter() - .copied() - .map(AttributeArrayValueBytes::Integer) - .collect(), - ), - ), + set_event_array_attribute(event, key, values, AttributeArrayValueBytes::Integer), gen_error!(ErrorCode::Panic) ) } @@ -407,17 +394,7 @@ pub unsafe extern "C" fn ddog_tracer_span_event_set_double_array( values: Slice, ) -> Option> { catch_panic!( - set_event_attribute( - event, - key, - AttributeAnyValueBytes::Array( - values - .iter() - .copied() - .map(AttributeArrayValueBytes::Double) - .collect(), - ), - ), + set_event_array_attribute(event, key, values, AttributeArrayValueBytes::Double), gen_error!(ErrorCode::Panic) ) } @@ -731,7 +708,7 @@ mod tests { assert!(span.0.span_links.is_empty()); assert!(span.0.span_events.is_empty()); - ddog_tracer_span_free(span); + ddog_tracer_span_free(Some(span)); } } @@ -750,7 +727,7 @@ mod tests { assert_eq!(span.0.meta.get("http.method").unwrap().as_ref(), "GET"); assert_eq!(span.0.meta.get("http.url").unwrap().as_ref(), "/users"); - ddog_tracer_span_free(span); + ddog_tracer_span_free(Some(span)); } } @@ -766,7 +743,7 @@ mod tests { // tolerates duplicate entries. assert_eq!(span.0.meta.get("k").unwrap().as_ref(), "v2"); - ddog_tracer_span_free(span); + ddog_tracer_span_free(Some(span)); } } @@ -786,7 +763,7 @@ mod tests { assert_eq!(*span.0.metrics.get("_dd.measured").unwrap(), 1.0); assert_eq!(*span.0.metrics.get("_sampling_priority_v1").unwrap(), 2.0); - ddog_tracer_span_free(span); + ddog_tracer_span_free(Some(span)); } } @@ -979,6 +956,238 @@ mod tests { } } + #[test] + fn malformed_event_arrays_report_invalid_argument() { + unsafe { + let mut event = make_event("malformed", 123); + + // Null pointer + let strings = Slice::from_raw_parts(std::ptr::null::(), 2); + let bools = Slice::from_raw_parts(std::ptr::null::(), 2); + let ints = Slice::from_raw_parts(std::ptr::null::(), 2); + let doubles = Slice::from_raw_parts(std::ptr::null::(), 2); + + for err in [ + ddog_tracer_span_event_set_string_array(Some(&mut event), cs("strings"), strings), + ddog_tracer_span_event_set_bool_array(Some(&mut event), cs("bools"), bools), + ddog_tracer_span_event_set_int_array(Some(&mut event), cs("ints"), ints), + ddog_tracer_span_event_set_double_array(Some(&mut event), cs("doubles"), doubles), + ] { + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + ddog_trace_exporter_error_free(err); + } + assert!(event.0.attributes.is_empty()); + + // Misaligned pointer + let buf = [0u8; 32]; + let misaligned = buf.as_ptr().add(1).cast::(); + assert!(!misaligned.is_aligned()); + let err = ddog_tracer_span_event_set_int_array( + Some(&mut event), + cs("ints"), + Slice::from_raw_parts(misaligned, 2), + ); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + ddog_trace_exporter_error_free(err); + assert!(event.0.attributes.is_empty()); + + // A malformed element + let elements = [cs("valid"), Slice::from_raw_parts(std::ptr::null(), 1)]; + let err = ddog_tracer_span_event_set_string_array( + Some(&mut event), + cs("strings"), + Slice::from(&elements[..]), + ); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + ddog_trace_exporter_error_free(err); + assert!(event.0.attributes.is_empty()); + + // A malformed key + let err = ddog_tracer_span_event_set_bool( + Some(&mut event), + Slice::from_raw_parts(std::ptr::null(), 1), + true, + ); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + ddog_trace_exporter_error_free(err); + assert!(event.0.attributes.is_empty()); + + // A length beyond `isize::MAX`. + let one = [1i64]; + let err = ddog_tracer_span_event_set_int_array( + Some(&mut event), + cs("ints"), + Slice::from_raw_parts(one.as_ptr(), usize::MAX), + ); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + ddog_trace_exporter_error_free(err); + assert!(event.0.attributes.is_empty()); + + ddog_tracer_span_event_free(Some(event)); + } + } + + #[test] + fn event_setters_reject_a_null_event() { + unsafe { + let strings = [cs("one")]; + let bools = [true]; + let ints = [1i64]; + let doubles = [1.0f64]; + + for err in [ + ddog_tracer_span_event_set_string(None, cs("k"), cs("v")), + ddog_tracer_span_event_set_bool(None, cs("k"), true), + ddog_tracer_span_event_set_int(None, cs("k"), 1), + ddog_tracer_span_event_set_double(None, cs("k"), 1.0), + ddog_tracer_span_event_set_string_array(None, cs("k"), Slice::from(&strings[..])), + ddog_tracer_span_event_set_bool_array(None, cs("k"), Slice::from(&bools[..])), + ddog_tracer_span_event_set_int_array(None, cs("k"), Slice::from(&ints[..])), + ddog_tracer_span_event_set_double_array(None, cs("k"), Slice::from(&doubles[..])), + ] { + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + ddog_trace_exporter_error_free(err); + } + } + } + + #[test] + fn invalid_utf8_event_keys_report_invalid_input() { + unsafe { + let mut event = make_event("keys", 1); + let invalid = CharSlice::from_bytes(&[0xff]); + let strings = [cs("one")]; + let bools = [true]; + let ints = [1i64]; + let doubles = [1.0f64]; + + for err in [ + ddog_tracer_span_event_set_string(Some(&mut event), invalid, cs("v")), + ddog_tracer_span_event_set_bool(Some(&mut event), invalid, true), + ddog_tracer_span_event_set_int(Some(&mut event), invalid, 1), + ddog_tracer_span_event_set_double(Some(&mut event), invalid, 1.0), + ddog_tracer_span_event_set_string_array( + Some(&mut event), + invalid, + Slice::from(&strings[..]), + ), + ddog_tracer_span_event_set_bool_array( + Some(&mut event), + invalid, + Slice::from(&bools[..]), + ), + ddog_tracer_span_event_set_int_array( + Some(&mut event), + invalid, + Slice::from(&ints[..]), + ), + ddog_tracer_span_event_set_double_array( + Some(&mut event), + invalid, + Slice::from(&doubles[..]), + ), + ] { + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidInput); + ddog_trace_exporter_error_free(err); + } + assert!(event.0.attributes.is_empty()); + + ddog_tracer_span_event_free(Some(event)); + } + } + + #[test] + fn event_new_rejects_invalid_names() { + unsafe { + // Invalid UTF-8 contents. + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + let err = ddog_tracer_span_event_new(out, CharSlice::from_bytes(&[0xff]), 1); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidInput); + ddog_trace_exporter_error_free(err); + + // A malformed name slice, rejected before any UTF-8 check. + let mut handle = MaybeUninit::>::uninit(); + let out = NonNull::new(handle.as_mut_ptr()).unwrap(); + let err = + ddog_tracer_span_event_new(out, Slice::from_raw_parts(std::ptr::null(), 1), 1); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + ddog_trace_exporter_error_free(err); + } + } + + #[test] + fn event_new_accepts_an_empty_name() { + unsafe { + let event = make_event("", 0); + assert_eq!(event.0.name.as_ref(), ""); + assert_eq!(event.0.time_unix_nano, 0); + assert!(event.0.attributes.is_empty()); + ddog_tracer_span_event_free(Some(event)); + } + } + + #[test] + fn event_free_accepts_null() { + unsafe { + ddog_tracer_span_event_free(None); + } + } + + #[test] + fn add_event_rejects_a_null_event() { + unsafe { + let mut span = make_minimal_span(); + let err = ddog_tracer_span_add_event(Some(&mut span), None); + assert_eq!(err.as_ref().unwrap().code, ErrorCode::InvalidArgument); + ddog_trace_exporter_error_free(err); + assert!(span.0.span_events.is_empty()); + ddog_tracer_span_free(Some(span)); + } + } + + #[test] + fn attached_events_keep_their_attributes() { + unsafe { + let mut span = make_minimal_span(); + let mut event = make_event("exception", 42); + assert!( + ddog_tracer_span_event_set_string(Some(&mut event), cs("message"), cs("boom")) + .is_none() + ); + let codes = [1i64, 2]; + assert!(ddog_tracer_span_event_set_int_array( + Some(&mut event), + cs("codes"), + Slice::from(&codes[..]) + ) + .is_none()); + + assert!(ddog_tracer_span_add_event(Some(&mut span), Some(event)).is_none()); + + assert_eq!(span.0.span_events.len(), 1); + let attached = &span.0.span_events[0]; + assert_eq!(attached.name.as_ref(), "exception"); + assert_eq!(attached.time_unix_nano, 42); + assert_eq!(attached.attributes.len(), 2); + assert_eq!( + attached.attributes.get("message"), + Some(&AttributeAnyValueBytes::SingleValue( + AttributeArrayValueBytes::String(BytesString::from("boom")) + )) + ); + assert_eq!( + attached.attributes.get("codes"), + Some(&AttributeAnyValueBytes::Array(vec![ + AttributeArrayValueBytes::Integer(1), + AttributeArrayValueBytes::Integer(2), + ])) + ); + + ddog_tracer_span_free(Some(span)); + } + } + #[test] fn attaching_events_is_atomic_and_preserves_order() { unsafe { @@ -1000,7 +1209,7 @@ mod tests { ddog_trace_exporter_error_free(err); assert_eq!(span.0.span_events.len(), 2); - ddog_tracer_span_free(span); + ddog_tracer_span_free(Some(span)); } } @@ -1030,7 +1239,7 @@ mod tests { assert_eq!(span.0.name.as_ref(), ""); assert_eq!(span.0.service.as_ref(), ""); - ddog_tracer_span_free(span); + ddog_tracer_span_free(Some(span)); } } From 53afa877b2d52b13680f394345a49cee5ba4a344 Mon Sep 17 00:00:00 2001 From: Julio Date: Tue, 4 Aug 2026 14:42:21 +0200 Subject: [PATCH 4/6] fix: make ddog_tracer_trace_chunks_free handle NULL values --- libdd-data-pipeline-ffi/src/tracer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 55a1b0b331..5eecc45ff5 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -464,7 +464,7 @@ pub unsafe extern "C" fn ddog_tracer_trace_chunks_new( /// /// `handle` must have been created by [`ddog_tracer_trace_chunks_new`]. #[no_mangle] -pub unsafe extern "C" fn ddog_tracer_trace_chunks_free(handle: Box) { +pub unsafe extern "C" fn ddog_tracer_trace_chunks_free(handle: Option>) { drop(handle); } From b1a2a2a30cdf0f4e8fc19e4a30656af78d5086f1 Mon Sep 17 00:00:00 2001 From: Julio Date: Tue, 4 Aug 2026 15:20:17 +0200 Subject: [PATCH 5/6] fix: align tests with latest changes --- libdd-data-pipeline-ffi/src/tracer.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 5eecc45ff5..7ad77c760e 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -1283,7 +1283,7 @@ mod tests { assert_eq!(chunks.0[0].len(), 2); assert_eq!(chunks.0[1].len(), 1); - ddog_tracer_trace_chunks_free(chunks); + ddog_tracer_trace_chunks_free(Some(chunks)); } } @@ -1307,7 +1307,7 @@ mod tests { assert!(err.is_some()); ddog_trace_exporter_error_free(err); - ddog_tracer_trace_chunks_free(chunks); + ddog_tracer_trace_chunks_free(Some(chunks)); } } @@ -1322,7 +1322,7 @@ mod tests { assert!(err.is_some()); ddog_trace_exporter_error_free(err); - ddog_tracer_trace_chunks_free(chunks); + ddog_tracer_trace_chunks_free(Some(chunks)); } } @@ -1341,7 +1341,7 @@ mod tests { unsafe { let chunks = make_chunks(0); assert_eq!(chunks.0.len(), 0); - ddog_tracer_trace_chunks_free(chunks); + ddog_tracer_trace_chunks_free(Some(chunks)); } } @@ -1355,7 +1355,7 @@ mod tests { assert_eq!(chunks.0.len(), 1); assert_eq!(chunks.0[0].len(), 0); - ddog_tracer_trace_chunks_free(chunks); + ddog_tracer_trace_chunks_free(Some(chunks)); } } @@ -1408,7 +1408,7 @@ mod tests { assert!(err.is_some()); assert_eq!(err.as_ref().unwrap().code, ErrorCode::Panic); ddog_trace_exporter_error_free(err); - ddog_tracer_trace_chunks_free(chunks); + ddog_tracer_trace_chunks_free(Some(chunks)); } } From f3fad3d23b971d444fe9ed4a831a6c840c342260 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Tue, 4 Aug 2026 17:08:44 -0400 Subject: [PATCH 6/6] add size check to prevent UB in try_as_slice when using multi-byte T --- libdd-common-ffi/src/slice.rs | 30 ++++++++++++++-- libdd-common-ffi/src/slice_mut.rs | 52 ++++++++++++++++++++++++--- libdd-data-pipeline-ffi/src/tracer.rs | 4 ++- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/libdd-common-ffi/src/slice.rs b/libdd-common-ffi/src/slice.rs index 58ecf96250..174c8727a1 100644 --- a/libdd-common-ffi/src/slice.rs +++ b/libdd-common-ffi/src/slice.rs @@ -222,11 +222,18 @@ impl<'a, T: 'a> Slice<'a, T> { /// /// 1. Fails if `self.ptr` is null and `self.len` is not zero. /// 2. Fails if `self.ptr` is not null and is unaligned. - /// 3. Fails if `self.len` is larger than [`isize::MAX`]. + /// 3. Fails if the total size in bytes (`self.len * size_of::()`) is larger than + /// [`isize::MAX`]. pub fn try_as_slice(&self) -> Result<&'a [T], SliceConversionError> { let (ptr, len) = self.as_raw_parts(); if !ptr.is_null() { - if len > isize::MAX as usize { + // `from_raw_parts` bounds the total size in *bytes*, not the element + // count: for a wide `T` a count within `isize::MAX` can still + // overflow. `checked_mul` also covers the overflow and ZST cases. + let too_large = len + .checked_mul(core::mem::size_of::()) + .is_none_or(|bytes| bytes > isize::MAX as usize); + if too_large { Err(SliceConversionError::LargeLength) } else if !ptr.is_aligned() { Err(SliceConversionError::MisalignedPointer) @@ -480,6 +487,25 @@ mod tests { )); } + #[test] + fn test_try_as_slice_large_byte_size() { + // `len` is within `isize::MAX` as an element count, but the byte size + // (`len * size_of::()`) exceeds it. The pointer is non-null and + // aligned, so the byte-size check is what must reject the slice. + let len = isize::MAX as usize / core::mem::size_of::() + 1; + let large_bytes: Slice = Slice { + ptr: ptr::NonNull::dangling().as_ptr(), + len, + _marker: PhantomData, + }; + + let result = large_bytes.try_as_slice(); + assert!(matches!( + result.unwrap_err(), + SliceConversionError::LargeLength + )); + } + #[test] fn test_try_as_slice_misaligned_pointer() { // Create a misaligned pointer for u64 by using a properly aligned diff --git a/libdd-common-ffi/src/slice_mut.rs b/libdd-common-ffi/src/slice_mut.rs index d87df1795b..309e295d5d 100644 --- a/libdd-common-ffi/src/slice_mut.rs +++ b/libdd-common-ffi/src/slice_mut.rs @@ -93,7 +93,13 @@ impl<'a, T: 'a> MutSlice<'a, T> { if let Some(ptr) = self.ptr { // Crashing immediately is likely better than ignoring these. assert!(ptr.is_aligned()); - assert!(self.len <= isize::MAX as usize); + // Total byte size, not element count, must fit in isize::MAX + // (from_raw_parts_mut). + let too_large = self + .len + .checked_mul(core::mem::size_of::()) + .is_none_or(|bytes| bytes > isize::MAX as usize); + assert!(!too_large); unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), self.len) } } else { // Crashing immediately is likely better than ignoring this. @@ -117,11 +123,18 @@ impl<'a, T: 'a> MutSlice<'a, T> { /// instead. /// - Returns [`SliceConversionError::MisalignedPointer`] if the pointer is non-null and is not /// aligned correctly for the type. - /// - Returns [`SliceConversionError::LargeLength`] if the length of the slice exceeds - /// [`isize::MAX`]. + /// - Returns [`SliceConversionError::LargeLength`] if the total size in bytes (`self.len * + /// size_of::()`) exceeds [`isize::MAX`]. pub fn try_as_slice(&self) -> Result<&'a [T], SliceConversionError> { if let Some(ptr) = self.ptr { - if self.len > isize::MAX as usize { + // `from_raw_parts` bounds the total size in *bytes*, not the element + // count: for a wide `T` a count within `isize::MAX` can still + // overflow. `checked_mul` also covers the overflow and ZST cases. + let too_large = self + .len + .checked_mul(core::mem::size_of::()) + .is_none_or(|bytes| bytes > isize::MAX as usize); + if too_large { Err(SliceConversionError::LargeLength) } else if !ptr.is_aligned() { Err(SliceConversionError::MisalignedPointer) @@ -263,6 +276,19 @@ mod tests { _ = dangerous.as_mut_slice(); } + #[should_panic] + #[test] + fn test_long_byte_size_panic() { + // Element count within isize::MAX, byte size beyond it. + let len = isize::MAX as usize / core::mem::size_of::() + 1; + let mut dangerous: MutSlice = MutSlice { + ptr: Some(ptr::NonNull::dangling()), + len, + _marker: PhantomData, + }; + _ = dangerous.as_mut_slice(); + } + #[test] fn test_try_as_slice_success() { let mut data = vec![1u8, 2, 3, 4, 5]; @@ -314,6 +340,24 @@ mod tests { )); } + #[test] + fn test_try_as_slice_large_byte_size() { + // `len` is within `isize::MAX` as an element count, but the byte size + // (`len * size_of::()`) exceeds it. + let len = isize::MAX as usize / core::mem::size_of::() + 1; + let large_bytes: MutSlice = MutSlice { + ptr: Some(ptr::NonNull::dangling()), + len, + _marker: PhantomData, + }; + + let result = large_bytes.try_as_slice(); + assert!(matches!( + result.unwrap_err(), + SliceConversionError::LargeLength + )); + } + #[test] fn test_try_as_slice_misaligned_pointer() { // Create a misaligned pointer for u64 by using a properly aligned diff --git a/libdd-data-pipeline-ffi/src/tracer.rs b/libdd-data-pipeline-ffi/src/tracer.rs index 7ad77c760e..647985b40b 100644 --- a/libdd-data-pipeline-ffi/src/tracer.rs +++ b/libdd-data-pipeline-ffi/src/tracer.rs @@ -401,7 +401,9 @@ pub unsafe extern "C" fn ddog_tracer_span_event_set_double_array( /// Atomically attach a completed event to a span, consuming the event. /// -/// If either handle is null, the event is dropped and the span is unchanged. +/// A non-null `event` is always consumed — moved into the span on success, or +/// dropped on any error (a null `span`, or an internal panic). Callers must not +/// use or free the event after this call. On error the span is unchanged. #[no_mangle] pub unsafe extern "C" fn ddog_tracer_span_add_event( span: Option<&mut TracerSpan>,