From beda638b5388fd7e2bbcf44ea69938471140768e Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 5 Mar 2026 11:46:54 +0200 Subject: [PATCH 1/3] add buf_len field to HTTP gadget to fix ring buffer data bleed Signed-off-by: Yakir Oren --- pkg/containerwatcher/v2/tracers/http.go | 2 +- pkg/containerwatcher/v2/tracers/httpparse.go | 12 +++++++- .../v2/tracers/httpparse_test.go | 30 +++++++++++++++++++ pkg/ebpf/gadgets/http/program.bpf.c | 6 +++- pkg/ebpf/gadgets/http/program.h | 1 + pkg/utils/datasource_event.go | 8 +++++ pkg/utils/events.go | 1 + pkg/utils/struct_event.go | 4 +++ 8 files changed, 61 insertions(+), 3 deletions(-) diff --git a/pkg/containerwatcher/v2/tracers/http.go b/pkg/containerwatcher/v2/tracers/http.go index 96ffb1833..59994d660 100644 --- a/pkg/containerwatcher/v2/tracers/http.go +++ b/pkg/containerwatcher/v2/tracers/http.go @@ -167,7 +167,7 @@ func (ht *HTTPTracer) GroupEvents(bpfEvent utils.HttpRawEvent) utils.HttpEvent { case utils.Response: if exists, ok := ht.eventsMap.Get(id); ok { grouped := exists - response, err := ParseHttpResponse(FromCString(bpfEvent.GetBuf()), grouped.GetRequest()) + response, err := ParseHttpResponse(GetValidBuf(bpfEvent), grouped.GetRequest()) if err != nil { return nil } diff --git a/pkg/containerwatcher/v2/tracers/httpparse.go b/pkg/containerwatcher/v2/tracers/httpparse.go index af53c92eb..d1c011cb4 100644 --- a/pkg/containerwatcher/v2/tracers/httpparse.go +++ b/pkg/containerwatcher/v2/tracers/httpparse.go @@ -38,7 +38,7 @@ var ConsistentHeaders = []string{ } func CreateEventFromRequest(bpfEvent utils.HttpRawEvent) (utils.HttpEvent, error) { - request, err := ParseHttpRequest(FromCString(bpfEvent.GetBuf())) + request, err := ParseHttpRequest(GetValidBuf(bpfEvent)) if err != nil { return nil, err } @@ -173,6 +173,16 @@ func FromCString(in []byte) []byte { return in } +// GetValidBuf returns only the valid portion of the BPF event buffer. +// It uses buf_len set by the gadget to determine the actual data length. +func GetValidBuf(event utils.HttpRawEvent) []byte { + buf := event.GetBuf() + if n := event.GetBufLen(); n > 0 && int(n) <= len(buf) { + return buf[:n] + } + return buf +} + func GetUniqueIdentifier(event utils.HttpRawEvent) string { return strconv.FormatUint(event.GetSocketInode(), 10) + strconv.FormatUint(uint64(event.GetSockFd()), 10) } diff --git a/pkg/containerwatcher/v2/tracers/httpparse_test.go b/pkg/containerwatcher/v2/tracers/httpparse_test.go index 62d90fb42..86a95291d 100644 --- a/pkg/containerwatcher/v2/tracers/httpparse_test.go +++ b/pkg/containerwatcher/v2/tracers/httpparse_test.go @@ -5,6 +5,7 @@ import ( "io" "testing" + "github.com/kubescape/node-agent/pkg/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -238,3 +239,32 @@ func TestParseHttpResponse_ContentLengthTruncatesGarbage(t *testing.T) { }) } } + +func TestGetValidBuf(t *testing.T) { + httpData := "POST /api HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello" + + t.Run("uses buf_len when set", func(t *testing.T) { + event := &utils.StructEvent{Buf: []byte(httpData)} + result := GetValidBuf(event) + assert.Equal(t, len(httpData), len(result)) + assert.Equal(t, httpData, string(result)) + }) + + t.Run("buf_len truncates garbage from ring buffer", func(t *testing.T) { + // BPF buffer has valid data followed by a previous event's data (no null byte separator). + previousEvent := "GET /old HTTP/1.1\r\nHost: old.com\r\n\r\n" + buf := makeBPFBuffer(httpData + previousEvent) + // With buf_len set correctly, only the real data is returned. + event := &utils.StructEvent{Buf: buf[:len(httpData)]} + result := GetValidBuf(event) + assert.Equal(t, len(httpData), len(result)) + assert.Equal(t, httpData, string(result)) + + // Verify the parsed request is clean. + req, err := ParseHttpRequest(result) + require.NoError(t, err) + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + assert.Equal(t, "hello", string(body)) + }) +} diff --git a/pkg/ebpf/gadgets/http/program.bpf.c b/pkg/ebpf/gadgets/http/program.bpf.c index b5e7660cb..906b1a2c5 100644 --- a/pkg/ebpf/gadgets/http/program.bpf.c +++ b/pkg/ebpf/gadgets/http/program.bpf.c @@ -227,7 +227,10 @@ static __always_inline int process_packet(struct syscall_trace_exit *ctx, char * dataevent->sock_fd = packet->sockfd; bpf_probe_read_str(&dataevent->syscall, sizeof(dataevent->syscall), syscall); - bpf_probe_read_user(&dataevent->buf, min_size(total_size, MAX_DATAEVENT_BUFFER), (void *)packet->buf); + + __u64 buf_copy_len = min_size(total_size, MAX_DATAEVENT_BUFFER); + dataevent->buf_len = (__u16)buf_copy_len; + bpf_probe_read_user(&dataevent->buf, buf_copy_len, (void *)packet->buf); dataevent->timestamp_raw = bpf_ktime_get_boot_ns(); @@ -316,6 +319,7 @@ static __always_inline int process_msg(struct syscall_trace_exit *ctx, char *sys if (copy_len > MAX_DATAEVENT_BUFFER) copy_len = MAX_DATAEVENT_BUFFER; + dataevent->buf_len = (__u16)copy_len; bpf_probe_read_user(&dataevent->buf, copy_len, iov.iov_base); bpf_probe_read_str(&dataevent->syscall, sizeof(dataevent->syscall), syscall); diff --git a/pkg/ebpf/gadgets/http/program.h b/pkg/ebpf/gadgets/http/program.h index ed974d0e2..48ba3a29c 100644 --- a/pkg/ebpf/gadgets/http/program.h +++ b/pkg/ebpf/gadgets/http/program.h @@ -41,6 +41,7 @@ struct httpevent { u8 type; u32 sock_fd; + u16 buf_len; u8 buf[MAX_DATAEVENT_BUFFER]; u8 syscall[MAX_SYSCALL]; diff --git a/pkg/utils/datasource_event.go b/pkg/utils/datasource_event.go index a7a58084d..55abaca20 100644 --- a/pkg/utils/datasource_event.go +++ b/pkg/utils/datasource_event.go @@ -216,6 +216,14 @@ func (e *DatasourceEvent) GetBuf() []byte { return buf } +func (e *DatasourceEvent) GetBufLen() uint16 { + bufLen, err := e.getFieldAccessor("buf_len").Uint16(e.Data) + if err != nil { + return 0 + } + return bufLen +} + func (e *DatasourceEvent) GetCapability() string { capability, _ := e.getFieldAccessor("cap").String(e.Data) return capability diff --git a/pkg/utils/events.go b/pkg/utils/events.go index 1b383c7e3..fda2e5a66 100644 --- a/pkg/utils/events.go +++ b/pkg/utils/events.go @@ -123,6 +123,7 @@ type HttpEvent interface { type HttpRawEvent interface { EnrichEvent GetBuf() []byte + GetBufLen() uint16 GetDstIP() string GetDstPort() uint16 GetSockFd() uint32 diff --git a/pkg/utils/struct_event.go b/pkg/utils/struct_event.go index 5995d2db7..278c8ebe8 100644 --- a/pkg/utils/struct_event.go +++ b/pkg/utils/struct_event.go @@ -116,6 +116,10 @@ func (e *StructEvent) GetBuf() []byte { return e.Buf } +func (e *StructEvent) GetBufLen() uint16 { + return uint16(len(e.Buf)) +} + func (e *StructEvent) GetCapability() string { return e.CapName } From 45f4d2b74c3d0bb68387fc3626a15cf680a015eb Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 5 Mar 2026 16:03:41 +0200 Subject: [PATCH 2/3] add buf_len to TestHttpFields expected fields Signed-off-by: Yakir Oren --- pkg/containerwatcher/v2/tracers/http_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/containerwatcher/v2/tracers/http_test.go b/pkg/containerwatcher/v2/tracers/http_test.go index 51e582d5f..b3c62e5c8 100644 --- a/pkg/containerwatcher/v2/tracers/http_test.go +++ b/pkg/containerwatcher/v2/tracers/http_test.go @@ -17,6 +17,7 @@ func TestHttpFields(t *testing.T) { expectedFields := map[string][]string{ "http": { "buf", + "buf_len", "dst", "dst.addr_raw", "dst.addr_raw.v4", From 3819b3e060d6c35bb7fefa14a40197c71a2c65b4 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 5 Mar 2026 16:27:52 +0200 Subject: [PATCH 3/3] add GetValidBuf edge case tests for buf_len handling Signed-off-by: Yakir Oren --- .../v2/tracers/httpparse_test.go | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pkg/containerwatcher/v2/tracers/httpparse_test.go b/pkg/containerwatcher/v2/tracers/httpparse_test.go index 86a95291d..69f9e5fc1 100644 --- a/pkg/containerwatcher/v2/tracers/httpparse_test.go +++ b/pkg/containerwatcher/v2/tracers/httpparse_test.go @@ -240,6 +240,15 @@ func TestParseHttpResponse_ContentLengthTruncatesGarbage(t *testing.T) { } } +// mockHttpRawEvent allows testing GetValidBuf with explicit buf_len values +// that differ from the actual buffer length (to test edge cases). +type mockHttpRawEvent struct { + utils.StructEvent + bufLen uint16 +} + +func (m *mockHttpRawEvent) GetBufLen() uint16 { return m.bufLen } + func TestGetValidBuf(t *testing.T) { httpData := "POST /api HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello" @@ -267,4 +276,43 @@ func TestGetValidBuf(t *testing.T) { require.NoError(t, err) assert.Equal(t, "hello", string(body)) }) + + t.Run("buf_len < len(buf) truncates correctly", func(t *testing.T) { + // Normal case: buf_len indicates valid data is shorter than buffer. + buf := makeBPFBuffer(httpData) + event := &mockHttpRawEvent{StructEvent: utils.StructEvent{Buf: buf}, bufLen: uint16(len(httpData))} + result := GetValidBuf(event) + assert.Equal(t, len(httpData), len(result), "should truncate to buf_len") + assert.Equal(t, httpData, string(result)) + }) + + t.Run("buf_len=0 returns full buffer (backward compatibility)", func(t *testing.T) { + // When buf_len is 0 (error case), we fall back to returning the full buffer. + buf := makeBPFBuffer(httpData) + event := &mockHttpRawEvent{StructEvent: utils.StructEvent{Buf: buf}, bufLen: 0} + result := GetValidBuf(event) + assert.Equal(t, len(buf), len(result), "buf_len=0 should return full buffer") + }) + + t.Run("buf_len > len(buf) returns full buffer (safety)", func(t *testing.T) { + // If buf_len exceeds buffer size, return full buffer as safety measure. + buf := []byte(httpData) + event := &mockHttpRawEvent{StructEvent: utils.StructEvent{Buf: buf}, bufLen: uint16(len(buf) + 100)} + result := GetValidBuf(event) + assert.Equal(t, len(buf), len(result), "buf_len > len(buf) should return full buffer") + }) + + t.Run("empty buffer", func(t *testing.T) { + event := &mockHttpRawEvent{StructEvent: utils.StructEvent{Buf: []byte{}}, bufLen: 0} + result := GetValidBuf(event) + assert.Empty(t, result) + }) + + t.Run("buf_len equals buffer length", func(t *testing.T) { + buf := []byte(httpData) + event := &mockHttpRawEvent{StructEvent: utils.StructEvent{Buf: buf}, bufLen: uint16(len(buf))} + result := GetValidBuf(event) + assert.Equal(t, len(buf), len(result)) + assert.Equal(t, httpData, string(result)) + }) }