From 9907b147227d5e35ddf9337712e936973981fdbc Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Wed, 15 Jul 2026 19:47:24 +0200 Subject: [PATCH 1/3] otel-thread-ctx: publish V8 + ObjectWrap layout constants for readers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the OTEP-4719 process-context attributes emitted by getProcessContextAttributes with three new entries an out-of-process eBPF reader needs to walk from our TLS discovery struct to the actual OTEP-4947 record: - threadlocal.native_wrap_fields_offset — sizeof(node::ObjectWrap). Given a pointer to a CtxWrap fetched via wrapped_object_offset, add this to reach CtxWrap::record_. - threadlocal.js_map_table_offset — offset within a V8 JSMap object of the tagged pointer to its backing OrderedHashMap table (JSCollection::kTableOffset in V8, 0x18). - threadlocal.ordered_hash_map_header_size — size of the header preceding the element_count / deleted_element_count / n_buckets fields inside an OrderedHashMap (0x10). Without these, a reader implementing the Node.js schema would have to hardcode them itself and re-verify them per V8 version. Keeping the constants in the addon puts one source of truth close to V8. The two V8 internal offsets are not exposed in V8's public headers (v8-internal.h); they live in Node's private V8 tree (deps/v8/src/objects/{js-collection,ordered-hash-table}.h) — we cite those paths in the code comments so a future V8 layout change has a clear pointer to what to re-check. New non-Linux fallback values match the Linux ones (24 / 0x18 / 0x10); the reader contract is Linux-only per the OTEP anyway, so this just keeps the exported surface consistent in shape across platforms. --- bindings/otel-thread-ctx.cc | 40 +++++++++++++++++++++++++++++++++ ts/src/otel-thread-ctx.ts | 15 +++++++++++++ ts/test/test-otel-thread-ctx.ts | 9 ++++++++ 3 files changed, 64 insertions(+) diff --git a/bindings/otel-thread-ctx.cc b/bindings/otel-thread-ctx.cc index 574a87cf..b24a868c 100644 --- a/bindings/otel-thread-ctx.cc +++ b/bindings/otel-thread-ctx.cc @@ -679,6 +679,28 @@ constexpr int WRAPPED_OBJECT_OFFSET = 0; #endif constexpr int TAGGED_SIZE = v8::internal::kApiTaggedSize; +// sizeof(node::ObjectWrap). Given a pointer to a CtxWrap — or any other +// ObjectWrap-derived C++ object attached to a JSObject via the V8 +// wrapped-object slot — add this offset to reach the derived class's own +// fields. For CtxWrap, that's `record_` (see the static_assert on its +// offset above). +constexpr int NATIVE_WRAP_FIELDS_OFFSET = + static_cast(sizeof(node::ObjectWrap)); + +// V8 JSMap layout: kTableOffset within the JSMap object holds a tagged +// pointer to the backing OrderedHashMap table. Not exposed in V8's +// public headers; kept in sync with +// deps/v8/src/objects/js-collection.h (JSCollection::kTableOffset) +// and the torque-generated JSCollection layout. +constexpr int JS_MAP_TABLE_OFFSET = 0x18; + +// V8 OrderedHashMap layout: the on-heap table starts with a 16-byte +// header before the element_count / deleted_element_count / +// number_of_buckets fields. Not exposed in V8's public headers; kept in +// sync with deps/v8/src/objects/ordered-hash-table.h +// (OrderedHashTable base layout). +constexpr int ORDERED_HASH_MAP_HEADER_SIZE = 0x10; + } // namespace void OtelThreadCtx::Init(Local exports) { @@ -699,6 +721,24 @@ void OtelThreadCtx::Init(Local exports) { String::NewFromUtf8Literal(isolate, "otelThreadCtxTaggedSize"), Integer::New(isolate, TAGGED_SIZE)) .FromJust(); + exports + ->Set(ctx, + String::NewFromUtf8Literal(isolate, + "otelThreadCtxNativeWrapFieldsOffset"), + Integer::New(isolate, NATIVE_WRAP_FIELDS_OFFSET)) + .FromJust(); + exports + ->Set(ctx, + String::NewFromUtf8Literal(isolate, + "otelThreadCtxJsMapTableOffset"), + Integer::New(isolate, JS_MAP_TABLE_OFFSET)) + .FromJust(); + exports + ->Set(ctx, + String::NewFromUtf8Literal( + isolate, "otelThreadCtxOrderedHashMapHeaderSize"), + Integer::New(isolate, ORDERED_HASH_MAP_HEADER_SIZE)) + .FromJust(); } } // namespace dd diff --git a/ts/src/otel-thread-ctx.ts b/ts/src/otel-thread-ctx.ts index 316e1505..cbc91c93 100644 --- a/ts/src/otel-thread-ctx.ts +++ b/ts/src/otel-thread-ctx.ts @@ -40,6 +40,9 @@ export interface ProcessContextAttributes { readonly 'threadlocal.attribute_key_map': readonly string[]; readonly 'threadlocal.wrapped_object_offset': number; readonly 'threadlocal.tagged_size': number; + readonly 'threadlocal.native_wrap_fields_offset': number; + readonly 'threadlocal.js_map_table_offset': number; + readonly 'threadlocal.ordered_hash_map_header_size': number; } /** @@ -103,6 +106,9 @@ interface Addon { otelThreadCtxGetStoredAlsHash(): number; otelThreadCtxWrappedObjectOffset: number; otelThreadCtxTaggedSize: number; + otelThreadCtxNativeWrapFieldsOffset: number; + otelThreadCtxJsMapTableOffset: number; + otelThreadCtxOrderedHashMapHeaderSize: number; } const SCHEMA_VERSION = 'nodejs_v1_dev'; @@ -114,6 +120,9 @@ const SCHEMA_VERSION = 'nodejs_v1_dev'; // consistent in shape. let WRAPPED_OBJECT_OFFSET = 24; let TAGGED_SIZE = 8; +let NATIVE_WRAP_FIELDS_OFFSET = 24; +let JS_MAP_TABLE_OFFSET = 0x18; +let ORDERED_HASH_MAP_HEADER_SIZE = 0x10; /** {@inheritDoc ThreadContextCtor} */ export let ThreadContext: ThreadContextCtor; @@ -140,6 +149,9 @@ if (process.platform === 'linux') { const addon: Addon = findBinding(join(__dirname, '..', '..')); WRAPPED_OBJECT_OFFSET = addon.otelThreadCtxWrappedObjectOffset; TAGGED_SIZE = addon.otelThreadCtxTaggedSize; + NATIVE_WRAP_FIELDS_OFFSET = addon.otelThreadCtxNativeWrapFieldsOffset; + JS_MAP_TABLE_OFFSET = addon.otelThreadCtxJsMapTableOffset; + ORDERED_HASH_MAP_HEADER_SIZE = addon.otelThreadCtxOrderedHashMapHeaderSize; ThreadContext = addon.threadContext; @@ -266,5 +278,8 @@ export function getProcessContextAttributes( 'threadlocal.attribute_key_map': Object.freeze(keys.slice()), 'threadlocal.wrapped_object_offset': WRAPPED_OBJECT_OFFSET, 'threadlocal.tagged_size': TAGGED_SIZE, + 'threadlocal.native_wrap_fields_offset': NATIVE_WRAP_FIELDS_OFFSET, + 'threadlocal.js_map_table_offset': JS_MAP_TABLE_OFFSET, + 'threadlocal.ordered_hash_map_header_size': ORDERED_HASH_MAP_HEADER_SIZE, }) as ProcessContextAttributes; } diff --git a/ts/test/test-otel-thread-ctx.ts b/ts/test/test-otel-thread-ctx.ts index 070cf5c0..f6229f2c 100644 --- a/ts/test/test-otel-thread-ctx.ts +++ b/ts/test/test-otel-thread-ctx.ts @@ -733,8 +733,17 @@ function captureBytes(opts: { strictAssert.deepEqual(pca['threadlocal.attribute_key_map'], keys); strictAssert.equal(pca['threadlocal.wrapped_object_offset'], 24); strictAssert.equal(pca['threadlocal.tagged_size'], 8); + strictAssert.equal(pca['threadlocal.native_wrap_fields_offset'], 24); + strictAssert.equal(pca['threadlocal.js_map_table_offset'], 0x18); + strictAssert.equal( + pca['threadlocal.ordered_hash_map_header_size'], + 0x10, + ); strictAssert.deepEqual(Object.keys(pca).sort(), [ 'threadlocal.attribute_key_map', + 'threadlocal.js_map_table_offset', + 'threadlocal.native_wrap_fields_offset', + 'threadlocal.ordered_hash_map_header_size', 'threadlocal.schema_version', 'threadlocal.tagged_size', 'threadlocal.wrapped_object_offset', From 0aa4e4fffdf11efb18307a69bcddbc3a9b8c4181 Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Thu, 16 Jul 2026 17:43:01 +0200 Subject: [PATCH 2/3] fix formatting --- bindings/otel-thread-ctx.cc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bindings/otel-thread-ctx.cc b/bindings/otel-thread-ctx.cc index b24a868c..3859582d 100644 --- a/bindings/otel-thread-ctx.cc +++ b/bindings/otel-thread-ctx.cc @@ -728,15 +728,15 @@ void OtelThreadCtx::Init(Local exports) { Integer::New(isolate, NATIVE_WRAP_FIELDS_OFFSET)) .FromJust(); exports - ->Set(ctx, - String::NewFromUtf8Literal(isolate, - "otelThreadCtxJsMapTableOffset"), - Integer::New(isolate, JS_MAP_TABLE_OFFSET)) + ->Set( + ctx, + String::NewFromUtf8Literal(isolate, "otelThreadCtxJsMapTableOffset"), + Integer::New(isolate, JS_MAP_TABLE_OFFSET)) .FromJust(); exports ->Set(ctx, - String::NewFromUtf8Literal( - isolate, "otelThreadCtxOrderedHashMapHeaderSize"), + String::NewFromUtf8Literal(isolate, + "otelThreadCtxOrderedHashMapHeaderSize"), Integer::New(isolate, ORDERED_HASH_MAP_HEADER_SIZE)) .FromJust(); } From 8f13616f313cbd23648bb39a494d2b590c74a338 Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Thu, 16 Jul 2026 18:04:45 +0200 Subject: [PATCH 3/3] otel-thread-ctx: fold repeated exports->Set into publish_int lambda The five layout-constant publications in OtelThreadCtx::Init were each spelled out as an exports->Set(ctx, NewFromUtf8Literal(...), Integer::New(...)).FromJust() block. Fold the shape into a `publish_int` lambda and call it five times, sorted alphabetically by exported name. NewFromUtf8 (with ToLocalChecked) replaces the compile-time NewFromUtf8Literal since the lambda parameter can't be a template-deduced string-literal reference; this runs at module init only, so the runtime cost is negligible. --- bindings/otel-thread-ctx.cc | 42 ++++++++++++------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/bindings/otel-thread-ctx.cc b/bindings/otel-thread-ctx.cc index 3859582d..c98bb209 100644 --- a/bindings/otel-thread-ctx.cc +++ b/bindings/otel-thread-ctx.cc @@ -710,35 +710,19 @@ void OtelThreadCtx::Init(Local exports) { Isolate* isolate = Isolate::GetCurrent(); Local ctx = isolate->GetCurrentContext(); - exports - ->Set(ctx, - String::NewFromUtf8Literal(isolate, - "otelThreadCtxWrappedObjectOffset"), - Integer::New(isolate, WRAPPED_OBJECT_OFFSET)) - .FromJust(); - exports - ->Set(ctx, - String::NewFromUtf8Literal(isolate, "otelThreadCtxTaggedSize"), - Integer::New(isolate, TAGGED_SIZE)) - .FromJust(); - exports - ->Set(ctx, - String::NewFromUtf8Literal(isolate, - "otelThreadCtxNativeWrapFieldsOffset"), - Integer::New(isolate, NATIVE_WRAP_FIELDS_OFFSET)) - .FromJust(); - exports - ->Set( - ctx, - String::NewFromUtf8Literal(isolate, "otelThreadCtxJsMapTableOffset"), - Integer::New(isolate, JS_MAP_TABLE_OFFSET)) - .FromJust(); - exports - ->Set(ctx, - String::NewFromUtf8Literal(isolate, - "otelThreadCtxOrderedHashMapHeaderSize"), - Integer::New(isolate, ORDERED_HASH_MAP_HEADER_SIZE)) - .FromJust(); + auto publish_int = [&](const char* name, int value) { + exports + ->Set(ctx, + String::NewFromUtf8(isolate, name).ToLocalChecked(), + Integer::New(isolate, value)) + .FromJust(); + }; + publish_int("otelThreadCtxJsMapTableOffset", JS_MAP_TABLE_OFFSET); + publish_int("otelThreadCtxNativeWrapFieldsOffset", NATIVE_WRAP_FIELDS_OFFSET); + publish_int("otelThreadCtxOrderedHashMapHeaderSize", + ORDERED_HASH_MAP_HEADER_SIZE); + publish_int("otelThreadCtxTaggedSize", TAGGED_SIZE); + publish_int("otelThreadCtxWrappedObjectOffset", WRAPPED_OBJECT_OFFSET); } } // namespace dd