Skip to content

Interface table grows without bound on a host with interface churn #2709

Description

@cplieger

Describe the bug

The interface table only ever grows. On RTM_DELLINK the entry is kept with ifindex zeroed (vrrp_if.c#L1519), and free_if() is only reachable from free_interface_queue(), which runs once in the child's shutdown path (vrrp_daemon.c#L330). So on a host that creates and destroys interfaces, the table grows for the life of the process.

I understand why the entry is kept, and the code says so where it is reused by name: garp_delay and tracking_vrrp are set up by name, so preserving them is reasonable. My problem is that nothing bounds the accumulation on a host where interface names are never reused, which is every container host: each container on a bridge network creates a veth.

Two costs. if_get_by_ifname() and if_get_by_ifindex() walk if_queue linearly (vrrp_if.c#L201), so every netlink message pays for every interface the host has ever had. And the address space grows: I measured about 0.33 kB of VmLck per interface in the reproduction below.

The growth also turns vrrp_no_swap into a repeating outage, which is how I found it. mlockall(MCL_FUTURE) charges the growth against RLIMIT_MEMLOCK, so the child eventually fails an allocation and exits 204; the parent respawns it, and the replacement starts in BACKUP and removes the VIPs. On the host where I hit it, the limit is 8192 kB, a fresh child starts with 7660 kB locked and dies at about 8184 kB; that has been three respawns today, each interrupting the long-lived connections I had through the node.

This only bites where the limit is actually enforced. With CAP_IPC_LOCK it is not, so a root process on an ordinary host is unaffected, while a container that does not add that capability is subject to it. I have sent a small PR for the diagnostics half in #2710, since the exit gave no hint which limit was involved.

Note for anyone reproducing: my musl build starts at 7660 kB locked and the Debian build below starts at 10316 kB, so the limit that produces the failure differs between them. With an 8192 kB limit the Debian build cannot lock its own startup footprint at all and logs Unable to lock process in memory instead, which is a different thing entirely.

To Reproduce

docker run --rm -it --cap-add NET_ADMIN --cap-add NET_RAW debian:trixie-slim
# in the container, with keepalived installed and the config below:
ip link add ka0 type dummy && ip addr add 10.99.0.2/24 dev ka0 && ip link set ka0 up
keepalived --dont-fork --log-console --log-detail &
for i in $(seq 1 1600); do ip link add "v$i" type veth peer name "p$i"; ip link del "v$i"; done

Both ends of each pair live in the container's namespace, so 1600 iterations present 3200 interfaces. VmLck in /proc/<vrrp child>/status climbs monotonically: 10316 kB at startup here, and about 0.33 kB more per interface for as long as the loop runs.

To see it as a failure rather than as growth, add --ulimit memlock=11534336 to the docker run (just above the startup footprint here); the child then dies partway through the loop. Dropping vrrp_no_swap from the config stops the failure and leaves the growth.

Expected behavior

Accumulation bounded by something other than process lifetime. I do not know which entries are safe to release: entries are reused by name, and other objects hold interface_t *, so an empty tracking_vrrp is not on its own proof that an entry is unreferenced. Is there a way to distinguish an entry that only ever came from transient netlink discovery, and release it once nothing refers to it?

Keepalived version

Keepalived v2.4.3 (08/11,2026)

Copyright(C) 2001-2026 Alexandre Cassen, <acassen@gmail.com>

Built with kernel headers for Linux 6.12.101
Running on Linux 6.12.95-production+truenas #1 SMP PREEMPT_DYNAMIC Wed Jul 22 16:39:01 UTC 2026
Distro: Debian GNU/Linux 13 (trixie)

Built from the v2.4.3 tag with --disable-lvs --disable-bfd. I also see the growth with a musl build of 2.4.3, which is what I run.

Distro (please complete the following information):

  • Name: Debian GNU/Linux (reproduction), Alpine Linux (production)
  • Version: 13 (trixie)
  • Architecture: x86_64

Details of any containerisation or hosted service (e.g. AWS)

Docker 28.3.1, host networking, --cap-add NET_ADMIN --cap-add NET_RAW, and no added IPC_LOCK. /proc/<pid>/limits reported Max locked memory 8388608 8388608, which is where the enforcement comes from.

Configuration file:

global_defs {
    router_id REPRO
    vrrp_no_swap
}
vrrp_instance VI_1 {
    state MASTER
    interface ka0
    virtual_router_id 51
    priority 150
    advert_int 1
    virtual_ipaddress {
        10.99.0.78/24
    }
}

Notify and track scripts

None in the reproduction.

System Log entries

Startup, then the end of the loop. The unprefixed line is perror() from mem_alloc_error(), so it carries no timestamp:

Tue Aug 11 13:37:56 2026: Starting Keepalived v2.4.3 (08/11,2026)
Tue Aug 11 13:37:56 2026: Command line: 'keepalived' '--dont-fork' '--log-console' '--log-detail'
Tue Aug 11 13:37:56 2026: Opening file '/etc/keepalived/keepalived.conf'.
Tue Aug 11 13:37:56 2026: Configuration file /etc/keepalived/keepalived.conf
Keepalived: Cannot allocate memory
Tue Aug 11 13:39:12 2026: pid 351 exited with status 204
Tue Aug 11 13:39:12 2026: VRRP child process(351) died: Respawning
Tue Aug 11 13:39:12 2026:   Please log an issue at https://github.com/acassen/keepalived/issues/
Tue Aug 11 13:39:12 2026:   and include a full copy of your keepalived configuration files, and

I can attach the full log from startup to respawn if that is useful; it is mostly 3200 interface add and delete lines.

Did keepalived coredump?

No.

Additional context

Happy to test a patch, or to run a longer measurement if a count of retained entries would help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions