Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/containerwatcher/v2/tracers/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions pkg/containerwatcher/v2/tracers/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion pkg/containerwatcher/v2/tracers/httpparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Comment on lines +180 to +183
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail closed when buf_len is zero or invalid.

On Line 180 and Line 183, buf_len == 0 (or out-of-range) falls back to the full buffer, which can reintroduce stale-data parsing. For this bug class, safer behavior is returning an empty slice when length metadata is invalid/unset.

🔧 Proposed fix
 func GetValidBuf(event utils.HttpRawEvent) []byte {
 	buf := event.GetBuf()
-	if n := event.GetBufLen(); n > 0 && int(n) <= len(buf) {
-		return buf[:n]
-	}
-	return buf
+	n := int(event.GetBufLen())
+	if n <= 0 || n > len(buf) {
+		return buf[:0]
+	}
+	return buf[:n]
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/containerwatcher/v2/tracers/httpparse.go` around lines 180 - 183, The
code currently returns the whole buffer when event.GetBufLen() is zero or
out-of-range, which can reintroduce stale data; update the logic in the
httpparse.go slice-return site to return an empty slice when n :=
event.GetBufLen() is <= 0 or int(n) > len(buf) so only a valid positive length
yields buf[:n]; specifically modify the branch that uses event.GetBufLen() and
buf to fail-closed (return empty slice) instead of returning buf.

}

func GetUniqueIdentifier(event utils.HttpRawEvent) string {
return strconv.FormatUint(event.GetSocketInode(), 10) + strconv.FormatUint(uint64(event.GetSockFd()), 10)
}
Expand Down
78 changes: 78 additions & 0 deletions pkg/containerwatcher/v2/tracers/httpparse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"testing"

"github.com/kubescape/node-agent/pkg/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -238,3 +239,80 @@ 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"

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))
})

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))
})
}
6 changes: 5 additions & 1 deletion pkg/ebpf/gadgets/http/program.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +231 to +233
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate payload copy success before publishing buf_len.

At Line 233 and Line 323, buf_len is set before verifying bpf_probe_read_user succeeded. If the copy fails, the event can still advertise valid bytes and reintroduce parse corruption/stale-data behavior.
Please gate publication on successful copy (or discard the reserved event on failure) so buf_len and buf stay consistent.

Also applies to: 322-323

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/ebpf/gadgets/http/program.bpf.c` around lines 231 - 233, The code
currently assigns dataevent->buf_len before checking whether bpf_probe_read_user
succeeded, allowing stale/invalid data to be published; change the flow in the
functions using dataevent, buf_copy_len, and bpf_probe_read_user so that you
first call bpf_probe_read_user and check its return value, and only on success
set dataevent->buf_len and keep/publish the event — on failure either discard
the reserved event (return early) or set buf_len to 0 and avoid publishing;
apply the same change for the second occurrence that uses packet->buf and
buf_copy_len to ensure buffer length and data remain consistent with the copy
result.


dataevent->timestamp_raw = bpf_ktime_get_boot_ns();

Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions pkg/ebpf/gadgets/http/program.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct httpevent {

u8 type;
u32 sock_fd;
u16 buf_len;
u8 buf[MAX_DATAEVENT_BUFFER];
u8 syscall[MAX_SYSCALL];

Expand Down
8 changes: 8 additions & 0 deletions pkg/utils/datasource_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/utils/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type HttpEvent interface {
type HttpRawEvent interface {
EnrichEvent
GetBuf() []byte
GetBufLen() uint16
GetDstIP() string
GetDstPort() uint16
GetSockFd() uint32
Expand Down
4 changes: 4 additions & 0 deletions pkg/utils/struct_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ func (e *StructEvent) GetBuf() []byte {
return e.Buf
}

func (e *StructEvent) GetBufLen() uint16 {
return uint16(len(e.Buf))
}
Comment on lines +119 to +121
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

StructEvent.GetBufLen() loses explicit kernel buf_len semantics.

GetBufLen() currently derives from len(Buf), which does not preserve the actual buf_len marker when Buf is fixed-capacity. This can skip truncation in GetValidBuf for StructEvent paths.

Suggested fix
 type StructEvent struct {
 	Addresses            []string                `json:"addresses,omitempty" yaml:"addresses,omitempty"`
 	Args                 []string                `json:"args,omitempty" yaml:"args,omitempty"`
 	AttrSize             uint32                  `json:"attrSize,omitempty" yaml:"attrSize,omitempty"`
 	Buf                  []byte                  `json:"buf,omitempty" yaml:"buf,omitempty"`
+	BufLen               uint16                  `json:"bufLen,omitempty" yaml:"bufLen,omitempty"`
 	CapName              string                  `json:"capName,omitempty" yaml:"capName,omitempty"`
 	...
 }

 func (e *StructEvent) GetBufLen() uint16 {
-	return uint16(len(e.Buf))
+	if e.BufLen > 0 {
+		return e.BufLen
+	}
+	return uint16(len(e.Buf))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/utils/struct_event.go` around lines 119 - 121, GetBufLen currently
returns uint16(len(e.Buf)) which loses the original kernel buf_len marker for
fixed-capacity Buf; change StructEvent to carry an explicit buf_len field (e.g.,
BufLen uint16) and implement GetBufLen to return that field instead of
len(e.Buf). Ensure the code that constructs/parses StructEvent sets BufLen from
the kernel/event metadata and update any consumers (notably GetValidBuf) to rely
on the explicit BufLen when deciding truncation/valid length.


func (e *StructEvent) GetCapability() string {
return e.CapName
}
Expand Down
Loading