diff --git a/src/ctr_decode_opentelemetry.c b/src/ctr_decode_opentelemetry.c index d62f78a..bf0fa28 100644 --- a/src/ctr_decode_opentelemetry.c +++ b/src/ctr_decode_opentelemetry.c @@ -31,6 +31,13 @@ static int convert_string_value(struct opentelemetry_decode_value *ctr_val, { int result; + /* cfl_array_append_string and cfl_variant_create_from_string call + * strlen() unguarded, so a NULL value would crash here. + */ + if (val == NULL) { + return -1; + } + result = -2; switch (value_type) { @@ -158,12 +165,23 @@ static int convert_array_value(struct opentelemetry_decode_value *ctr_val, struct opentelemetry_decode_value *ctr_arr_val; Opentelemetry__Proto__Common__V1__AnyValue *val; + if (otel_arr == NULL) { + return -1; + } + if (otel_arr->n_values > 0 && otel_arr->values == NULL) { + return -1; + } + ctr_arr_val = malloc(sizeof(struct opentelemetry_decode_value)); if (!ctr_arr_val) { ctr_errno(); return -1; } ctr_arr_val->cfl_arr = cfl_array_create(otel_arr->n_values); + if (!ctr_arr_val->cfl_arr) { + free(ctr_arr_val); + return -1; + } result = 0; @@ -171,6 +189,10 @@ static int convert_array_value(struct opentelemetry_decode_value *ctr_val, array_index < otel_arr->n_values && result == 0; array_index++) { val = otel_arr->values[array_index]; + if (val == NULL) { + /* skip malformed entry rather than failing the whole array */ + continue; + } result = convert_any_value(ctr_arr_val, CTR_OPENTELEMETRY_TYPE_ARRAY, NULL, val); } @@ -215,12 +237,23 @@ static int convert_kvlist_value(struct opentelemetry_decode_value *ctr_val, struct opentelemetry_decode_value *ctr_kvlist_val; Opentelemetry__Proto__Common__V1__KeyValue *kv; + if (otel_kvlist == NULL) { + return -1; + } + if (otel_kvlist->n_values > 0 && otel_kvlist->values == NULL) { + return -1; + } + ctr_kvlist_val = malloc(sizeof(struct opentelemetry_decode_value)); if (!ctr_kvlist_val) { ctr_errno(); return -1; } ctr_kvlist_val->cfl_kvlist = cfl_kvlist_create(); + if (!ctr_kvlist_val->cfl_kvlist) { + free(ctr_kvlist_val); + return -1; + } result = 0; for (kvlist_index = 0; @@ -228,6 +261,12 @@ static int convert_kvlist_value(struct opentelemetry_decode_value *ctr_val, kvlist_index++) { kv = otel_kvlist->values[kvlist_index]; + /* skip entries that would crash strlen() inside the cfl_kvlist + * inserters (which don't NULL-check the key for non-string types). + */ + if (kv == NULL || kv->key == NULL || kv->value == NULL) { + continue; + } result = convert_any_value(ctr_kvlist_val, CTR_OPENTELEMETRY_TYPE_KVLIST, kv->key, kv->value); } @@ -305,7 +344,12 @@ static int convert_any_value(struct opentelemetry_decode_value *ctr_val, switch (val->value_case) { case OPENTELEMETRY__PROTO__COMMON__V1__ANY_VALUE__VALUE_STRING_VALUE: - result = convert_string_value(ctr_val, value_type, key, val->string_value); + if (val->string_value == NULL) { + result = -1; + } + else { + result = convert_string_value(ctr_val, value_type, key, val->string_value); + } break; case OPENTELEMETRY__PROTO__COMMON__V1__ANY_VALUE__VALUE_STRING_VALUE_STRINDEX: @@ -334,7 +378,12 @@ static int convert_any_value(struct opentelemetry_decode_value *ctr_val, break; case OPENTELEMETRY__PROTO__COMMON__V1__ANY_VALUE__VALUE_BYTES_VALUE: - result = convert_bytes_value(ctr_val, value_type, key, val->bytes_value.data, val->bytes_value.len); + if (val->bytes_value.len > 0 && val->bytes_value.data == NULL) { + result = -1; + } + else { + result = convert_bytes_value(ctr_val, value_type, key, val->bytes_value.data, val->bytes_value.len); + } break; default: @@ -371,8 +420,21 @@ static struct ctrace_attributes *convert_otel_attrs(size_t n_attributes, result = 0; + if (n_attributes > 0 && otel_attr == NULL) { + ctr_attributes_destroy(ctr_decoded_attributes->ctr_attr); + free(ctr_decoded_attributes); + return NULL; + } + for (index_kv = 0; index_kv < n_attributes && result == 0; index_kv++) { kv = otel_attr[index_kv]; + /* Skip malformed entries that would otherwise crash strlen() inside + * the cfl_kvlist inserters (none of the non-string variants checks + * the key for NULL). + */ + if (kv == NULL || kv->key == NULL || kv->value == NULL) { + continue; + } key = kv->key; val = kv->value; @@ -422,8 +484,16 @@ static int span_set_events(struct ctrace_span *span, cfl_list_init(&span->events); + if (n_events > 0 && events == NULL) { + return -1; + } + for (index_event = 0; index_event < n_events; index_event++) { event = events[index_event]; + if (event == NULL || event->name == NULL) { + /* skip malformed event rather than failing the whole span */ + continue; + } ctr_event = ctr_span_event_add_ts(span, event->name, event->time_unix_nano); if (ctr_event == NULL) { @@ -492,8 +562,16 @@ void ctr_span_set_links(struct ctrace_span *ctr_span, size_t n_links, struct ctrace_attributes *ctr_attributes; Opentelemetry__Proto__Trace__V1__Span__Link *link; + if (n_links > 0 && links == NULL) { + return; + } + for (index_link = 0; index_link < n_links; index_link++) { link = links[index_link]; + if (link == NULL) { + /* skip malformed link rather than dropping the rest */ + continue; + } ctr_link = ctr_link_create(ctr_span, link->trace_id.data, link->trace_id.len, @@ -545,6 +623,16 @@ int ctr_decode_opentelemetry_create(struct ctrace **out_ctr, } ctr = ctr_create(NULL); + if (ctr == NULL) { + opentelemetry__proto__collector__trace__v1__export_trace_service_request__free_unpacked(service_request, NULL); + return CTR_DECODE_OPENTELEMETRY_ALLOCATION_ERROR; + } + + if (service_request->n_resource_spans > 0 && service_request->resource_spans == NULL) { + opentelemetry__proto__collector__trace__v1__export_trace_service_request__free_unpacked(service_request, NULL); + ctr_destroy(ctr); + return CTR_DECODE_OPENTELEMETRY_INVALID_PAYLOAD; + } for (resource_span_index = 0; resource_span_index < service_request->n_resource_spans; resource_span_index++) { otel_resource_span = service_request->resource_spans[resource_span_index]; @@ -564,6 +652,12 @@ int ctr_decode_opentelemetry_create(struct ctrace **out_ctr, ctr_resource_set_dropped_attr_count(resource, otel_resource_span->resource->dropped_attributes_count); + if (otel_resource_span->n_scope_spans > 0 && otel_resource_span->scope_spans == NULL) { + opentelemetry__proto__collector__trace__v1__export_trace_service_request__free_unpacked(service_request, NULL); + ctr_destroy(ctr); + return CTR_DECODE_OPENTELEMETRY_INVALID_PAYLOAD; + } + for (scope_span_index = 0; scope_span_index < otel_resource_span->n_scope_spans; scope_span_index++) { otel_scope_span = otel_resource_span->scope_spans[scope_span_index]; @@ -589,10 +683,16 @@ int ctr_decode_opentelemetry_create(struct ctrace **out_ctr, ctr_scope_span_set_scope(scope_span, otel_scope_span->scope); } + if (otel_scope_span->n_spans > 0 && otel_scope_span->spans == NULL) { + opentelemetry__proto__collector__trace__v1__export_trace_service_request__free_unpacked(service_request, NULL); + ctr_destroy(ctr); + return CTR_DECODE_OPENTELEMETRY_INVALID_PAYLOAD; + } + for (span_index = 0; span_index < otel_scope_span->n_spans; span_index++) { otel_span = otel_scope_span->spans[span_index]; - if (otel_span == NULL) { + if (otel_span == NULL || otel_span->name == NULL) { opentelemetry__proto__collector__trace__v1__export_trace_service_request__free_unpacked(service_request, NULL); ctr_destroy(ctr); diff --git a/src/ctr_encode_opentelemetry.c b/src/ctr_encode_opentelemetry.c index 8ec3ad5..28373b3 100644 --- a/src/ctr_encode_opentelemetry.c +++ b/src/ctr_encode_opentelemetry.c @@ -485,8 +485,7 @@ static inline Opentelemetry__Proto__Common__V1__AnyValue *ctr_variant_binary_to_ if (result->bytes_value.data == NULL) { otlp_any_value_destroy(result); - result = NULL; - + return NULL; } memcpy(result->bytes_value.data, value->data.as_bytes, result->bytes_value.len); @@ -686,6 +685,10 @@ static Opentelemetry__Proto__Trace__V1__Span__Event *set_event(struct ctrace_spa Opentelemetry__Proto__Trace__V1__Span__Event *event; event = calloc(1, sizeof(Opentelemetry__Proto__Trace__V1__Span__Event)); + if (!event) { + ctr_errno(); + return NULL; + } opentelemetry__proto__trace__v1__span__event__init(event); event->time_unix_nano = ctr_event->time_unix_nano; @@ -709,6 +712,10 @@ static Opentelemetry__Proto__Trace__V1__Span__Event **set_events_from_ctr(struct Opentelemetry__Proto__Trace__V1__Span__Event **event_arr; event_arr = calloc(count, sizeof(Opentelemetry__Proto__Trace__V1__Span__Event *)); + if (!event_arr) { + ctr_errno(); + return NULL; + } event_index = 0; cfl_list_foreach(head, events) { @@ -750,6 +757,10 @@ static void otel_span_set_status(Opentelemetry__Proto__Trace__V1__Span *otel_spa Opentelemetry__Proto__Trace__V1__Status *otel_status; otel_status = calloc(1, sizeof(Opentelemetry__Proto__Trace__V1__Status)); + if (!otel_status) { + ctr_errno(); + return; + } opentelemetry__proto__trace__v1__status__init(otel_status); otel_status->code = status.code; @@ -776,6 +787,10 @@ static void otel_span_set_links(Opentelemetry__Proto__Trace__V1__Span *otel_span Opentelemetry__Proto__Trace__V1__Span__Link *otel_link; otel_links = calloc(count, sizeof(Opentelemetry__Proto__Trace__V1__Span__Link *)); + if (!otel_links) { + ctr_errno(); + return; + } link_index = 0; @@ -783,6 +798,10 @@ static void otel_span_set_links(Opentelemetry__Proto__Trace__V1__Span *otel_span link = cfl_list_entry(head, struct ctrace_link, _head); otel_link = calloc(1, sizeof(Opentelemetry__Proto__Trace__V1__Span__Link)); + if (!otel_link) { + ctr_errno(); + break; + } opentelemetry__proto__trace__v1__span__link__init(otel_link); if (link->trace_id) { @@ -810,7 +829,7 @@ static void otel_span_set_links(Opentelemetry__Proto__Trace__V1__Span *otel_span otel_links[link_index++] = otel_link; } - otel_span->n_links = count; + otel_span->n_links = link_index; otel_span->links = otel_links; } @@ -1057,6 +1076,9 @@ static Opentelemetry__Proto__Trace__V1__ResourceSpans **set_resource_spans(struc resource_span_count = cfl_list_size(&ctr->resource_spans); rs = initialize_resource_spans(resource_span_count); + if (!rs) { + return NULL; + } resource_span_index = 0; @@ -1106,8 +1128,12 @@ static Opentelemetry__Proto__Collector__Trace__V1__ExportTraceServiceRequest *cr return NULL; } - req->n_resource_spans = cfl_list_size(&ctr->resource_spans); rs = set_resource_spans(ctr); + if (!rs) { + free(req); + return NULL; + } + req->n_resource_spans = cfl_list_size(&ctr->resource_spans); req->resource_spans = rs; return req; @@ -1155,9 +1181,15 @@ static void destroy_events(Opentelemetry__Proto__Trace__V1__Span__Event **events int event_index; Opentelemetry__Proto__Trace__V1__Span__Event *event; + if (events == NULL) { + return; + } + for (event_index = 0; event_index < count; event_index++) { event = events[event_index]; - destroy_event(event); + if (event != NULL) { + destroy_event(event); + } } free(events); @@ -1184,9 +1216,15 @@ static void destroy_links(Opentelemetry__Proto__Trace__V1__Span__Link **links, s int link_index; Opentelemetry__Proto__Trace__V1__Span__Link *link; + if (links == NULL) { + return; + } + for (link_index = 0; link_index < count; link_index++) { link = links[link_index]; - destroy_link(link); + if (link != NULL) { + destroy_link(link); + } } free(links); @@ -1221,9 +1259,11 @@ static void destroy_span(Opentelemetry__Proto__Trace__V1__Span *span) span->name = NULL; span->kind = 0; - span->status->message = NULL; - span->status->code = 0; - free(span->status); + if (span->status) { + span->status->message = NULL; + span->status->code = 0; + free(span->status); + } free(span); } @@ -1325,6 +1365,9 @@ cfl_sds_t ctr_encode_opentelemetry_create(struct ctrace *ctr) Opentelemetry__Proto__Collector__Trace__V1__ExportTraceServiceRequest *req; req = create_export_service_request(ctr); + if (!req) { + return NULL; + } len = opentelemetry__proto__collector__trace__v1__export_trace_service_request__get_packed_size(req); buf = cfl_sds_create_size(len); diff --git a/src/ctr_resource.c b/src/ctr_resource.c index 718ef7b..2d12161 100644 --- a/src/ctr_resource.c +++ b/src/ctr_resource.c @@ -101,9 +101,6 @@ struct ctrace_resource_span *ctr_resource_span_create(struct ctrace *ctx) } cfl_list_init(&resource_span->scope_spans); - /* link to ctraces context */ - cfl_list_add(&resource_span->_head, &ctx->resource_spans); - /* create an empty resource */ resource_span->resource = ctr_resource_create(); if (!resource_span->resource) { @@ -111,6 +108,11 @@ struct ctrace_resource_span *ctr_resource_span_create(struct ctrace *ctx) return NULL; } + /* link to ctraces context only after the resource has been created + * so we never leave a freed node attached to ctx->resource_spans. + */ + cfl_list_add(&resource_span->_head, &ctx->resource_spans); + return resource_span; } diff --git a/src/ctraces.c b/src/ctraces.c index d2258be..0ec70c4 100644 --- a/src/ctraces.c +++ b/src/ctraces.c @@ -61,6 +61,10 @@ void ctr_destroy(struct ctrace *ctx) struct cfl_list *tmp; struct ctrace_resource_span *resource_span; + if (!ctx) { + return; + } + /* delete resources */ cfl_list_foreach_safe(head, tmp, &ctx->resource_spans) { resource_span = cfl_list_entry(head, struct ctrace_resource_span, _head); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9f28410..5a54d68 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,6 +2,7 @@ set(UNIT_TESTS_FILES decoding.c basic.c span.c + opentelemetry.c ) set(CTR_TESTS_DATA_PATH "${CMAKE_CURRENT_SOURCE_DIR}/data") diff --git a/tests/basic.c b/tests/basic.c index 74e4c6b..2d7365b 100644 --- a/tests/basic.c +++ b/tests/basic.c @@ -52,8 +52,18 @@ void test_options() ctr_opts_exit(&opts); } +void test_destroy_null() +{ + /* ctr_destroy(NULL) must be a safe no-op so callers don't need to + * branch on allocation failures of ctr_create(). + */ + ctr_destroy(NULL); + TEST_CHECK(1); +} + TEST_LIST = { {"basic", test_basic}, {"options", test_options}, + {"destroy_null", test_destroy_null}, { 0 } }; diff --git a/tests/opentelemetry.c b/tests/opentelemetry.c new file mode 100644 index 0000000..4525cc8 --- /dev/null +++ b/tests/opentelemetry.c @@ -0,0 +1,538 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* CTraces + * ======= + * Copyright 2022 The CTraces Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include "ctr_tests.h" + +/* build a non-trivial trace that exercises spans, events, links, status, + * attributes (scalars/array/kvlist) and an instrumentation scope - the same + * surface that triggered the encoder NULL-deref fixes. + */ +static struct ctrace *build_sample_trace() +{ + struct ctrace *ctx; + struct ctrace_span *span_root; + struct ctrace_span *span_child; + struct ctrace_span_event *event; + struct ctrace_resource_span *resource_span; + struct ctrace_resource *resource; + struct ctrace_scope_span *scope_span; + struct ctrace_instrumentation_scope *scope; + struct ctrace_link *link; + struct ctrace_id *span_id; + struct ctrace_id *trace_id; + struct cfl_array *array; + struct cfl_kvlist *kv; + + ctx = ctr_create(NULL); + if (!ctx) { + return NULL; + } + + resource_span = ctr_resource_span_create(ctx); + ctr_resource_span_set_schema_url(resource_span, "https://ctraces/rs"); + + resource = ctr_resource_span_get_resource(resource_span); + ctr_resource_set_dropped_attr_count(resource, 5); + + scope_span = ctr_scope_span_create(resource_span); + ctr_scope_span_set_schema_url(scope_span, "https://ctraces/ss"); + + scope = ctr_instrumentation_scope_create("ctrace", "1.2.3", 3, NULL); + ctr_scope_span_set_instrumentation_scope(scope_span, scope); + + trace_id = ctr_id_create_random(CTR_ID_OTEL_TRACE_SIZE); + span_id = ctr_id_create_random(CTR_ID_OTEL_SPAN_SIZE); + + span_root = ctr_span_create(ctx, scope_span, "main", NULL); + ctr_span_set_trace_id_with_cid(span_root, trace_id); + ctr_span_set_span_id_with_cid(span_root, span_id); + ctr_span_set_status(span_root, CTRACE_SPAN_STATUS_CODE_OK, "all good"); + + ctr_span_set_attribute_string(span_root, "service.name", "ctraces"); + ctr_span_set_attribute_int64(span_root, "year", 2026); + ctr_span_set_attribute_bool(span_root, "open_source", CTR_TRUE); + ctr_span_set_attribute_double(span_root, "ratio", 0.42); + + array = cfl_array_create(3); + cfl_array_append_string(array, "a"); + cfl_array_append_int64(array, 1); + cfl_array_append_bool(array, CFL_FALSE); + ctr_span_set_attribute_array(span_root, "list", array); + + kv = cfl_kvlist_create(); + cfl_kvlist_insert_string(kv, "language", "c"); + ctr_span_set_attribute_kvlist(span_root, "meta", kv); + + event = ctr_span_event_add(span_root, "connect"); + ctr_span_event_set_attribute_string(event, "host", "localhost"); + ctr_span_event_set_dropped_attributes_count(event, 1); + + span_child = ctr_span_create(ctx, scope_span, "do-work", span_root); + ctr_span_set_trace_id_with_cid(span_child, trace_id); + ctr_span_kind_set(span_child, CTRACE_SPAN_CLIENT); + + link = ctr_link_create_with_cid(span_child, trace_id, span_id); + ctr_link_set_trace_state(link, "state=1"); + ctr_link_set_dropped_attr_count(link, 2); + + ctr_id_destroy(trace_id); + ctr_id_destroy(span_id); + + return ctx; +} + +/* count spans across the whole context, traversing the resource/scope tree + * so we verify that the decoder rebuilt the structure end-to-end. + */ +static int count_spans(struct ctrace *ctx) +{ + int count = 0; + struct cfl_list *rs_head; + struct cfl_list *ss_head; + struct ctrace_resource_span *rs; + struct ctrace_scope_span *ss; + + cfl_list_foreach(rs_head, &ctx->resource_spans) { + rs = cfl_list_entry(rs_head, struct ctrace_resource_span, _head); + cfl_list_foreach(ss_head, &rs->scope_spans) { + ss = cfl_list_entry(ss_head, struct ctrace_scope_span, _head); + count += cfl_list_size(&ss->spans); + } + } + + return count; +} + +void test_otlp_roundtrip() +{ + cfl_sds_t buf; + size_t offset = 0; + struct ctrace *ctx; + struct ctrace *decoded; + int ret; + + ctx = build_sample_trace(); + TEST_ASSERT(ctx != NULL); + + buf = ctr_encode_opentelemetry_create(ctx); + TEST_ASSERT(buf != NULL); + TEST_CHECK(cfl_sds_len(buf) > 0); + + ret = ctr_decode_opentelemetry_create(&decoded, buf, cfl_sds_len(buf), &offset); + TEST_ASSERT(ret == CTR_DECODE_OPENTELEMETRY_SUCCESS); + TEST_ASSERT(decoded != NULL); + TEST_CHECK(count_spans(decoded) == 2); + + ctr_encode_opentelemetry_destroy(buf); + ctr_destroy(decoded); + ctr_destroy(ctx); +} + +void test_otlp_decode_corrupted() +{ + struct ctrace *decoded = NULL; + size_t offset = 0; + /* not a valid protobuf payload */ + char garbage[] = "\xff\xff\xff\xff\xff\xff\xff\xff"; + int ret; + + ret = ctr_decode_opentelemetry_create(&decoded, garbage, sizeof(garbage) - 1, &offset); + TEST_CHECK(ret != CTR_DECODE_OPENTELEMETRY_SUCCESS); + + /* offset past end of buffer must be reported as insufficient data, + * not crash. + */ + offset = 64; + ret = ctr_decode_opentelemetry_create(&decoded, garbage, sizeof(garbage) - 1, &offset); + TEST_CHECK(ret == CTR_DECODE_OPENTELEMETRY_INSUFFICIENT_DATA); +} + +/* minimal trace: one span, no attributes, events, links, or instrumentation + * scope. Exercises the encoder's empty-list paths and the decoder's + * "optional fields not present" paths. + */ +void test_otlp_minimal_trace() +{ + cfl_sds_t buf; + size_t offset = 0; + struct ctrace *ctx; + struct ctrace *decoded = NULL; + struct ctrace_resource_span *rs; + struct ctrace_scope_span *ss; + struct ctrace_span *span; + int ret; + + ctx = ctr_create(NULL); + TEST_ASSERT(ctx != NULL); + + rs = ctr_resource_span_create(ctx); + ss = ctr_scope_span_create(rs); + span = ctr_span_create(ctx, ss, "bare", NULL); + TEST_ASSERT(span != NULL); + + buf = ctr_encode_opentelemetry_create(ctx); + TEST_ASSERT(buf != NULL); + + ret = ctr_decode_opentelemetry_create(&decoded, buf, cfl_sds_len(buf), &offset); + TEST_ASSERT(ret == CTR_DECODE_OPENTELEMETRY_SUCCESS); + TEST_CHECK(count_spans(decoded) == 1); + + ctr_encode_opentelemetry_destroy(buf); + ctr_destroy(decoded); + ctr_destroy(ctx); +} + +/* attributes carry bytes only when wrapped in an array or kvlist + * (convert_bytes_value rejects raw bytes at attribute top-level). + * This exercises ctr_variant_binary_to_otlp_any_value end-to-end. + */ +void test_otlp_bytes_in_array() +{ + cfl_sds_t buf; + size_t offset = 0; + struct ctrace *ctx; + struct ctrace *decoded = NULL; + struct ctrace_resource_span *rs; + struct ctrace_scope_span *ss; + struct ctrace_span *span; + struct cfl_array *array; + int ret; + + ctx = ctr_create(NULL); + rs = ctr_resource_span_create(ctx); + ss = ctr_scope_span_create(rs); + span = ctr_span_create(ctx, ss, "bytes", NULL); + + array = cfl_array_create(2); + cfl_array_append_bytes(array, "\xDE\xAD\xBE\xEF", 4, CFL_FALSE); + cfl_array_append_string(array, "tail"); + ctr_span_set_attribute_array(span, "blob", array); + + buf = ctr_encode_opentelemetry_create(ctx); + TEST_ASSERT(buf != NULL); + + ret = ctr_decode_opentelemetry_create(&decoded, buf, cfl_sds_len(buf), &offset); + TEST_ASSERT(ret == CTR_DECODE_OPENTELEMETRY_SUCCESS); + TEST_CHECK(count_spans(decoded) == 1); + + ctr_encode_opentelemetry_destroy(buf); + ctr_destroy(decoded); + ctr_destroy(ctx); +} + +/* multiple resource_spans, each with multiple scope_spans, each with + * multiple spans - exercises every encoder/decoder outer loop including + * the per-resource_span/per-scope_span/per-span destroy paths. + */ +void test_otlp_multiple_spans() +{ + cfl_sds_t buf; + size_t offset = 0; + struct ctrace *ctx; + struct ctrace *decoded = NULL; + struct ctrace_resource_span *rs; + struct ctrace_scope_span *ss; + int r; + int s; + int n; + int ret; + char name[32]; + + ctx = ctr_create(NULL); + + for (r = 0; r < 2; r++) { + rs = ctr_resource_span_create(ctx); + for (s = 0; s < 2; s++) { + ss = ctr_scope_span_create(rs); + for (n = 0; n < 2; n++) { + snprintf(name, sizeof(name), "span-%d-%d-%d", r, s, n); + TEST_ASSERT(ctr_span_create(ctx, ss, name, NULL) != NULL); + } + } + } + + buf = ctr_encode_opentelemetry_create(ctx); + TEST_ASSERT(buf != NULL); + + ret = ctr_decode_opentelemetry_create(&decoded, buf, cfl_sds_len(buf), &offset); + TEST_ASSERT(ret == CTR_DECODE_OPENTELEMETRY_SUCCESS); + TEST_CHECK(count_spans(decoded) == 2 * 2 * 2); + + ctr_encode_opentelemetry_destroy(buf); + ctr_destroy(decoded); + ctr_destroy(ctx); +} + +/* hand-build a protobuf payload that is well-formed wire-wise but + * omits the required `resource` field on the ResourceSpans message. + * The decoder must reject it without crashing or leaking. + */ +void test_otlp_decode_missing_resource() +{ + Opentelemetry__Proto__Collector__Trace__V1__ExportTraceServiceRequest req; + Opentelemetry__Proto__Trace__V1__ResourceSpans rs; + Opentelemetry__Proto__Trace__V1__ResourceSpans *rs_arr[1]; + uint8_t *wire; + size_t wire_len; + size_t offset = 0; + struct ctrace *decoded = NULL; + int ret; + + opentelemetry__proto__collector__trace__v1__export_trace_service_request__init(&req); + opentelemetry__proto__trace__v1__resource_spans__init(&rs); + + /* explicitly leave rs.resource = NULL and rs.n_scope_spans = 0 */ + rs_arr[0] = &rs; + req.resource_spans = rs_arr; + req.n_resource_spans = 1; + + wire_len = opentelemetry__proto__collector__trace__v1__export_trace_service_request__get_packed_size(&req); + wire = malloc(wire_len); + TEST_ASSERT(wire != NULL); + opentelemetry__proto__collector__trace__v1__export_trace_service_request__pack(&req, wire); + + ret = ctr_decode_opentelemetry_create(&decoded, (char *) wire, wire_len, &offset); + TEST_CHECK(ret == CTR_DECODE_OPENTELEMETRY_INVALID_PAYLOAD); + + free(wire); +} + +/* feed a valid payload truncated to half its length. Protobuf decoding + * must fail with CORRUPTED_DATA rather than reading past the end. + */ +void test_otlp_decode_truncated() +{ + cfl_sds_t buf; + size_t offset = 0; + struct ctrace *ctx; + struct ctrace *decoded = NULL; + int ret; + + ctx = build_sample_trace(); + TEST_ASSERT(ctx != NULL); + + buf = ctr_encode_opentelemetry_create(ctx); + TEST_ASSERT(buf != NULL); + TEST_ASSERT(cfl_sds_len(buf) > 4); + + ret = ctr_decode_opentelemetry_create(&decoded, buf, cfl_sds_len(buf) / 2, &offset); + TEST_CHECK(ret == CTR_DECODE_OPENTELEMETRY_CORRUPTED_DATA); + TEST_CHECK(decoded == NULL); + + ctr_encode_opentelemetry_destroy(buf); + ctr_destroy(ctx); +} + +/* Helpers to build a malicious payload: a service_request containing + * exactly one span with attributes/events/links chosen by the caller. + * The returned wire buffer must be freed by the caller. + */ +static uint8_t *pack_one_span_payload(Opentelemetry__Proto__Trace__V1__Span *span, + size_t *out_len) +{ + Opentelemetry__Proto__Collector__Trace__V1__ExportTraceServiceRequest req; + Opentelemetry__Proto__Trace__V1__ResourceSpans rs; + Opentelemetry__Proto__Trace__V1__ResourceSpans *rs_arr[1]; + Opentelemetry__Proto__Trace__V1__ScopeSpans ss; + Opentelemetry__Proto__Trace__V1__ScopeSpans *ss_arr[1]; + Opentelemetry__Proto__Trace__V1__Span *span_arr[1]; + Opentelemetry__Proto__Resource__V1__Resource resource; + size_t len; + uint8_t *wire; + + opentelemetry__proto__collector__trace__v1__export_trace_service_request__init(&req); + opentelemetry__proto__trace__v1__resource_spans__init(&rs); + opentelemetry__proto__resource__v1__resource__init(&resource); + opentelemetry__proto__trace__v1__scope_spans__init(&ss); + + rs.resource = &resource; + ss_arr[0] = &ss; + rs.scope_spans = ss_arr; + rs.n_scope_spans = 1; + + span_arr[0] = span; + ss.spans = span_arr; + ss.n_spans = 1; + + rs_arr[0] = &rs; + req.resource_spans = rs_arr; + req.n_resource_spans = 1; + + len = opentelemetry__proto__collector__trace__v1__export_trace_service_request__get_packed_size(&req); + wire = malloc(len); + if (wire) { + opentelemetry__proto__collector__trace__v1__export_trace_service_request__pack(&req, wire); + } + *out_len = len; + return wire; +} + +/* A KeyValue with no `value` field set unpacks to value=NULL. The decoder + * must skip the entry instead of dereferencing it. + */ +void test_otlp_decode_attribute_null_value() +{ + Opentelemetry__Proto__Common__V1__KeyValue kv; + Opentelemetry__Proto__Common__V1__KeyValue *kv_arr[1]; + Opentelemetry__Proto__Trace__V1__Span span; + uint8_t *wire; + size_t wire_len; + size_t offset = 0; + struct ctrace *decoded = NULL; + int ret; + + opentelemetry__proto__trace__v1__span__init(&span); + opentelemetry__proto__common__v1__key_value__init(&kv); + kv.key = "bad-key"; + /* leave kv.value = NULL */ + + kv_arr[0] = &kv; + span.name = "victim"; + span.attributes = kv_arr; + span.n_attributes = 1; + + wire = pack_one_span_payload(&span, &wire_len); + TEST_ASSERT(wire != NULL); + + ret = ctr_decode_opentelemetry_create(&decoded, (char *) wire, wire_len, &offset); + TEST_CHECK(ret == CTR_DECODE_OPENTELEMETRY_SUCCESS); + TEST_CHECK(decoded != NULL); + if (decoded != NULL) { + TEST_CHECK(count_spans(decoded) == 1); + ctr_destroy(decoded); + } + free(wire); +} + +/* A nested AnyValue with VALUE_INT_VALUE but key=NULL would, before the + * hardening, call strlen(NULL) inside cfl_kvlist_insert_int64. Build a + * kvlist attribute containing an int entry with empty key (the closest + * we can encode via protobuf-c, which forbids NULL on wire) plus a + * KeyValue whose value field is absent - both must be handled safely. + */ +void test_otlp_decode_nested_malformed_kv() +{ + Opentelemetry__Proto__Common__V1__AnyValue any_int; + Opentelemetry__Proto__Common__V1__KeyValue inner_good; + Opentelemetry__Proto__Common__V1__KeyValue inner_bad; + Opentelemetry__Proto__Common__V1__KeyValue *inner_arr[2]; + Opentelemetry__Proto__Common__V1__KeyValueList kvlist; + Opentelemetry__Proto__Common__V1__AnyValue any_kv; + Opentelemetry__Proto__Common__V1__KeyValue outer; + Opentelemetry__Proto__Common__V1__KeyValue *outer_arr[1]; + Opentelemetry__Proto__Trace__V1__Span span; + uint8_t *wire; + size_t wire_len; + size_t offset = 0; + struct ctrace *decoded = NULL; + int ret; + + opentelemetry__proto__trace__v1__span__init(&span); + + opentelemetry__proto__common__v1__any_value__init(&any_int); + any_int.value_case = OPENTELEMETRY__PROTO__COMMON__V1__ANY_VALUE__VALUE_INT_VALUE; + any_int.int_value = 42; + + opentelemetry__proto__common__v1__key_value__init(&inner_good); + inner_good.key = "k"; + inner_good.value = &any_int; + + opentelemetry__proto__common__v1__key_value__init(&inner_bad); + inner_bad.key = "missing-value"; + /* leave inner_bad.value = NULL */ + + inner_arr[0] = &inner_good; + inner_arr[1] = &inner_bad; + + opentelemetry__proto__common__v1__key_value_list__init(&kvlist); + kvlist.values = inner_arr; + kvlist.n_values = 2; + + opentelemetry__proto__common__v1__any_value__init(&any_kv); + any_kv.value_case = OPENTELEMETRY__PROTO__COMMON__V1__ANY_VALUE__VALUE_KVLIST_VALUE; + any_kv.kvlist_value = &kvlist; + + opentelemetry__proto__common__v1__key_value__init(&outer); + outer.key = "nested"; + outer.value = &any_kv; + outer_arr[0] = &outer; + + span.name = "victim"; + span.attributes = outer_arr; + span.n_attributes = 1; + + wire = pack_one_span_payload(&span, &wire_len); + TEST_ASSERT(wire != NULL); + + ret = ctr_decode_opentelemetry_create(&decoded, (char *) wire, wire_len, &offset); + TEST_CHECK(ret == CTR_DECODE_OPENTELEMETRY_SUCCESS); + TEST_CHECK(decoded != NULL); + if (decoded != NULL) { + ctr_destroy(decoded); + } + free(wire); +} + +/* A span whose `name` field is absent on the wire unpacks with an + * empty-string name (protobuf-c default). The decoder must accept it + * gracefully, not crash on the empty key. + */ +void test_otlp_decode_span_empty_name() +{ + Opentelemetry__Proto__Trace__V1__Span span; + uint8_t *wire; + size_t wire_len; + size_t offset = 0; + struct ctrace *decoded = NULL; + int ret; + + opentelemetry__proto__trace__v1__span__init(&span); + /* span.name defaults to the empty-string sentinel */ + + wire = pack_one_span_payload(&span, &wire_len); + TEST_ASSERT(wire != NULL); + + ret = ctr_decode_opentelemetry_create(&decoded, (char *) wire, wire_len, &offset); + TEST_CHECK(ret == CTR_DECODE_OPENTELEMETRY_SUCCESS); + if (decoded != NULL) { + TEST_CHECK(count_spans(decoded) == 1); + ctr_destroy(decoded); + } + + free(wire); +} + +TEST_LIST = { + {"otlp_roundtrip", test_otlp_roundtrip}, + {"otlp_minimal_trace", test_otlp_minimal_trace}, + {"otlp_bytes_in_array", test_otlp_bytes_in_array}, + {"otlp_multiple_spans", test_otlp_multiple_spans}, + {"otlp_decode_corrupted", test_otlp_decode_corrupted}, + {"otlp_decode_missing_resource", test_otlp_decode_missing_resource}, + {"otlp_decode_truncated", test_otlp_decode_truncated}, + {"otlp_decode_attribute_null_value", test_otlp_decode_attribute_null_value}, + {"otlp_decode_nested_malformed_kv", test_otlp_decode_nested_malformed_kv}, + {"otlp_decode_span_empty_name", test_otlp_decode_span_empty_name}, + { 0 } +};