syzbot
WARNING in xt_dnat_target_v2
Status:
upstream: reported C repro on 2026/11/05 21:17
Subsystems:
netfilter
Labels:
prio:high
[Documentation on labels]
Reported-by: syzbot+2e819f6641708bb6ab9d@syzkaller.appspotmail.com
First crash: 4d14h, last: 2d03h
✨ AI-generated root cause summary:
When an incoming TCP packet is rejected by an nftables reject rule (nft_reject_ipv4_eval), nf_send_reset() creates a TCP RST reply and attaches the original packet's conntrack entry via nf_ct_attach(), marking its ctinfo as IP_CT_RELATED_REPLY. As the RST packet traverses the NF_INET_LOCAL_OUT hook, nf_nat_ipv4_fn() passes it to nf_nat_inet_fn() because it is not an ICMP packet. In nf_nat_inet_fn(), IP_CT_RELATED_REPLY falls through to evaluation of uninitialized NAT mappings, executing rules in the LOCAL_OUT NAT chain. If the chain includes an xtables DNAT target (evaluated via nft_compat), xt_dnat_target_v2() triggers WARN_ON because its assertion checks only for IP_CT_NEW or IP_CT_RELATED, omitting IP_CT_RELATED_REPLY.
▶
▼
✨ Full AI root cause analysis
Root Cause Analysis
Involved Functions, Structs, and Files
- Files:
- Functions:
- Types and Fields:
enum ip_conntrack_info (IP_CT_NEW, IP_CT_RELATED, IP_CT_RELATED_REPLY)
struct nf_conn
Triggering Mechanism and Invariant Violation
-
Triggering Packet Rejection:
An incoming IPv4 TCP packet (oldskb) arrives in the original direction (IP_CT_DIR_ORIGINAL) with an associated connection tracking entry (ctinfo == IP_CT_NEW). If the packet is matched by an nftables filter rule before PREROUTING NAT completes destination NAT initialization (or if no PREROUTING NAT table/hook is registered), IPS_DST_NAT_DONE is not set on the conntrack entry. The rule evaluates nft_reject_ipv4_eval(), which calls nf_send_reset().
-
Attachment of Related Conntrack Info:
nf_send_reset() synthesizes a TCP reset packet (nskb) and attaches the conntrack entry of the rejected packet by calling nf_ct_attach(nskb, oldskb). This invokes nf_conntrack_attach(), which inspects the original packet's direction:
ct = nf_ct_get(skb, &ctinfo);
if (CTINFO2DIR(ctinfo) == IP_CT_DIR_ORIGINAL)
ctinfo = IP_CT_RELATED_REPLY;
else
ctinfo = IP_CT_RELATED;
nf_ct_set(nskb, ct, ctinfo);
nf_conntrack_get(skb_nfct(nskb));
Because oldskb was in IP_CT_DIR_ORIGINAL, the newly generated reset packet nskb is tagged with ctinfo = IP_CT_RELATED_REPLY.
-
Traversal of Local Output Hooks:
nf_send_reset() then transmits the packet via ip_local_out(), entering the NF_INET_LOCAL_OUT netfilter hook.
The hook calls nf_nat_ipv4_local_fn() $\rightarrow$ nf_nat_ipv4_fn().
In nf_nat_ipv4_fn():
if (ctinfo == IP_CT_RELATED || ctinfo == IP_CT_RELATED_REPLY) {
if (ip_hdr(skb)->protocol == IPPROTO_ICMP) {
if (!nf_nat_icmp_reply_translation(skb, ct, ctinfo, state->hook))
return NF_DROP;
else
return NF_ACCEPT;
}
}
return nf_nat_inet_fn(priv, skb, state);
Because nskb is a TCP packet (IPPROTO_TCP) and not an ICMP packet, it bypasses ICMP translation and enters nf_nat_inet_fn().
-
NAT Chain Evaluation:
In nf_nat_inet_fn():
- The hook is
NF_INET_LOCAL_OUT, so maniptype is NF_NAT_MANIP_DST.
- For
ctinfo == IP_CT_RELATED_REPLY, the switch statement falls through to case IP_CT_NEW:
switch (ctinfo) {
case IP_CT_RELATED:
case IP_CT_RELATED_REPLY:
/* Only ICMPs can be IP_CT_IS_REPLY. Fallthrough */
case IP_CT_NEW:
if (!nf_nat_initialized(ct, maniptype)) {
...
for (i = 0; i < e->num_hook_entries; i++) {
ret = e->hooks[i].hook(e->hooks[i].priv, skb, state);
- Because
ct has not yet had NF_NAT_MANIP_DST initialized (!nf_nat_initialized(ct, NF_NAT_MANIP_DST) is true), nf_nat_inet_fn() executes the rules on the registered LOCAL_OUT NAT chain (nft_nat_do_chain).
-
Inconsistent State Assertion in xt_dnat_target:
The chain matches and evaluates an xtables DNAT target via nft_compat (nft_target_eval_xt()), invoking xt_dnat_target_v2().
Inside xt_dnat_target_v2():
ct = nf_ct_get(skb, &ctinfo);
WARN_ON(!(ct != NULL &&
(ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED)));
While xt_snat_target_v0/v1/v2() explicitly permit ctinfo == IP_CT_RELATED_REPLY, the DNAT targets (xt_dnat_target_v0/v1/v2()) only expect IP_CT_NEW or IP_CT_RELATED. Because ctinfo is IP_CT_RELATED_REPLY, the assertion check fails, producing the reported warning:
!(ct != NULL && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED))
WARNING: net/netfilter/xt_nat.c:154 at xt_dnat_target_v2+0x97/0xc0
▶
▼
Last patch testing requests (3)
Sample crash report:
* Struck through repros no longer work on HEAD.