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(®s) == 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
Summary
decodeSyscallsinpkg/containerwatcher/v2/tracers/syscall.godecodes the raw per-mntns syscall bitmap produced by the vendored Inspektor Gadgetadvise_seccompeBPF 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_seccompgadget (gadgets/advise_seccomp/program.bpf.c), the eBPF map value is:Byte
syscalls[SYSCALLS_COUNT](index 500) is not a syscall bit — it's an internal "runc recording started" state flag, set once the program observesrunc'sprctl(PR_GET_PDEATHSIG)call:Since virtually every container's
runcinit process trips this flag, byte 500 is set to1for essentially every observed container.decodeSyscalls(pkg/containerwatcher/v2/tracers/syscall.go:141-153) iterates the entire raw buffer, including this footer byte:Index
i=500doesn't correspond to any real x86_64 syscall (the highest defined syscall number is ~461), sosyscalls.GetSyscallNameByNumber(500)fails and the literal string"unknown"is appended to the decoded syscall list — and from there flows straight into the generatedSeccompProfile/ApplicationProfileas a bogus allow-rule entry.Live reproduction
On a real cluster (kubescape-managed node-agent, backend-storage mode), the generated
SeccompProfileCR for a workload contained:"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
runc-attributed syscall (i.e. essentially all of them) carries this spurious"unknown"entry."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.Suggested fix
In
decodeSyscalls, exclude the footer byte from iteration, e.g.:or equivalently bound the loop to
SYSCALLS_COUNTexplicitly (matching the gadget's ownSYSCALLS_COUNTconstant) rather thanlen(syscallsBuffer), so the internal state byte is never treated as syscall data.Environment
kubescape/node-agentv0.3.179 (vendored)advise_seccompgadget (github.com/matthyx/inspektor-gadgetfork, pinned via replace directive)