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

Wei Fang <[email protected]>
Newsgroups org.kernel.vger.netdev,org.kernel.vger.netfilter-devel
Message-ID <CANE+tVrDeNCHQVmsqkV2ozeBqyE3GtRDMhZgsg1bhw10yGNTRQ@mail.gmail.com>
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;
}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.