Skip to content

decodeSyscalls misreads internal state footer byte as syscall bit, injecting bogus "unknown" into every generated profile #910

Description

@matthyx

Summary

decodeSyscalls in pkg/containerwatcher/v2/tracers/syscall.go decodes the raw per-mntns syscall bitmap produced by the vendored Inspektor Gadget advise_seccomp eBPF gadget, but it iterates one byte too far and misinterprets an internal state flag as a syscall bit. This injects a bogus "unknown" entry into every generated seccomp/application profile.

Root cause

In the advise_seccomp gadget (gadgets/advise_seccomp/program.bpf.c), the eBPF map value is:

#define SYSCALLS_COUNT 500
#define SYSCALLS_MAP_VALUE_FOOTER_SIZE 1
#define SYSCALLS_MAP_VALUE_SIZE (SYSCALLS_COUNT + SYSCALLS_MAP_VALUE_FOOTER_SIZE)

struct val_t {
	unsigned char syscalls[SYSCALLS_MAP_VALUE_SIZE]; // 501 bytes
};

Byte syscalls[SYSCALLS_COUNT] (index 500) is not a syscall bit — it's an internal "runc recording started" state flag, set once the program observes runc's prctl(PR_GET_PDEATHSIG) call:

if (is_runc) {
	if (syscall_bitmap->syscalls[SYSCALLS_COUNT] == 0) {
		if (id == __NR_prctl && PT_REGS_PARM1(&regs) == PR_GET_PDEATHSIG) {
			syscall_bitmap->syscalls[SYSCALLS_COUNT] = 1;
		}
		return 0;
	}
	...

Since virtually every container's runc init process trips this flag, byte 500 is set to 1 for essentially every observed container.

decodeSyscalls (pkg/containerwatcher/v2/tracers/syscall.go:141-153) iterates the entire raw buffer, including this footer byte:

func decodeSyscalls(syscallsBuffer []byte) []string {
	syscallStrings := make([]string, 0)
	for i := range syscallsBuffer {
		if syscallsBuffer[i] > 0 {
			syscallName, exist := syscalls.GetSyscallNameByNumber(i)
			if !exist {
				syscallName = "unknown"
			}
			syscallStrings = append(syscallStrings, syscallName)
		}
	}
	return syscallStrings
}

Index i=500 doesn't correspond to any real x86_64 syscall (the highest defined syscall number is ~461), so syscalls.GetSyscallNameByNumber(500) fails and the literal string "unknown" is appended to the decoded syscall list — and from there flows straight into the generated SeccompProfile/ApplicationProfile as a bogus allow-rule entry.

Live reproduction

On a real cluster (kubescape-managed node-agent, backend-storage mode), the generated SeccompProfile CR for a workload contained:

"syscalls": [{
  "names": ["accept4", "access", ..., "umask", "uname", "unknown", "unlinkat", ...],
  "action": "SCMP_ACT_ALLOW"
}]

"unknown" is not a valid syscall name on any architecture and has no meaning to the kernel/runc when the profile is compiled into a BPF filter.

Impact

  • Every generated profile that ever observed a runc-attributed syscall (i.e. essentially all of them) carries this spurious "unknown" entry.
  • We confirmed via live testing that removing "unknown" from the profile alone does not by itself fix a separately-observed enforcement issue on the same workload, so on the runtimes we tested, the bogus entry appears to be tolerated/ignored by the seccomp compiler rather than corrupting the whole rule — but this is undocumented/version-dependent behavior of the downstream OCI runtime, not something to rely on. On stricter or different seccomp-profile loaders, an unresolvable syscall name could plausibly cause the whole rule (or the whole profile) to be rejected.
  • More generally, this pollutes generated profiles with meaningless data, which is confusing when inspecting/auditing generated seccomp profiles and wastes an entry slot.

Suggested fix

In decodeSyscalls, exclude the footer byte from iteration, e.g.:

for i := 0; i < len(syscallsBuffer)-1; i++ { // skip the SYSCALLS_COUNT footer flag byte

or equivalently bound the loop to SYSCALLS_COUNT explicitly (matching the gadget's own SYSCALLS_COUNT constant) rather than len(syscallsBuffer), so the internal state byte is never treated as syscall data.

Environment

  • kubescape/node-agent v0.3.179 (vendored)
  • IG advise_seccomp gadget (github.com/matthyx/inspektor-gadget fork, pinned via replace directive)
  • Reproduced on a DigitalOcean K8s cluster, containerd 2.2.3, kernel 6.12.96, x86_64

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions