From fd82fef7464fd668e34cb99652bc8343ed59370a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20J=2E=20Wal?= Date: Tue, 14 Jul 2026 15:51:26 +0200 Subject: [PATCH 1/2] Resolve issues with `unknown` Go version .TEXT --- .github/workflows/obi-patch-ci.yml | 67 +++++++++++++++++ ebpf/Dockerfile | 27 +++++-- .../obi/001-respect-child-exclusions.patch | 67 +++++++++++++++++ .../obi/002-reject-invalid-go-versions.patch | 71 +++++++++++++++++++ 4 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/obi-patch-ci.yml create mode 100644 ebpf/patches/obi/001-respect-child-exclusions.patch create mode 100644 ebpf/patches/obi/002-reject-invalid-go-versions.patch diff --git a/.github/workflows/obi-patch-ci.yml b/.github/workflows/obi-patch-ci.yml new file mode 100644 index 0000000..ad9e91e --- /dev/null +++ b/.github/workflows/obi-patch-ci.yml @@ -0,0 +1,67 @@ +name: OBI Patch CI + +on: + pull_request: + paths: + - "ebpf/patches/obi/**" + - "ebpf/Dockerfile" + - ".github/workflows/obi-patch-ci.yml" + push: + branches: + - main + paths: + - "ebpf/patches/obi/**" + - "ebpf/Dockerfile" + - ".github/workflows/obi-patch-ci.yml" + +permissions: + contents: read + +concurrency: + group: obi-patch-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-obi-patches: + runs-on: ubuntu-22.04 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - name: Read OBI build pins + run: | + { + echo "OBI_VERSION=$(grep -oP '^ARG OBI_VERSION=\K.*' ebpf/Dockerfile | head -1 | tr -d '\"')" + echo "OBI_REVISION=$(grep -oP '^ARG OBI_REVISION=\K.*' ebpf/Dockerfile | head -1 | tr -d '\"')" + echo "OBI_GENERATOR=$(grep -oP '^FROM \Kghcr.io/open-telemetry/obi-generator:[^ ]+' ebpf/Dockerfile | head -1)" + } >> "$GITHUB_ENV" + + - name: Clone OBI and apply patches + run: | + git clone --depth 1 --branch "$OBI_VERSION" \ + https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation.git "$RUNNER_TEMP/obi" + test "$(git -C "$RUNNER_TEMP/obi" rev-parse HEAD)" = "$OBI_REVISION" + git -C "$RUNNER_TEMP/obi" apply "$GITHUB_WORKSPACE"/ebpf/patches/obi/*.patch + + - name: Generate eBPF sources + run: | + docker run --rm \ + -v "$RUNNER_TEMP/obi:/src" \ + -w /src \ + "$OBI_GENERATOR" + + - uses: actions/setup-go@v5 + with: + go-version-file: ${{ runner.temp }}/obi/go.mod + cache: false + + - name: Download Go modules + working-directory: ${{ runner.temp }}/obi + run: go mod download + + - name: Test patched packages + working-directory: ${{ runner.temp }}/obi + run: go test -race ./pkg/appolly/discover ./pkg/internal/goexec + + - name: Build production OBI stage + run: docker build --target obi-builder --file ebpf/Dockerfile . diff --git a/ebpf/Dockerfile b/ebpf/Dockerfile index 094f2c5..3525bca 100644 --- a/ebpf/Dockerfile +++ b/ebpf/Dockerfile @@ -48,12 +48,25 @@ FROM quay.io/prometheuscommunity/elasticsearch-exporter:v1.11.0 AS elasticsearch # Add pgbouncer exporter to the image FROM prometheuscommunity/pgbouncer-exporter:v0.12.0 AS pgbouncer-exporter -# Download OBI binary from GitHub release (Docker image otel/ebpf-instrument:v0.5.0 exists but is amd64-only) -FROM debian:13.5-slim AS obi-source +# Build OBI from source with patches +FROM ghcr.io/open-telemetry/obi-generator:0.2.15@sha256:9cbb1b567377d5779b04e6bcdb87431c77a19e797b4630eba30f5417de96ea33 AS obi-builder +RUN apk add --no-cache bash git make ARG TARGETARCH +ARG CUSTOMIZATION_TAG="betterstack" ARG OBI_VERSION="v0.10.0" -ADD https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/releases/download/${OBI_VERSION}/obi-${OBI_VERSION}-linux-${TARGETARCH}.tar.gz /tmp/obi.tar.gz -RUN tar -xzf /tmp/obi.tar.gz -C /tmp +ARG OBI_REVISION="4a4dc7ecdb2aeba6791d02fbebb5048c0f0c1261" +RUN git clone --depth 1 --branch ${OBI_VERSION} \ + https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation.git /tmp/obi && \ + test "$(git -C /tmp/obi rev-parse HEAD)" = "${OBI_REVISION}" +WORKDIR /tmp/obi +RUN --mount=type=cache,target=/go/pkg/mod go mod download +COPY ebpf/patches/obi/*.patch /tmp/obi-patches/ +RUN git apply /tmp/obi-patches/*.patch +ENV GOARCH=${TARGETARCH} +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + /generate.sh && \ + make compile RELEASE_VERSION="${OBI_VERSION}-${CUSTOMIZATION_TAG}" # Final stage - Debian slim for glibc compatibility with node-agent FROM debian:13.5-slim @@ -79,9 +92,9 @@ RUN curl -sS https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \ # Create necessary directories RUN mkdir -p /etc/supervisor/conf.d /var/log/supervisor /enrichment /bootstrap -# Copy OBI binary and license files from extracted tarball -COPY --from=obi-source --chmod=755 /tmp/obi /usr/local/bin/ebpf-instrument -COPY --from=obi-source /tmp/LICENSE /tmp/NOTICE /usr/share/doc/ebpf-instrument/ +# Copy OBI binary and license files +COPY --from=obi-builder --chmod=755 /tmp/obi/bin/obi /usr/local/bin/ebpf-instrument +COPY --from=obi-builder /tmp/obi/LICENSE /tmp/obi/NOTICE /usr/share/doc/ebpf-instrument/ # Copy Node Agent COPY --from=node-agent-builder --chmod=755 /usr/bin/coroot-node-agent /usr/local/bin/node-agent diff --git a/ebpf/patches/obi/001-respect-child-exclusions.patch b/ebpf/patches/obi/001-respect-child-exclusions.patch new file mode 100644 index 0000000..16730c1 --- /dev/null +++ b/ebpf/patches/obi/001-respect-child-exclusions.patch @@ -0,0 +1,67 @@ +diff --git a/pkg/appolly/discover/matcher.go b/pkg/appolly/discover/matcher.go +index 414feac82..79a4c8796 100644 +--- a/pkg/appolly/discover/matcher.go ++++ b/pkg/appolly/discover/matcher.go +@@ -183,7 +183,7 @@ func (m *Matcher) filterCreated(obj ProcessAttrs) (Event[ProcessMatch], bool) { + } + + // We didn't match the process, but let's see if the parent PID is tracked, it might be the child hasn't opened the port yet +- if procMatch, ok := m.ProcessHistory[proc.PPid]; ok { ++ if procMatch, ok := m.ProcessHistory[proc.PPid]; ok && !m.isExcluded(&obj, proc) { + m.Log.Debug("found process by matching the process parent id", "pid", proc.Pid, "ppid", proc.PPid, "comm", proc.ExePath, "metadata", obj.metadata) + + procMatch.Process = proc +diff --git a/pkg/appolly/discover/matcher_test.go b/pkg/appolly/discover/matcher_test.go +index 10da151ae..c4b28eb99 100644 +--- a/pkg/appolly/discover/matcher_test.go ++++ b/pkg/appolly/discover/matcher_test.go +@@ -448,6 +448,49 @@ func TestCriteriaMatcherMissingPort(t *testing.T) { + assert.Zero(t, matches[1].Obj.DynamicSelectorPID) + } + ++func TestCriteriaMatcherExcludedChildDoesNotInheritParentMatch(t *testing.T) { ++ pipeConfig := obi.Config{} ++ require.NoError(t, yaml.Unmarshal([]byte(`discovery: ++ instrument: ++ - name: port-only ++ open_ports: 80 ++ exclude_instrument: ++ - exe_path: /tmp/provjob* ++`), &pipeConfig)) ++ ++ discoveredProcesses := msg.NewQueue[[]Event[ProcessAttrs]](msg.ChannelBufferLen(10)) ++ filteredProcessesQu := msg.NewQueue[[]Event[ProcessMatch]](msg.ChannelBufferLen(10)) ++ filteredProcesses := filteredProcessesQu.Subscribe() ++ matcherFunc, err := criteriaMatcherProvider(&pipeConfig, discoveredProcesses, filteredProcessesQu, FindingCriteria(&pipeConfig), nil)(t.Context()) ++ require.NoError(t, err) ++ go matcherFunc(t.Context()) ++ defer filteredProcessesQu.Close() ++ ++ processInfo = func(pp ProcessAttrs) (*services.ProcessInfo, error) { ++ proc := map[app.PID]struct { ++ Exe string ++ PPid app.PID ++ }{ ++ 1: {Exe: "/bin/parent"}, ++ 2: {Exe: "/bin/allowed", PPid: 1}, ++ 3: {Exe: "/tmp/provjob123 (deleted)", PPid: 1}, ++ }[pp.pid] ++ return &services.ProcessInfo{Pid: pp.pid, ExePath: proc.Exe, PPid: proc.PPid, OpenPorts: pp.openPorts}, nil ++ } ++ ++ discoveredProcesses.Send([]Event[ProcessAttrs]{ ++ {Type: EventCreated, Obj: ProcessAttrs{pid: 1, openPorts: []uint32{80}}}, ++ {Type: EventCreated, Obj: ProcessAttrs{pid: 2}}, ++ {Type: EventCreated, Obj: ProcessAttrs{pid: 3}}, ++ }) ++ ++ matches := testutil.ReadChannel(t, filteredProcesses, testTimeout) ++ require.Len(t, matches, 2) ++ assert.Equal(t, app.PID(1), matches[0].Obj.Process.Pid) ++ assert.Equal(t, app.PID(2), matches[1].Obj.Process.Pid) ++ assert.NotContains(t, matches[1].Obj.Process.ExePath, "provjob") ++} ++ + func TestDynamicMatcher_ChildInheritsDynamicSelectorPID(t *testing.T) { + dynamicSelector := NewDynamicPIDSelector() + dynamicSelector.AddPIDs(100) diff --git a/ebpf/patches/obi/002-reject-invalid-go-versions.patch b/ebpf/patches/obi/002-reject-invalid-go-versions.patch new file mode 100644 index 0000000..77fc50e --- /dev/null +++ b/ebpf/patches/obi/002-reject-invalid-go-versions.patch @@ -0,0 +1,71 @@ +diff --git a/pkg/internal/goexec/instructions.go b/pkg/internal/goexec/instructions.go +index 0c7d62447..e41f06e7c 100644 +--- a/pkg/internal/goexec/instructions.go ++++ b/pkg/internal/goexec/instructions.go +@@ -304,6 +304,10 @@ func loadModuledataOffsets(elfF *elf.File) (moduledataOffsets, error) { + return moduledataOffsets{}, fmt.Errorf("getting Go version: %w", err) + } + ++ if !supportedGoVersion(goVersion) { ++ return moduledataOffsets{}, fmt.Errorf("unsupported Go version: %v. Minimum supported version is %v", goVersion, minGoVersion) ++ } ++ + goVersion = strings.ReplaceAll(goVersion, "go", "") + + offs, err := offsets.Read(bytes.NewBufferString(prefetchedOffsets)) +diff --git a/pkg/internal/goexec/moduledata_test.go b/pkg/internal/goexec/moduledata_test.go +index 72c25f576..e13d417ac 100644 +--- a/pkg/internal/goexec/moduledata_test.go ++++ b/pkg/internal/goexec/moduledata_test.go +@@ -77,7 +77,7 @@ const ( + // + // pclntableData is the value written into the runtime.moduledata pclntable.data field. + // All other moduledata fields are fixed (pcHeader = testGopclntabVMA, text = testTextVMA, …). +-func buildTestELF(t *testing.T, pclntableData uint64) *elf.File { ++func buildTestELF(t *testing.T, pclntableData uint64, goVersions ...string) *elf.File { + t.Helper() + + const ( +@@ -161,6 +161,17 @@ func buildTestELF(t *testing.T, pclntableData uint64) *elf.File { + put64(db+int(testMDText), testTextVMA) // text + put64(db+int(testMDEtext), testTextVMA+testTextSize) // etext + ++ if len(goVersions) > 0 { ++ buildInfoOffset := db + 0x200 ++ copy(buf[buildInfoOffset:], buildInfoMagic) ++ buf[buildInfoOffset+14] = 8 ++ buf[buildInfoOffset+15] = 2 ++ buildInfo := binary.AppendUvarint(nil, uint64(len(goVersions[0]))) ++ buildInfo = append(buildInfo, goVersions[0]...) ++ buildInfo = append(buildInfo, 0) ++ copy(buf[buildInfoOffset+32:], buildInfo) ++ } ++ + // ── .shstrtab ──────────────────────────────────────────────────────────────── + shstrtab := "\x00.text\x00.gopclntab\x00.data\x00.shstrtab\x00" + copy(buf[shstrtabFileOff:], shstrtab) +@@ -208,6 +219,24 @@ func buildTestELF(t *testing.T, pclntableData uint64) *elf.File { + return f + } + ++func TestLoadModuledataOffsets(t *testing.T) { ++ elfF := buildTestELF(t, testGopclntabVMA, "go1.26.4") ++ ++ actual, err := loadModuledataOffsets(elfF) ++ require.NoError(t, err) ++ require.Equal(t, testMDOffsets, actual) ++} ++ ++func TestLoadModuledataOffsetsRejectsInvalidGoVersion(t *testing.T) { ++ elfF := buildTestELF(t, testGopclntabVMA, "unknown") ++ ++ var err error ++ require.NotPanics(t, func() { ++ _, err = loadModuledataOffsets(elfF) ++ }) ++ require.ErrorContains(t, err, "unsupported Go version: unknown") ++} ++ + // emptyRelocs returns a zero-populated relocationInfo (no RELA, no RELR). + func emptyRelocs() relocationInfo { + return relocationInfo{ From 48dc967ce115572c8dcd480387305c90cce18305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20J=2E=20Wal?= Date: Wed, 15 Jul 2026 08:45:44 +0200 Subject: [PATCH 2/2] Bump COLLECTOR_VERSION to 1.1.30 --- collector/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collector/Dockerfile b/collector/Dockerfile index d661965..ef875f0 100644 --- a/collector/Dockerfile +++ b/collector/Dockerfile @@ -59,7 +59,7 @@ RUN mkdir -p /versions/0-default \ # Set environment variables ENV BASE_URL=https://telemetry.betterstack.com ENV CLUSTER_COLLECTOR=false -ENV COLLECTOR_VERSION=1.1.29 +ENV COLLECTOR_VERSION=1.1.30 ENV VECTOR_VERSION=0.47.0 ENV OBI_VERSION=0.10.0 ENV CLUSTER_AGENT_VERSION=1.6.1