How an improper verdict handling bug in net/netfilter/nf_tables_api.c caused double reference drops on network packets, enabling container root breakouts.
Think of a ticket inspector on a train. A passenger hands over a ticket. The first inspector punches the ticket and marks it as used. Then, because the passenger sneezes, an unhandled rule causes a second inspector to grab the exact same ticket and punch it again. In Linux networking, dropping a packet twice freed its memory twice, allowing malicious code to masquerade as the train conductor.
An unprivileged user creates an isolated network namespace via unshare(CLONE_NEWUSER | CLONE_NEWNET).
The attacker loads a custom nftables rule returning a negative verdict code mismanaged by the hook pipeline.
The packet processing engine drops the sk_buff, but the error return path drops the exact same packet a second time.
The attacker sprays fake Page Middle Directory (PMD) entries into the freed memory, achieving full arbitrary physical memory write.
Delivered in clean, readable high-level source code (no raw assembly or binary diffs).
// VULNERABLE: net/netfilter/nf_tables_api.c before patch
static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, ...) {
// ROOT CAUSE:
// If verdict evaluation returns an invalid negative integer,
// the evaluation loop treats it as both NF_DROP and an internal error!
if (verdict < 0) {
kfree_skb(skb); // First free of socket buffer!
return verdict; // Calling hook also invokes kfree_skb(), triggering DOUBLE FREE!
}
return 0;
}
// SECURE: net/netfilter/nf_tables_api.c patch
static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, ...) {
// 1. Strict verdict assertion
if (verdict < 0) {
// Clear ownership flags to guarantee only one caller deallocates skb
nf_tables_rule_destroy(rule);
// Let the upper caller handle the single, orderly drop:
return -EINVAL;
}
return 0;
}