From 0ed3ac017dd18007df029edf16195f6c50d9fba4 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Dec 2025 10:10:21 +0000 Subject: [PATCH 1/9] Minor: Introduce macro with OTEL_CTX literal --- process-context/c-and-cpp/otel_process_ctx.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index 8221b7b..e491a5b 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -27,6 +27,7 @@ #define ADD_QUOTES(x) ADD_QUOTES_HELPER(x) #define KEY_VALUE_LIMIT 4096 #define UINT14_MAX 16383 +#define OTEL_CTX_SIGNATURE "OTEL_CTX" #ifndef PR_SET_VMA #define PR_SET_VMA 0x53564d41 @@ -74,7 +75,7 @@ static const otel_process_ctx_data empty_data = { * An outside-of-process reader will read this struct + otel_process_payload to get the data. */ typedef struct __attribute__((packed, aligned(8))) { - char otel_process_ctx_signature[8]; // Always "OTEL_CTX" + char otel_process_ctx_signature[8]; // Always OTEL_CTX_SIGNATURE uint32_t otel_process_ctx_version; // Always > 0, incremented when the data structure changes, currently v2 uint32_t otel_process_payload_size; // Always > 0, size of storage uint64_t otel_process_ctx_published_at_ns; // Always > 0, timestamp from when the context was published in nanoseconds since epoch @@ -184,7 +185,7 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da // Step: Populate the signature into the mapping // The signature must come last and not be reordered with the fields above by the compiler. After this step, external readers // can read the signature and know that the payload is ready to be read. - memcpy(published_state.mapping->otel_process_ctx_signature, "OTEL_CTX", sizeof(published_state.mapping->otel_process_ctx_signature)); + memcpy(published_state.mapping->otel_process_ctx_signature, OTEL_CTX_SIGNATURE, sizeof(published_state.mapping->otel_process_ctx_signature)); // Step: Change permissions on the mapping to only read permission // We've observed the combination of anonymous mapping + a given number of pages + read-only permission is not very common, @@ -201,12 +202,12 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da // Step: Attempt to name the mapping so outside readers can: // * Find it by name // * Hook on prctl to detect when new mappings are published - if (prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, published_state.mapping, mapping_size, "OTEL_CTX") == -1) { + if (prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, published_state.mapping, mapping_size, OTEL_CTX_SIGNATURE) == -1) { // Naming an anonymous mapping is an optional Linux 5.17+ feature (`CONFIG_ANON_VMA_NAME`). // Many distros, such as Ubuntu and Arch enable it. On earlier kernel versions or kernels without the feature, this call can fail. // // It's OK for this to fail because (per-usecase): - // 1. "Find it by name" => As a fallback, it's possible to scan the mappings and look for the "OTEL_CTX" signature in the memory itself, + // 1. "Find it by name" => As a fallback, it's possible to scan the mappings and look for the OTEL_CTX_SIGNATURE in the memory itself, // after observing the mapping has the expected number of pages and permissions. // 2. "Hook on prctl" => When hooking on prctl via eBPF it's still possible to see this call, even when it's not supported/enabled. // This works even on older kernels! For this reason we unconditionally make this call even on older kernels -- to @@ -415,7 +416,7 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o ssize_t bytes_read = process_vm_readv(getpid(), local, 1, remote, 1, 0); if (bytes_read != sizeof(buffer)) return false; - return memcmp(buffer, "OTEL_CTX", sizeof(buffer)) == 0; + return memcmp(buffer, OTEL_CTX_SIGNATURE, sizeof(buffer)) == 0; } static otel_process_ctx_mapping *try_finding_mapping(void) { @@ -584,7 +585,7 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o return (otel_process_ctx_read_result) {.success = false, .error_message = "No OTEL_CTX mapping found (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", .data = empty_data}; } - if (strncmp(mapping->otel_process_ctx_signature, "OTEL_CTX", sizeof(mapping->otel_process_ctx_signature)) != 0 || mapping->otel_process_ctx_version != 2) { + if (strncmp(mapping->otel_process_ctx_signature, OTEL_CTX_SIGNATURE, sizeof(mapping->otel_process_ctx_signature)) != 0 || mapping->otel_process_ctx_version != 2) { return (otel_process_ctx_read_result) {.success = false, .error_message = "Invalid OTEL_CTX signature or version (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", .data = empty_data}; } From f85609e5bbf7613a6f37946ee844c10a8e880789 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Dec 2025 11:01:08 +0000 Subject: [PATCH 2/9] Minor: Tweak error message It's not actually the previous context that failed, it's the context that's _being built_. --- process-context/c-and-cpp/otel_process_ctx.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index e491a5b..8993442 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -157,7 +157,7 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da if (otel_process_ctx_drop_current()) { return (otel_process_ctx_result) {.success = false, .error_message = "Failed to setup MADV_DONTFORK (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } else { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } } @@ -195,7 +195,7 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da if (otel_process_ctx_drop_current()) { return (otel_process_ctx_result) {.success = false, .error_message = "Failed to change permissions on mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } else { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } } From 6f8b21b907c5c6672602894c6733d3a151ab7537 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Dec 2025 11:09:21 +0000 Subject: [PATCH 3/9] Create mmap from memfd as a first option By using a mmap from memfd, the `OTEL_CTX` name will show up in /proc/pid/maps even if the naming of the mapping using `prctl` fails, thus making finding the mapping more efficient. --- process-context/c-and-cpp/otel_process_ctx.c | 25 ++++++++++++++++--- .../c-and-cpp/otel_process_ctx_dump.sh | 2 +- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index 8993442..80bddbe 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -144,11 +144,28 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da // Step: Create the mapping published_state.publisher_pid = getpid(); // This allows us to detect in forks that we shouldn't touch the mapping - published_state.mapping = (otel_process_ctx_mapping *) - mmap(NULL, mapping_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - if (published_state.mapping == MAP_FAILED) { + int fd = memfd_create("OTEL_CTX", MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); + bool failed_to_close_fd = false; + if (fd >= 0) { + // Try to create mapping from memfd + if (ftruncate(fd, mapping_size) == -1) { + otel_process_ctx_drop_current(); + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to truncate memfd (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + published_state.mapping = (otel_process_ctx_mapping *) mmap(NULL, mapping_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); + failed_to_close_fd = (close(fd) == -1); + } else { + // Fallback: Use an anonymous mapping instead + published_state.mapping = (otel_process_ctx_mapping *) mmap(NULL, mapping_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + } + if (published_state.mapping == MAP_FAILED || failed_to_close_fd) { otel_process_ctx_drop_current(); - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to allocate mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + + if (failed_to_close_fd) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to close memfd (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } else { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to allocate mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } } // Step: Setup MADV_DONTFORK diff --git a/process-context/c-and-cpp/otel_process_ctx_dump.sh b/process-context/c-and-cpp/otel_process_ctx_dump.sh index b521af3..90ce016 100755 --- a/process-context/c-and-cpp/otel_process_ctx_dump.sh +++ b/process-context/c-and-cpp/otel_process_ctx_dump.sh @@ -27,7 +27,7 @@ if ! [[ "$pid" =~ ^[0-9]+$ ]]; then fi # Try to use the name of the mapping to find the context first -line="$(grep -F '[anon:OTEL_CTX]' "/proc/$pid/maps" | head -n 1 || true)" +line="$(grep -F -e '[anon_shmem:OTEL_CTX]' -e '/memfd:OTEL_CTX' "/proc/$pid/maps" | head -n 1 || true)" if [ -n "$line" ]; then start_addr="${line%%-*}" From 530242730d71ac39e29e9fb412ff40d739097e96 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Dec 2025 11:27:36 +0000 Subject: [PATCH 4/9] Drop "probing mappings" as a fallback As we now have memfd naming as a fallback, we don't need to to probing (looking at mappings with a given size + flags with no name) to find the process context. This allows the following simplifications: * The mapping size is no longer fixed * The mapping is no longer set to read-only * (Probing code gets dropped) --- process-context/c-and-cpp/otel_process_ctx.c | 72 ++----------------- .../c-and-cpp/otel_process_ctx_dump.sh | 42 ++--------- 2 files changed, 12 insertions(+), 102 deletions(-) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index 80bddbe..4ebe1f8 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -104,16 +104,6 @@ static otel_process_ctx_state published_state; static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **out, uint32_t *out_size, otel_process_ctx_data data); -// We use a mapping size of 2 pages explicitly as a hint when running on legacy kernels that don't support the -// PR_SET_VMA_ANON_NAME prctl call; see below for more details. -static ssize_t size_for_mapping(void) { - long page_size_bytes = sysconf(_SC_PAGESIZE); - if (page_size_bytes < 4096) { - return -1; - } - return page_size_bytes * 2; -} - static uint64_t time_now_ns(void) { struct timespec ts; if (clock_gettime(CLOCK_REALTIME, &ts) == -1) return 0; @@ -129,12 +119,6 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } - // Step: Determine size for mapping - ssize_t mapping_size = size_for_mapping(); - if (mapping_size == -1) { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get page size (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; - } - // Step: Prepare the payload to be published // The payload SHOULD be ready and valid before trying to actually create the mapping. if (!data) return (otel_process_ctx_result) {.success = false, .error_message = "otel_process_ctx_data is NULL (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; @@ -143,6 +127,7 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da if (!result.success) return result; // Step: Create the mapping + const ssize_t mapping_size = sizeof(otel_process_ctx_mapping); published_state.publisher_pid = getpid(); // This allows us to detect in forks that we shouldn't touch the mapping int fd = memfd_create("OTEL_CTX", MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); bool failed_to_close_fd = false; @@ -204,18 +189,6 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da // can read the signature and know that the payload is ready to be read. memcpy(published_state.mapping->otel_process_ctx_signature, OTEL_CTX_SIGNATURE, sizeof(published_state.mapping->otel_process_ctx_signature)); - // Step: Change permissions on the mapping to only read permission - // We've observed the combination of anonymous mapping + a given number of pages + read-only permission is not very common, - // so this is left as a hint for when running on older kernels and the naming the mapping feature below isn't available. - // For modern kernels, doing this is harmless so we do it unconditionally. - if (mprotect(published_state.mapping, mapping_size, PROT_READ) == -1) { - if (otel_process_ctx_drop_current()) { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to change permissions on mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; - } else { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; - } - } - // Step: Attempt to name the mapping so outside readers can: // * Find it by name // * Hook on prctl to detect when new mappings are published @@ -224,8 +197,7 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da // Many distros, such as Ubuntu and Arch enable it. On earlier kernel versions or kernels without the feature, this call can fail. // // It's OK for this to fail because (per-usecase): - // 1. "Find it by name" => As a fallback, it's possible to scan the mappings and look for the OTEL_CTX_SIGNATURE in the memory itself, - // after observing the mapping has the expected number of pages and permissions. + // 1. "Find it by name" => As a fallback, it's possible to scan the mappings and for the memfd name. // 2. "Hook on prctl" => When hooking on prctl via eBPF it's still possible to see this call, even when it's not supported/enabled. // This works even on older kernels! For this reason we unconditionally make this call even on older kernels -- to // still allow detection via hooking onto prctl. @@ -248,8 +220,7 @@ bool otel_process_ctx_drop_current(void) { // The mapping only exists if it was created by the current process; if it was inherited by a fork it doesn't exist anymore // (due to the MADV_DONTFORK) and we don't need to do anything to it. if (state.mapping != NULL && state.mapping != MAP_FAILED && getpid() == state.publisher_pid) { - ssize_t mapping_size = size_for_mapping(); - success = mapping_size > 0 && munmap(state.mapping, mapping_size) == 0; + success = munmap(state.mapping, sizeof(otel_process_ctx_mapping)) == 0; } // The payload may have been inherited from a parent. This is a regular malloc so we need to free it so we don't leak. @@ -402,40 +373,6 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o return (void *)(uintptr_t) start; } - static bool is_otel_process_ctx_mapping(char *line) { - size_t name_len = sizeof("[anon:OTEL_CTX]") - 1; - size_t line_len = strlen(line); - if (line_len < name_len) return false; - if (line[line_len-1] == '\n') line[--line_len] = '\0'; - - // Validate expected permission - if (strstr(line, " r--p ") == NULL) return false; - - // Validate expected context size - int64_t start, end; - if (sscanf(line, "%" PRIx64 "-%" PRIx64, &start, &end) != 2) return false; - if (start == 0 || end == 0 || end <= start) return false; - if ((end - start) != size_for_mapping()) return false; - - // Check if the mapping is named - if (memcmp(line + (line_len - name_len), "[anon:OTEL_CTX]", name_len) == 0) return true; - - // To support legacy kernels and those with naming mappings disabled, let's check the mapping for the expected signature - - void *addr = parse_mapping_start(line); - if (addr == NULL) return false; - - // Read 8 bytes at the address using process_vm_readv (to avoid any issues with concurrency/races) - char buffer[8]; - struct iovec local[] = {{.iov_base = buffer, .iov_len = sizeof(buffer)}}; - struct iovec remote[] = {{.iov_base = addr, .iov_len = sizeof(buffer)}}; - - ssize_t bytes_read = process_vm_readv(getpid(), local, 1, remote, 1, 0); - if (bytes_read != sizeof(buffer)) return false; - - return memcmp(buffer, OTEL_CTX_SIGNATURE, sizeof(buffer)) == 0; - } - static otel_process_ctx_mapping *try_finding_mapping(void) { char line[8192]; otel_process_ctx_mapping *result = NULL; @@ -444,7 +381,8 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o if (!fp) return result; while (fgets(line, sizeof(line), fp)) { - if (is_otel_process_ctx_mapping(line)) { + bool is_process_ctx = strstr(line, "[anon_shmem:OTEL_CTX]") != NULL || strstr(line, "/memfd:OTEL_CTX") != NULL; + if (is_process_ctx) { result = (otel_process_ctx_mapping *)parse_mapping_start(line); break; } diff --git a/process-context/c-and-cpp/otel_process_ctx_dump.sh b/process-context/c-and-cpp/otel_process_ctx_dump.sh index 90ce016..7e84682 100755 --- a/process-context/c-and-cpp/otel_process_ctx_dump.sh +++ b/process-context/c-and-cpp/otel_process_ctx_dump.sh @@ -26,43 +26,15 @@ if ! [[ "$pid" =~ ^[0-9]+$ ]]; then exit 1 fi -# Try to use the name of the mapping to find the context first -line="$(grep -F -e '[anon_shmem:OTEL_CTX]' -e '/memfd:OTEL_CTX' "/proc/$pid/maps" | head -n 1 || true)" - -if [ -n "$line" ]; then - start_addr="${line%%-*}" - found_via="named mapping" -else - # Fallback: scan for anonymous mappings that could be OTEL_CTX - while IFS= read -r line; do - # Parse start-end addresses and check if line contains required characteristics - if [[ "$line" =~ "00:00 0" ]] && \ - [[ "$line" =~ r--p ]] && \ - [[ "$line" =~ ^([0-9a-f]+)-([0-9a-f]+) ]]; then - - start_addr_hex="${BASH_REMATCH[1]}" - end_addr_hex="${BASH_REMATCH[2]}" - size=$((16#$end_addr_hex - 16#$start_addr_hex)) - - # Check if size is 8192 bytes and verify signature - if [ "$size" -eq 8192 ]; then - candidate_data_b64="$(dd if="/proc/$pid/mem" bs=1 count=8 skip=$((16#$start_addr_hex)) status=none 2>/dev/null | base64 -w0)" - if check_otel_signature "$candidate_data_b64"; then - start_addr="$start_addr_hex" - found_via="mapping not named" - break - fi - fi - fi - done < "/proc/$pid/maps" - - if [ -z "${start_addr:-}" ]; then - echo "No OTEL_CTX context found." >&2 - exit 1 - fi +# Find the mapping by name +if ! line="$(grep -F -m 1 -e '[anon_shmem:OTEL_CTX]' -e '/memfd:OTEL_CTX' "/proc/$pid/maps")"; then + echo "No OTEL_CTX context found." >&2 + exit 1 fi -echo "Found OTEL context for PID $pid ($found_via)" +start_addr="${line%%-*}" + +echo "Found OTEL context for PID $pid" echo "Start address: $start_addr" # Read struct otel_process_ctx_mapping, encode as base64 so we can safely store it in a shell variable. From 62cb445becf9361579ed061b62829f17a57d477f Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Dec 2025 12:15:43 +0000 Subject: [PATCH 5/9] Refactor: Extract `ctx_is_published` out from `otel_process_ctx_drop_current` --- process-context/c-and-cpp/otel_process_ctx.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index 4ebe1f8..8fbf895 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -110,6 +110,10 @@ static uint64_t time_now_ns(void) { return ts.tv_sec * 1000000000ULL + ts.tv_nsec; } +static bool ctx_is_published(otel_process_ctx_state state) { + return state.mapping != NULL && state.mapping != MAP_FAILED && getpid() == state.publisher_pid; +} + // The process context is designed to be read by an outside-of-process reader. Thus, for concurrency purposes the steps // on this method are ordered in a way to avoid races, or if not possible to avoid, to allow the reader to detect if there was a race. otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *data) { @@ -219,7 +223,7 @@ bool otel_process_ctx_drop_current(void) { // The mapping only exists if it was created by the current process; if it was inherited by a fork it doesn't exist anymore // (due to the MADV_DONTFORK) and we don't need to do anything to it. - if (state.mapping != NULL && state.mapping != MAP_FAILED && getpid() == state.publisher_pid) { + if (ctx_is_published(state)) { success = munmap(state.mapping, sizeof(otel_process_ctx_mapping)) == 0; } From c7ddce23020a331d6a95894213f2e073aeb4d355 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Dec 2025 12:17:05 +0000 Subject: [PATCH 6/9] Refactor: Move check for NULL data and getting timestamp to beginning of publish step --- process-context/c-and-cpp/otel_process_ctx.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index 8fbf895..2a0e911 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -117,7 +117,14 @@ static bool ctx_is_published(otel_process_ctx_state state) { // The process context is designed to be read by an outside-of-process reader. Thus, for concurrency purposes the steps // on this method are ordered in a way to avoid races, or if not possible to avoid, to allow the reader to detect if there was a race. otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *data) { - // Step: Drop any previous context it if it exists + if (!data) return (otel_process_ctx_result) {.success = false, .error_message = "otel_process_ctx_data is NULL (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + + uint64_t published_at_ns = time_now_ns(); + if (published_at_ns == 0) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get current time (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + // Step: Drop any previous context state it if it exists // No state should be around anywhere after this step. if (!otel_process_ctx_drop_current()) { return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; @@ -125,7 +132,6 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da // Step: Prepare the payload to be published // The payload SHOULD be ready and valid before trying to actually create the mapping. - if (!data) return (otel_process_ctx_result) {.success = false, .error_message = "otel_process_ctx_data is NULL (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; uint32_t payload_size = 0; otel_process_ctx_result result = otel_process_ctx_encode_protobuf_payload(&published_state.payload, &payload_size, *data); if (!result.success) return result; @@ -169,12 +175,6 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da // Step: Populate the mapping // The payload and any extra fields must come first and not be reordered with the signature by the compiler. - - uint64_t published_at_ns = time_now_ns(); - if (published_at_ns == 0) { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get current time (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; - } - *published_state.mapping = (otel_process_ctx_mapping) { .otel_process_ctx_signature = {0}, // Set in "Step: Populate the signature into the mapping" below .otel_process_ctx_version = 2, From 7914d484786532be3726f59614d3671ba96d2288 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Dec 2025 12:22:05 +0000 Subject: [PATCH 7/9] Perform in-place updates instead of dropping/recreating mapping In-place updates are now made by using a zeroed `otel_process_ctx_published_at_ns` field as a marker for a reader that an update is in progress (because otherwise we'd need the published_at + size + payload writes to be atomic). This means the same mapping stays in place, and thus a reader that already has found the mapping doesn't need to re-parse proc to keep reading from it. Here's an example of this in action: ``` # `example_ctx` before updating... $ sudo ./otel_process_ctx_dump.sh 53992 Found OTEL context for PID 53992 Start address: 7286ccd62000 00000000 4f 54 45 4c 5f 43 54 58 02 00 00 00 50 01 00 00 |OTEL_CTX....P...| 00000010 b0 c5 c5 98 89 4e 82 18 a0 72 8e 82 6d 5f 00 00 |.....N...r..m_..| 00000020 Parsed struct: otel_process_ctx_signature : "OTEL_CTX" otel_process_ctx_version : 2 otel_process_payload_size : 336 otel_process_ctx_published_at_ns : 1766060356763239856 (2025-12-18 12:19:16 GMT) otel_process_payload : 0x00005f6d828e72a0 Payload dump (336 bytes): ... attributes { key: "service.instance.id" value { string_value: "123d8444-2c7e-46e3-89f6-6217880f7123" } } attributes { key: "service.name" value { string_value: "my-service" } } # `example_ctx` after updating... $ sudo ./otel_process_ctx_dump.sh 53992 Found OTEL context for PID 53992 Start address: 7286ccd62000 # <-- Mapping still at same address! 00000000 4f 54 45 4c 5f 43 54 58 02 00 00 00 5b 01 00 00 |OTEL_CTX....[...| 00000010 41 54 fc 7e 8e 4e 82 18 00 7e 8e 82 6d 5f 00 00 |AT.~.N...~..m_..| 00000020 Parsed struct: otel_process_ctx_signature : "OTEL_CTX" otel_process_ctx_version : 2 otel_process_payload_size : 347 # <-- Updated! otel_process_ctx_published_at_ns : 1766060377805444161 (2025-12-18 12:19:37 GMT) # <-- Updated! otel_process_payload : 0x00005f6d828e7e00 # <-- Updated! Payload dump (347 bytes): ... attributes { key: "service.instance.id" value { string_value: "456d8444-2c7e-46e3-89f6-6217880f7456" } } attributes { key: "service.name" value { string_value: "my-service-updated" } } ``` --- process-context/c-and-cpp/otel_process_ctx.c | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index 2a0e911..6ebca62 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -102,6 +102,7 @@ typedef struct { */ static otel_process_ctx_state published_state; +static otel_process_ctx_result otel_process_ctx_update(uint64_t published_at_ns, const otel_process_ctx_data *data); static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **out, uint32_t *out_size, otel_process_ctx_data data); static uint64_t time_now_ns(void) { @@ -124,6 +125,8 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get current time (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } + if (ctx_is_published(published_state)) return otel_process_ctx_update(published_at_ns, data); + // Step: Drop any previous context state it if it exists // No state should be around anywhere after this step. if (!otel_process_ctx_drop_current()) { @@ -233,6 +236,50 @@ bool otel_process_ctx_drop_current(void) { return success; } +static otel_process_ctx_result otel_process_ctx_update(uint64_t published_at_ns, const otel_process_ctx_data *data) { + if (data == NULL || !ctx_is_published(published_state)) { + return (otel_process_ctx_result) {.success = false, .error_message = "Unexpected: otel_process_ctx_data is NULL or context is not published (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + // Step: Prepare the new payload to be published + // The payload SHOULD be ready and valid before trying to actually update the mapping. + uint32_t payload_size = 0; + char *payload; + otel_process_ctx_result result = otel_process_ctx_encode_protobuf_payload(&payload, &payload_size, *data); + if (!result.success) return result; + + // Step: Zero out published_at_ns in the mapping + // This enables readers to detect that an update is in-progress + published_state.mapping->otel_process_ctx_published_at_ns = 0; + + // Step: Synchronization - Make sure readers observe the zeroing above before anything else below + atomic_thread_fence(memory_order_seq_cst); + + // Step: Install updated data + published_state.mapping->otel_process_payload_size = payload_size; + published_state.mapping->otel_process_payload = payload; + + // Step: Synchronization - Make sure readers observe the updated data before anything else below + atomic_thread_fence(memory_order_seq_cst); + + // Step: Install new published_at_ns + // The update is now complete -- readers that observe the new timestamp will observe the updated payload + published_state.mapping->otel_process_ctx_published_at_ns = published_at_ns; + + // Step: Attempt to name the mapping so outside readers can detect the update + if (prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, published_state.mapping, sizeof(otel_process_ctx_mapping), OTEL_CTX_SIGNATURE) == -1) { + // It's OK for this to fail -- see otel_process_ctx_publish for why + } + + // Step: Update bookkeeping + free(published_state.payload); // This was still pointing to the old payload + published_state.payload = payload; + + // All done! + + return (otel_process_ctx_result) {.success = true, .error_message = NULL}; +} + // The caller is responsible for enforcing that value fits within UINT14_MAX static size_t protobuf_varint_size(uint16_t value) { return value >= 128 ? 2 : 1; } From 4fcbc1b5dd05a5a4f83276114afe0982503f727b Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Dec 2025 13:58:08 +0000 Subject: [PATCH 8/9] Minor: Add comment for update step --- process-context/c-and-cpp/otel_process_ctx.c | 1 + 1 file changed, 1 insertion(+) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index 6ebca62..9650d52 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -125,6 +125,7 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get current time (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } + // Step: If the context has published by this process, update it in place if (ctx_is_published(published_state)) return otel_process_ctx_update(published_at_ns, data); // Step: Drop any previous context state it if it exists From 03501eb1801ff8116b86e088a9bc64cde6d6e167 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Wed, 7 Jan 2026 10:05:44 +0000 Subject: [PATCH 9/9] Apply suggestions from code review Small comment improvements Co-authored-by: Christos Kalkanis --- process-context/c-and-cpp/otel_process_ctx.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c index 9650d52..cf527b3 100644 --- a/process-context/c-and-cpp/otel_process_ctx.c +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -125,10 +125,10 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get current time (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } - // Step: If the context has published by this process, update it in place + // Step: If the context has been published by this process, update it in place if (ctx_is_published(published_state)) return otel_process_ctx_update(published_at_ns, data); - // Step: Drop any previous context state it if it exists + // Step: Drop any previous context state if it exists // No state should be around anywhere after this step. if (!otel_process_ctx_drop_current()) { return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"};