Thread (2 messages) flat view 2 messages, 2 authors, 29d ago

[nf_tables] netdev chain: DELCHAIN with an exact device name matches a prefix hook and deletes the chain

From: Wei Fang <hidden>
Date: 2026-08-11 10:29:49
Also in: netfilter-devel

Hi netfilter-devel,

The following nf_tables issue was found by metamorphic testing on
linux-next 7.2.0-rc6-next-20260803 (x86_64) and reproduced on four
independent VMs.  The reproducer is a single self-contained C program
that uses only the raw netlink API (no nft CLI involved).

This report is about netdev-chain device matching: a chain hooked
via the device prefix "vbug" can be deleted by a DELCHAIN request
that names the exact device "vbug0", even though that device is not
a member of the prefix hook.

Problem
-------
A chain hooked via the device prefix vbug (covering vbug0 and
vbug1) can be deleted by specifying the exact name vbug0 in the
DELCHAIN request - a device that is not a member of the prefix hook.
Deleting by a non-member name must fail with -ENOENT; instead it
succeeds and the whole chain is removed.

Steps to reproduce
------------------
netdev table, chain dev (filter, ingress) hooked with prefix vbug
(veths vbug0/vbug1 exist):

1. control: DELCHAIN with HOOK_DEVS {DEV_NAME "zzz"} (no match
   either way) -> -ENOENT;
2. variant: DELCHAIN with HOOK_DEVS {DEV_NAME "vbug0"} (an exact
   name under the prefix).

Both requests are symmetric: neither zzz nor vbug0 is a member of
the prefix hook's device set.

Expected vs. actual
-------------------
expected: both -ENOENT
actual:   control -ENOENT; variant 0 - chain fully removed

Root cause
----------
net/netfilter/nf_tables_api.c, nft_delchain_hook(): the device in
the delete request is matched against the registered hooks with
nft_hook_list_find(), which uses a prefix comparison (strncmp over
min(ifnamelen)).  The exact name "vbug0" (ifnamelen 6) compares
equal to the registered prefix "vbug" (ifnamelen 4) up to 4 chars,
so it is treated as a member and the hook is scheduled for removal.

Reproducer
--------------

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netlink.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

/* NFT_MSG_* enum comes directly from <linux/netfilter/nf_tables.h>
 * (NEWTABLE=0, GETTABLE=1, DELTABLE=2, NEWCHAIN=3, ..., NEWRULE=6) -
 * do not #define the same names, it would break the header enum. */
#define NFT_MSG_TYPE(s, m) (((s) << 8) | (m))

#ifndef NFTA_DEV_NAME
/* Host headers lack the nft_dev_attributes enum; in the VM linux-next
 * uapi NFTA_DEV_NAME=1, NFTA_DEV_PREFIX=2 (verified by the Python
 * version). Defined only when the header lacks them. */
#define NFTA_DEV_NAME 1
#endif
#ifndef NFTA_DEV_PREFIX
#define NFTA_DEV_PREFIX 2
#endif

/* ---------------- Encoding helpers (wire matches the Python
reference) ---------------- */

static void put_u16(char **p, uint16_t v) { memcpy(*p, &v, 2); *p += 2; }
static void put_u32(char **p, uint32_t v) { memcpy(*p, &v, 4); *p += 4; }

/* attr: little-endian len/type header + payload, 4-byte aligned at
the end (len excludes padding) */
static void put_attr(char **p, uint16_t type, const void *data, uint16_t len)
{
    put_u16(p, 4 + len);
    put_u16(p, type);
    if (len)
        memcpy(*p, data, len);
    *p += len;
    while ((uintptr_t)*p % 4) {
        **p = 0;
        (*p)++;
    }
}

static void put_attr_be32(char **p, uint16_t type, uint32_t v)
{
    uint32_t be = htonl(v);
    put_attr(p, type, &be, 4);
}

static void put_attr_str(char **p, uint16_t type, const char *s)
{
    put_attr(p, type, s, strlen(s) + 1);
}

static void put_nfgenmsg(char **p, uint8_t family, uint16_t res_id)
{
    char *q = *p;
    q[0] = family;
    q[1] = 0;
    uint16_t be = htons(res_id);
    memcpy(q + 2, &be, 2);
    *p += 4;
}

/* nlmsghdr + nfgenmsg + attrs; returns total length (header included) */
static int build_msg(char *buf, uint16_t type, uint16_t flags, uint32_t seq,
                     uint8_t family, uint16_t res_id, char *attrs, int alen)
{
    char *p = buf;
    put_u32(&p, 0);
    put_u16(&p, type);
    put_u16(&p, flags);
    put_u32(&p, seq);
    put_u32(&p, 0);
    put_nfgenmsg(&p, family, res_id);
    memcpy(p, attrs, alen);
    p += alen;
    int total = (int)(p - buf);
    memcpy(buf, &total, 4);
    return total;
}

/* BEGIN/END: nlmsghdr(type=0x10/0x11) + nfgenmsg(res=htons(10)) */
static int build_batch_frame(char *buf, uint16_t type, uint16_t flags,
                             uint32_t seq)
{
    return build_msg(buf, type, flags, seq, 0, NFNL_SUBSYS_NFTABLES, NULL, 0);
}

/* ---------------- netlink session (same as b16.c) ---------------- */

static int nl_fd = -1;

static void nl_open(void)
{
    nl_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER);
    if (nl_fd < 0) { perror("socket"); exit(1); }
    struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
    if (bind(nl_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
        perror("bind"); exit(1);
    }
    int fl = fcntl(nl_fd, F_GETFL, 0);
    fcntl(nl_fd, F_SETFL, fl | O_NONBLOCK);
}

/* receive acks (NLMSG_ERROR), collect seq->errno; wait at most wait_ms */
static int collect_acks(uint32_t *wanted, int nwanted, int *errs,
                        int wait_ms)
{
    int got = 0;
    struct timeval tv = { .tv_sec = wait_ms / 1000,
                          .tv_usec = (wait_ms % 1000) * 1000 };
    setsockopt(nl_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    int deadline = (int)time(NULL) + wait_ms / 1000 + 1;
    while (got < nwanted && time(NULL) < deadline) {
        char buf[65536];
        int n = recv(nl_fd, buf, sizeof(buf), 0);
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) continue;
            break;
        }
        for (int off = 0; off + 16 <= n;) {
            struct nlmsghdr *h = (struct nlmsghdr *)(buf + off);
            int len = h->nlmsg_len;
            if (len < 16 || off + len > n) break;
            if (h->nlmsg_type == NLMSG_ERROR && len >= 20) {
                struct nlmsgerr *e = (struct nlmsgerr *)(h + 1);
                for (int i = 0; i < nwanted; i++) {
                    if (wanted[i] == h->nlmsg_seq && errs[i] == 0x7fffffff) {
                        errs[i] = e->error;
                        got++;
                        break;
                    }
                }
            }
            off += NLMSG_ALIGN(len);
        }
    }
    return got;
}

static int E(int v) { return v == 0x7fffffff ? -1 : v; }

/* send batch (BEGIN + n bodies + END), collect body acks into errs[] */
static void send_and_collect(char msgs[][512], int *lens, uint32_t *seqs,
                             int n, uint32_t begin_seq, uint32_t end_seq,
                             int *errs, int wait_ms)
{
    char blob[4096], *p = blob;
    int b = build_batch_frame(p, NFNL_MSG_BATCH_BEGIN, NLM_F_REQUEST,
                              begin_seq);
    p += b;
    for (int i = 0; i < n; i++) { memcpy(p, msgs[i], lens[i]); p += lens[i]; }
    int e = build_batch_frame(p, NFNL_MSG_BATCH_END, NLM_F_REQUEST, end_seq);
    p += e;
    send(nl_fd, blob, (int)(p - blob), 0);
    for (int i = 0; i < n; i++) errs[i] = 0x7fffffff;
    collect_acks(seqs, n, errs, wait_ms);
}

/* DELTABLE (inside a batch); deleting a nonexistent table -ENOENT is
harmless; used to clean leftovers */
static void del_table(uint8_t family, const char *tbl)
{
    char blob[1024], *p = blob;
    int b = build_batch_frame(p, NFNL_MSG_BATCH_BEGIN, NLM_F_REQUEST, 900);
    p += b;
    char attrs[64], *q = attrs;
    put_attr_str(&q, NFTA_TABLE_NAME, tbl);
    int len = build_msg(p, NFT_MSG_TYPE(NFNL_SUBSYS_NFTABLES, NFT_MSG_DELTABLE),
                        NLM_F_REQUEST | NLM_F_ACK, 901, family, 0, attrs,
                        (int)(q - attrs));
    p += len;
    int e = build_batch_frame(p, NFNL_MSG_BATCH_END, NLM_F_REQUEST, 902);
    p += e;
    send(nl_fd, blob, (int)(p - blob), 0);
    uint32_t w = 901;
    int er = 0x7fffffff;
    collect_acks(&w, 1, &er, 500);
}

/* ---------------- scenario construction ---------------- */

/* best-effort shell command; ignore the result (same pattern as b25.c/n18.c) */
static void syscmd(const char *cmd)
{
    int rc = system(cmd);
    (void)rc;
}

/* prefix hook needs real devices (same as Python setup()/teardown()) */
static void setup_veth(void)
{
    syscmd("ip link del vbug0 2>/dev/null");
    syscmd("ip link add vbug0 type veth peer name vbug1");
    syscmd("ip link set vbug0 up");
    syscmd("ip link set vbug1 up");
}

static void teardown_veth(void)
{
    syscmd("ip link del vbug0 2>/dev/null");
}

/* NEWTABLE: NAME(1) (netdev family) */
static void mk_table(char *buf, int *len, uint32_t seq)
{
    char attrs[64], *p = attrs;
    put_attr_str(&p, NFTA_TABLE_NAME, "t");
    *len = build_msg(buf, NFT_MSG_TYPE(NFNL_SUBSYS_NFTABLES, NFT_MSG_NEWTABLE),
                     NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE, seq, 5, 0,
                     attrs, (int)(p - attrs));
}

/* NEWCHAIN: TABLE(1) NAME(3)="dev" TYPE(7)="filter" POLICY(5)=1
 * HOOK(4){ HOOKNUM(1)=0 (netdev ingress) PRIORITY(2)=0
 *         DEVS(4){ PREFIX(2)="vbug" } } (prefix hook, no wildcard) */
static void mk_chain_new(char *buf, int *len, uint32_t seq)
{
    char attrs[256], *p = attrs;
    put_attr_str(&p, NFTA_CHAIN_TABLE, "t");
    put_attr_str(&p, NFTA_CHAIN_NAME, "dev");
    put_attr_str(&p, NFTA_CHAIN_TYPE, "filter");
    put_attr_be32(&p, NFTA_CHAIN_POLICY, 1);
    {
        char hook[64], *h = hook;
        put_attr_be32(&h, NFTA_HOOK_HOOKNUM, 0);
        put_attr_be32(&h, NFTA_HOOK_PRIORITY, 0);
        {
            char devs[32], *d = devs;
            put_attr_str(&d, NFTA_DEV_PREFIX, "vbug");
            put_attr(&h, NFTA_HOOK_DEVS, devs, (int)(d - devs));
        }
        put_attr(&p, NFTA_CHAIN_HOOK, hook, (int)(h - hook));
    }
    *len = build_msg(buf, NFT_MSG_TYPE(NFNL_SUBSYS_NFTABLES, NFT_MSG_NEWCHAIN),
                     NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE, seq, 5, 0,
                     attrs, (int)(p - attrs));
}

/* DELCHAIN: TABLE(1) NAME(3)="dev" HOOK(4){ DEV(3)=exact device name } */
static void mk_chain_del(char *buf, int *len, uint32_t seq, const char *dev)
{
    char attrs[256], *p = attrs;
    put_attr_str(&p, NFTA_CHAIN_TABLE, "t");
    put_attr_str(&p, NFTA_CHAIN_NAME, "dev");
    {
        char hook[64], *h = hook;
        put_attr_str(&h, NFTA_HOOK_DEV, dev);
        put_attr(&p, NFTA_CHAIN_HOOK, hook, (int)(h - hook));
    }
    *len = build_msg(buf, NFT_MSG_TYPE(NFNL_SUBSYS_NFTABLES, NFT_MSG_DELCHAIN),
                     NLM_F_REQUEST | NLM_F_ACK, seq, 5, 0, attrs,
                     (int)(p - attrs));
}

/* ---------------- main ---------------- */

int main(void)
{
    char msgs[3][512];
    int lens[3];
    uint32_t seqs[3];
    int errs[3];

    nl_open();
    setup_veth();
    del_table(5, "t");              /* clean leftovers from the last run */

    /* control batch: del "zzz-notmatching" -> chain delete must be -ENOENT */
    seqs[0] = 11; seqs[1] = 12; seqs[2] = 13;
    mk_table(msgs[0], &lens[0], seqs[0]);
    mk_chain_new(msgs[1], &lens[1], seqs[1]);
    mk_chain_del(msgs[2], &lens[2], seqs[2], "zzz-notmatching");
    send_and_collect(msgs, lens, seqs, 3, 10, 14, errs, 1500);
    int ctrl_del = E(errs[2]);
    printf("control (del zzz) ack errno: [%d, %d, %d] (chain delete
should be -ENOENT)\n",
           E(errs[0]), E(errs[1]), ctrl_del);
    del_table(5, "t");

    /* variant batch: del "vbug0" (exact name matching the prefix) -> must
     * also be -ENOENT; bug: 0 (whole chain removed) */
    seqs[0] = 101; seqs[1] = 102; seqs[2] = 103;
    mk_table(msgs[0], &lens[0], seqs[0]);
    mk_chain_new(msgs[1], &lens[1], seqs[1]);
    mk_chain_del(msgs[2], &lens[2], seqs[2], "vbug0");
    send_and_collect(msgs, lens, seqs, 3, 100, 104, errs, 1500);
    int bug_del = E(errs[2]);
    printf("variant (del vbug0) ack errno: [%d, %d, %d]\n",
           E(errs[0]), E(errs[1]), bug_del);
    del_table(5, "t");
    teardown_veth();

    if (bug_del == 0)
        printf("REPRODUCED: exact-name delete matches the prefix hook and "
               "removes the chain (control same op %d)\n", ctrl_del);
    else if (bug_del == -2)
        printf("NOT_REPRODUCED: exact-name delete correctly rejected "
               "(-ENOENT)\n");
    else
        printf("UNAVAILABLE: unexpected observation (del=%d)\n", bug_del);

    close(nl_fd);
    return 0;
}
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help