Skip to content

Commit 3d85eef

Browse files
Copilotcijothomas
andcommitted
Add comprehensive defensive parsing tests for TraceState and Baggage
Co-authored-by: cijothomas <[email protected]>
1 parent 02fa8f9 commit 3d85eef

File tree

4 files changed

+596
-4
lines changed

4 files changed

+596
-4
lines changed

opentelemetry-sdk/src/logs/logger_provider.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -607,8 +607,8 @@ mod tests {
607607
tracer.in_span("test-span", |cx| {
608608
let ambient_ctxt = cx.span().span_context().clone();
609609
let explicit_ctxt = TraceContext {
610-
trace_id: TraceId::from_u128(13),
611-
span_id: SpanId::from_u64(14),
610+
trace_id: TraceId::from_bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13]),
611+
span_id: SpanId::from_bytes([0, 0, 0, 0, 0, 0, 0, 14]),
612612
trace_flags: None,
613613
};
614614

opentelemetry-sdk/src/propagation/baggage.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,167 @@ mod tests {
320320
}
321321
}
322322
}
323+
324+
#[rustfmt::skip]
325+
fn malformed_baggage_test_data() -> Vec<(String, &'static str)> {
326+
vec![
327+
// Empty and whitespace
328+
("".to_string(), "empty header"),
329+
(" ".to_string(), "whitespace only header"),
330+
331+
// Malformed key-value pairs
332+
("key_without_value".to_string(), "missing equals sign"),
333+
("=value_without_key".to_string(), "missing key"),
334+
("key=".to_string(), "empty value allowed"),
335+
("=".to_string(), "empty key and value"),
336+
337+
// Multiple equals signs
338+
("key=value=extra".to_string(), "multiple equals signs"),
339+
("key=val=ue=more".to_string(), "many equals signs"),
340+
341+
// Control characters and non-printable characters
342+
("key=val\x00ue".to_string(), "null character in value"),
343+
("key\x01=value".to_string(), "control character in key"),
344+
("key=value\x7F".to_string(), "DEL character in value"),
345+
("key\t=value".to_string(), "tab character in key"),
346+
("key=val\nue".to_string(), "newline in value"),
347+
("key=val\rue".to_string(), "carriage return in value"),
348+
349+
// Invalid UTF-8 sequences (these will be handled by percent decoding)
350+
("key=%80".to_string(), "invalid UTF-8 start byte"),
351+
("key=%C2".to_string(), "incomplete UTF-8 sequence"),
352+
("key=%ED%A0%80".to_string(), "UTF-8 surrogate"),
353+
354+
// Very long keys and values
355+
(format!("{}=value", "a".repeat(1000)), "very long key"),
356+
(format!("key={}", "v".repeat(1000)), "very long value"),
357+
(format!("{}={}", "k".repeat(500), "v".repeat(500)), "long key and value"),
358+
359+
// Many entries to test memory usage
360+
((0..1000).map(|i| format!("key{}=val{}", i, i)).collect::<Vec<_>>().join(","), "many entries"),
361+
362+
// Malformed metadata
363+
("key=value;".to_string(), "empty metadata"),
364+
("key=value;;".to_string(), "double semicolon"),
365+
("key=value;meta;".to_string(), "trailing semicolon"),
366+
("key=value;meta=".to_string(), "metadata with empty value"),
367+
368+
// Mixed valid and invalid entries
369+
("valid_key=valid_value,invalid_key,another_valid=value".to_string(), "mixed valid and invalid"),
370+
("key1=val1,=,key2=val2".to_string(), "empty entry in middle"),
371+
372+
// Extreme whitespace
373+
(" key1 = val1 , key2 = val2 ".to_string(), "excessive whitespace"),
374+
375+
// Special characters that might cause issues
376+
("key=value,".to_string(), "trailing comma"),
377+
(",key=value".to_string(), "leading comma"),
378+
("key=value,,".to_string(), "double comma"),
379+
("key=val,ue,key2=val2".to_string(), "comma in entry"),
380+
381+
// Unicode characters
382+
("café=bücher".to_string(), "unicode characters"),
383+
("key=🔥".to_string(), "emoji in value"),
384+
("🗝️=value".to_string(), "emoji in key"),
385+
]
386+
}
387+
388+
#[test]
389+
fn extract_baggage_defensive_parsing() {
390+
let propagator = BaggagePropagator::new();
391+
392+
for (malformed_header, description) in malformed_baggage_test_data() {
393+
let mut extractor: HashMap<String, String> = HashMap::new();
394+
extractor.insert(BAGGAGE_HEADER.to_string(), malformed_header.clone());
395+
396+
// The main requirement is that parsing doesn't crash or hang
397+
let context = propagator.extract(&extractor);
398+
let baggage = context.baggage();
399+
400+
// Baggage should be created without crashing, regardless of content
401+
// Invalid entries should be ignored or handled gracefully
402+
assert!(
403+
baggage.len() <= 1000, // Reasonable upper bound
404+
"Too many baggage entries extracted from malformed header: {} ({})",
405+
description, baggage.len()
406+
);
407+
408+
// No entry should have an empty key (our validation should prevent this)
409+
for (key, _) in baggage {
410+
assert!(
411+
!key.as_str().is_empty(),
412+
"Empty key found in baggage from header: {} ({})",
413+
malformed_header, description
414+
);
415+
}
416+
}
417+
}
418+
419+
#[test]
420+
fn extract_baggage_memory_safety() {
421+
let propagator = BaggagePropagator::new();
422+
423+
// Test extremely long header to ensure no memory exhaustion
424+
let very_long_header = format!("key={}", "x".repeat(100_000));
425+
let mut extractor: HashMap<String, String> = HashMap::new();
426+
extractor.insert(BAGGAGE_HEADER.to_string(), very_long_header);
427+
428+
let context = propagator.extract(&extractor);
429+
let baggage = context.baggage();
430+
431+
// Should handle gracefully without crashing
432+
assert!(baggage.len() <= 1);
433+
434+
// Test header with many small entries
435+
let many_entries: Vec<String> = (0..10_000)
436+
.map(|i| format!("k{}=v{}", i, i))
437+
.collect();
438+
let large_header = many_entries.join(",");
439+
440+
let mut extractor2: HashMap<String, String> = HashMap::new();
441+
extractor2.insert(BAGGAGE_HEADER.to_string(), large_header);
442+
443+
let context2 = propagator.extract(&extractor2);
444+
let baggage2 = context2.baggage();
445+
446+
// Should handle gracefully, possibly truncating or limiting entries
447+
assert!(baggage2.len() <= 10_000);
448+
449+
// Verify no extremely long keys or values made it through
450+
for (key, (value, _)) in baggage2 {
451+
assert!(
452+
key.as_str().len() <= 1000,
453+
"Key too long: {} chars", key.as_str().len()
454+
);
455+
assert!(
456+
value.as_str().len() <= 1000,
457+
"Value too long: {} chars", value.as_str().len()
458+
);
459+
}
460+
}
461+
462+
#[test]
463+
fn extract_baggage_percent_encoding_edge_cases() {
464+
let propagator = BaggagePropagator::new();
465+
466+
let test_cases = vec![
467+
("%", "lone percent sign"),
468+
("key=%", "percent at end"),
469+
("key=%2", "incomplete percent encoding"),
470+
("key=%ZZ", "invalid hex in percent encoding"),
471+
("key=%2G", "invalid hex digit"),
472+
("key=%%20", "double percent"),
473+
("key=%20%20%20", "multiple encoded spaces"),
474+
];
475+
476+
for (header, _description) in test_cases {
477+
let mut extractor: HashMap<String, String> = HashMap::new();
478+
extractor.insert(BAGGAGE_HEADER.to_string(), header.to_string());
479+
480+
// Should not crash on invalid percent encoding
481+
let context = propagator.extract(&extractor);
482+
let _baggage = context.baggage();
483+
// Test passes if no panic occurs
484+
}
485+
}
323486
}

opentelemetry-sdk/src/propagation/trace_context.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,197 @@ mod tests {
295295

296296
assert_eq!(Extractor::get(&injector, TRACESTATE_HEADER), Some(state))
297297
}
298+
299+
#[rustfmt::skip]
300+
fn malformed_traceparent_test_data() -> Vec<(String, &'static str)> {
301+
vec![
302+
// Existing invalid cases are already covered, adding more edge cases
303+
("".to_string(), "completely empty"),
304+
(" ".to_string(), "whitespace only"),
305+
("00".to_string(), "too few parts"),
306+
("00-".to_string(), "incomplete with separator"),
307+
("00--00".to_string(), "missing trace ID"),
308+
("00-4bf92f3577b34da6a3ce929d0e0e4736--01".to_string(), "missing span ID"),
309+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-".to_string(), "missing flags"),
310+
311+
// Very long inputs
312+
(format!("00-{}-00f067aa0ba902b7-01", "a".repeat(1000)), "very long trace ID"),
313+
(format!("00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01", "b".repeat(1000)), "very long span ID"),
314+
(format!("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-{}", "c".repeat(1000)), "very long flags"),
315+
316+
// Non-hex characters
317+
("00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01".to_string(), "non-hex in trace ID"),
318+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b$-01".to_string(), "non-hex in span ID"),
319+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0g".to_string(), "non-hex in flags"),
320+
321+
// Unicode and special characters
322+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01🔥".to_string(), "emoji in flags"),
323+
("00-café4da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string(), "unicode in trace ID"),
324+
("00-4bf92f3577b34da6a3ce929d0e0e4736-café67aa0ba902b7-01".to_string(), "unicode in span ID"),
325+
326+
// Control characters
327+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\x00".to_string(), "null terminator"),
328+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\n".to_string(), "newline"),
329+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\t".to_string(), "tab character"),
330+
331+
// Multiple separators
332+
("00--4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string(), "double separator"),
333+
("00-4bf92f3577b34da6a3ce929d0e0e4736--00f067aa0ba902b7-01".to_string(), "double separator middle"),
334+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7--01".to_string(), "double separator end"),
335+
]
336+
}
337+
338+
#[rustfmt::skip]
339+
fn malformed_tracestate_header_test_data() -> Vec<(String, &'static str)> {
340+
vec![
341+
// Very long tracestate headers
342+
(format!("key={}", "x".repeat(100_000)), "extremely long value"),
343+
(format!("{}=value", "k".repeat(100_000)), "extremely long key"),
344+
((0..10_000).map(|i| format!("k{}=v{}", i, i)).collect::<Vec<_>>().join(","), "many entries"),
345+
346+
// Malformed but should not crash
347+
("key=value,malformed".to_string(), "mixed valid and invalid"),
348+
("=value1,key2=value2,=value3".to_string(), "multiple empty keys"),
349+
("key1=value1,,key2=value2".to_string(), "empty entry"),
350+
("key1=value1,key2=".to_string(), "empty value"),
351+
("key1=,key2=value2".to_string(), "another empty value"),
352+
353+
// Control characters and special cases
354+
("key=val\x00ue".to_string(), "null character"),
355+
("key=val\nue".to_string(), "newline character"),
356+
("key=val\tue".to_string(), "tab character"),
357+
("key\x01=value".to_string(), "control character in key"),
358+
359+
// Unicode
360+
("café=bücher".to_string(), "unicode key and value"),
361+
("🔥=🎉".to_string(), "emoji key and value"),
362+
("ключ=значение".to_string(), "cyrillic"),
363+
364+
// Invalid percent encoding patterns
365+
("key=%ZZ".to_string(), "invalid hex in percent encoding"),
366+
("key=%".to_string(), "incomplete percent encoding"),
367+
("key=%%".to_string(), "double percent"),
368+
]
369+
}
370+
371+
#[test]
372+
fn extract_w3c_defensive_traceparent() {
373+
let propagator = TraceContextPropagator::new();
374+
375+
// Test all the malformed traceparent cases
376+
for (invalid_header, reason) in malformed_traceparent_test_data() {
377+
let mut extractor = HashMap::new();
378+
extractor.insert(TRACEPARENT_HEADER.to_string(), invalid_header.clone());
379+
380+
// Should not crash and should return empty context
381+
let result = propagator.extract(&extractor);
382+
assert_eq!(
383+
result.span().span_context(),
384+
&SpanContext::empty_context(),
385+
"Failed to reject invalid traceparent: {} ({})", invalid_header, reason
386+
);
387+
}
388+
}
389+
390+
#[test]
391+
fn extract_w3c_defensive_tracestate() {
392+
let propagator = TraceContextPropagator::new();
393+
394+
// Use a valid traceparent with various malformed tracestate headers
395+
let valid_parent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
396+
397+
for (malformed_state, description) in malformed_tracestate_header_test_data() {
398+
let mut extractor = HashMap::new();
399+
extractor.insert(TRACEPARENT_HEADER.to_string(), valid_parent.to_string());
400+
extractor.insert(TRACESTATE_HEADER.to_string(), malformed_state.clone());
401+
402+
// Should not crash - malformed tracestate should fallback to default
403+
let result = propagator.extract(&extractor);
404+
let span_context = result.span().span_context();
405+
406+
// Should still have valid span context from traceparent
407+
assert!(span_context.is_valid(), "Valid traceparent should create valid context despite malformed tracestate: {}", description);
408+
409+
// Tracestate should either be default or contain only valid entries
410+
let trace_state = span_context.trace_state();
411+
let header = trace_state.header();
412+
413+
// Verify the tracestate header is reasonable (no extremely long result)
414+
assert!(
415+
header.len() <= malformed_state.len() + 1000,
416+
"TraceState header grew unreasonably for input '{}' ({}): {} -> {}",
417+
malformed_state, description, malformed_state.len(), header.len()
418+
);
419+
}
420+
}
421+
422+
#[test]
423+
fn extract_w3c_memory_safety() {
424+
let propagator = TraceContextPropagator::new();
425+
426+
// Test extremely long traceparent
427+
let very_long_traceparent = format!(
428+
"00-{}-{}-01",
429+
"a".repeat(1_000_000), // Very long trace ID
430+
"b".repeat(1_000_000) // Very long span ID
431+
);
432+
433+
let mut extractor = HashMap::new();
434+
extractor.insert(TRACEPARENT_HEADER.to_string(), very_long_traceparent);
435+
436+
// Should not crash or consume excessive memory
437+
let result = propagator.extract(&extractor);
438+
assert_eq!(result.span().span_context(), &SpanContext::empty_context());
439+
440+
// Test with both long traceparent and tracestate
441+
let long_tracestate = format!("key={}", "x".repeat(1_000_000));
442+
let mut extractor2 = HashMap::new();
443+
extractor2.insert(TRACEPARENT_HEADER.to_string(), "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string());
444+
extractor2.insert(TRACESTATE_HEADER.to_string(), long_tracestate);
445+
446+
// Should handle gracefully without excessive memory usage
447+
let result2 = propagator.extract(&extractor2);
448+
let span_context2 = result2.span().span_context();
449+
assert!(span_context2.is_valid());
450+
}
451+
452+
#[test]
453+
fn extract_w3c_boundary_conditions() {
454+
let propagator = TraceContextPropagator::new();
455+
456+
// Test boundary conditions for version and flags
457+
let boundary_test_cases = vec![
458+
("ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "max version"),
459+
("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-ff", "max flags"),
460+
("00-00000000000000000000000000000001-0000000000000001-01", "minimal valid IDs"),
461+
("00-ffffffffffffffffffffffffffffffff-ffffffffffffffff-01", "maximal valid IDs"),
462+
];
463+
464+
for (test_header, description) in boundary_test_cases {
465+
let mut extractor = HashMap::new();
466+
extractor.insert(TRACEPARENT_HEADER.to_string(), test_header.to_string());
467+
468+
let result = propagator.extract(&extractor);
469+
let span_context = result.span().span_context();
470+
471+
// These should be handled according to W3C spec
472+
// The test passes if no panic occurs and behavior is consistent
473+
match description {
474+
"max version" => {
475+
// Version 255 should be accepted (as per spec, parsers should accept unknown versions)
476+
// But our implementation might reject it - either behavior is defensive
477+
}
478+
"max flags" => {
479+
// Max flags should be accepted but masked to valid bits
480+
if span_context.is_valid() {
481+
// Only the sampled bit should be preserved
482+
assert!(span_context.trace_flags().as_u8() <= 1);
483+
}
484+
}
485+
_ => {
486+
// Other cases should work normally
487+
}
488+
}
489+
}
490+
}
298491
}

0 commit comments

Comments
 (0)