cilium/cilium · error
resolving tail calls: %w
Error message
resolving tail calls: %w
What it means
LoadCollection wraps failures from resolveTailCalls, which populates the cilium_calls program array with programs annotated __declare_tail. Failures occur when the cilium_calls map is not a ProgramArray, a tail-call program has no resolvable slot, or two programs claim the same tail-call slot (duplicate slot). This guarantees the kernel program array maps each slot exactly once before the collection is created.
Source
Thrown at pkg/bpf/collection.go:251
if err := renameMaps(spec, opts.MapRenames); err != nil {
return nil, nil, fmt.Errorf("renaming maps: %w", err)
}
if err := applyConstants(spec, opts.Constants); err != nil {
return nil, nil, fmt.Errorf("applying variable overrides: %w", err)
}
reach, err := computeReachability(spec)
if err != nil {
return nil, nil, fmt.Errorf("computing reachability: %w", err)
}
if err := removeUnusedTailcalls(spec, reach, logger); err != nil {
return nil, nil, fmt.Errorf("removing unused tail calls: %w", err)
}
if err := resolveTailCalls(spec); err != nil {
return nil, nil, fmt.Errorf("resolving tail calls: %w", err)
}
fixed := fixedResources(spec, opts.Keep)
if err := removeUnusedMaps(spec, fixed, reach, logger); err != nil {
return nil, nil, fmt.Errorf("pruning unused maps: %w", err)
}
if err := dumpConstants(spec, opts); err != nil {
return nil, nil, fmt.Errorf("writing constants: %w", err)
}
if err := modifyAuxData(spec); err != nil {
return nil, nil, fmt.Errorf("loading auxiliary data: %w", err)
}
// Find and strip all CILIUM_PIN_REPLACE pinning flags before creating the
// Collection. ebpf-go will reject maps with pins it doesn't recognize.
toReplace := consumePinReplace(spec)View on GitHub (pinned to ac7b90affa)
Solutions
- Read the wrapped message: fix duplicate slot by giving each tail-call program a unique tail:N value in the C source.
- Ensure the map named cilium_calls (post-rename) is declared as BPF_MAP_TYPE_PROG_ARRAY.
- Do not merge objects that both declare the same tail-call slots; build one collection or adjust slot numbers.
- If you renamed maps with opts.MapRenames, confirm the rename target still points at the ProgramArray.
Example fix
// C before: two programs share slot 3
__declare_tail(3) int handle_ipv6(struct __ctx_buff *ctx) { ... }
__declare_tail(3) int handle_ipv4(struct __ctx_buff *ctx) { ... }
// resolving tail calls: duplicate tail call slot 3
// after: unique slots
__declare_tail(3) int handle_ipv6(struct __ctx_buff *ctx) { ... }
__declare_tail(4) int handle_ipv4(struct __ctx_buff *ctx) { ... } Defensive patterns
Strategy: validation
Validate before calling
func validateCallsMap(spec *ebpf.CollectionSpec) error {
ms, ok := spec.Maps["cilium_calls"]
if !ok {
return nil // resolveTailCalls no-ops when the map is absent
}
if ms.Type != ebpf.ProgramArray {
return fmt.Errorf("cilium_calls must be a program array, got %s", ms.Type)
}
return nil
} Type guard
func isProgramArray(spec *ebpf.CollectionSpec, name string) bool {
m, ok := spec.Maps[name]
return ok && m.Type == ebpf.ProgramArray
} Try / catch
if _, _, err := bpf.LoadCollection(logger, spec, opts); err != nil {
if strings.Contains(err.Error(), "resolving tail calls") {
if strings.Contains(err.Error(), "duplicate tail call slot") {
return fmt.Errorf("two programs claim the same tail-call slot; assign unique tail:N values in C source: %w", err)
}
return fmt.Errorf("cilium_calls map invalid or tail slot unresolved: %w", err)
}
return err
} Prevention
- Give every tail-call program a unique tail:N slot in the C source; keep slot assignments in one header.
- Never rename cilium_calls to a map that isn't BPF_MAP_TYPE_PROG_ARRAY via MapRenames.
- Load each compiled object exactly once per collection to avoid duplicate slot declarations.
- Add a CI check that builds the collection spec and asserts one program per slot.
When it happens
Trigger: Calling LoadCollection/LoadAndAssign where spec.Maps['cilium_calls'] exists but has a type other than ebpf.ProgramArray; a program marked IsTailCall returns an error from tailCallSlot ('no tail call slot'/'no function metadata'); or two programs resolve to the same slot index producing 'duplicate tail call slot %d'.
Common situations: Renaming or replacing the cilium_calls map via MapRenames so it loses its ProgramArray type; compiling two C files with colliding tail:N slot values into one collection; hand-built specs that include a calls map of the wrong type; including the same object twice so duplicate slots appear.
Related errors
- program %s is not a tail call
- program %s has no function metadata
- program %s has no tail call slot
- %s is not a program array, got %s
- getting tail call slot: %w
AI-assisted analysis of cilium/cilium@ac7b90affa (2026-08-31).
Data as JSON: /api/errors/34fc49e838e160d0.
Report an issue: GitHub.