AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/07/13 21:56 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "VLAN_8021Q",
    "INET",
    "IPV6"
  ],
  "FocusSymbols": [
    "bpf_fib_vlan_input_dev",
    "bpf_fib_set_fwd_params",
    "bpf_fib_lookup_flags_ok",
    "bpf_xdp_fib_lookup",
    "bpf_skb_fib_lookup",
    "bpf_ipv4_fib_lookup",
    "bpf_ipv6_fib_lookup"
  ],
  "Reasoning": "The patch adds support for VLANs in the BPF FIB lookup helpers (`bpf_xdp_fib_lookup` and `bpf_skb_fib_lookup`). It introduces new flags `BPF_FIB_LOOKUP_VLAN` and `BPF_FIB_LOOKUP_VLAN_INPUT` to handle VLAN egress and ingress resolution. This modifies core networking and BPF logic, which is reachable from BPF programs and can be fuzzed by syzkaller.",
  "WorthFuzzing": true
}

1/1 2026/07/13 21:56 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 4a7f1fa08dce1e1c25400be0f3cf11db30c9dc1b\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Mon Jul 13 21:56:06 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h\nindex 89b36de5fdbb6d..ff03019abe3554 100644\n--- a/include/uapi/linux/bpf.h\n+++ b/include/uapi/linux/bpf.h\n@@ -3532,6 +3532,47 @@ union bpf_attr {\n  *\t\t\tUse the mark present in *params*-\u003emark for the fib lookup.\n  *\t\t\tThis option should not be used with BPF_FIB_LOOKUP_DIRECT,\n  *\t\t\tas it only has meaning for full lookups.\n+ *\t\t**BPF_FIB_LOOKUP_VLAN**\n+ *\t\t\tIf the fib lookup resolves to a VLAN device whose\n+ *\t\t\tparent is a real (non-VLAN) device, set\n+ *\t\t\t*params*-\u003eh_vlan_proto and *params*-\u003eh_vlan_TCI from\n+ *\t\t\tthe VLAN device and replace *params*-\u003eifindex with the\n+ *\t\t\tparent's ifindex. *params*-\u003eh_vlan_TCI carries the VID\n+ *\t\t\tonly, with PCP and DEI bits zero; a consumer wanting to\n+ *\t\t\tset egress priority writes PCP itself. *params*-\u003esmac is\n+ *\t\t\tthe VLAN device's own address, which can differ from the\n+ *\t\t\tparent's. Only the immediate parent is resolved; if it\n+ *\t\t\tis itself a VLAN device (QinQ) or in another namespace,\n+ *\t\t\tthe egress cannot be reduced to a physical device plus\n+ *\t\t\tone tag and the lookup returns\n+ *\t\t\t**BPF_FIB_LKUP_RET_VLAN_FAILURE** with *params*-\u003eifindex\n+ *\t\t\tleft at the input. To obtain the VLAN device's own\n+ *\t\t\tifindex, repeat the lookup without\n+ *\t\t\t**BPF_FIB_LOOKUP_VLAN**, re-initializing *params*\n+ *\t\t\tfirst: output fields overwrite the inputs they share\n+ *\t\t\tstorage with. The swap and the vlan fields\n+ *\t\t\tare written only on success; other output fields keep\n+ *\t\t\tthe helper's existing behaviour, so a frag-needed result\n+ *\t\t\tstill reports the route mtu in *params*-\u003emtu_result.\n+ *\t\t\tThis flag is only valid for XDP programs; tc programs\n+ *\t\t\treceive -EINVAL since they can redirect to the VLAN\n+ *\t\t\tdevice directly.\n+ *\t\t**BPF_FIB_LOOKUP_VLAN_INPUT**\n+ *\t\t\tTreat *params*-\u003eh_vlan_proto and *params*-\u003eh_vlan_TCI\n+ *\t\t\tas an input VLAN tag and run the lookup as if ingress\n+ *\t\t\thad happened on the VLAN subinterface carrying that tag\n+ *\t\t\ton *params*-\u003eifindex. The VID is the low 12 bits of\n+ *\t\t\t*params*-\u003eh_vlan_TCI; *params*-\u003eh_vlan_proto must be\n+ *\t\t\tETH_P_8021Q or ETH_P_8021AD in network byte order, else\n+ *\t\t\t**-EINVAL**. If *params*-\u003eifindex is itself a VLAN\n+ *\t\t\tdevice, its inner (QinQ) subinterface is matched; for a\n+ *\t\t\tbond or team, pass the master's ifindex. An unmatched\n+ *\t\t\ttag, a down device, or one in another namespace returns\n+ *\t\t\t**BPF_FIB_LKUP_RET_NOT_FWDED**, mirroring real ingress.\n+ *\t\t\tA VID of 0 is looked up literally, so do not set this\n+ *\t\t\tflag for priority-tagged frames. Cannot be combined with\n+ *\t\t\t**BPF_FIB_LOOKUP_TBID** or **BPF_FIB_LOOKUP_OUTPUT**\n+ *\t\t\t(returns **-EINVAL**).\n  *\n  *\t\t*ctx* is either **struct xdp_md** for XDP programs or\n  *\t\t**struct sk_buff** tc cls_act programs.\n@@ -7327,6 +7368,8 @@ enum {\n \tBPF_FIB_LOOKUP_TBID    = (1U \u003c\u003c 3),\n \tBPF_FIB_LOOKUP_SRC     = (1U \u003c\u003c 4),\n \tBPF_FIB_LOOKUP_MARK    = (1U \u003c\u003c 5),\n+\tBPF_FIB_LOOKUP_VLAN    = (1U \u003c\u003c 6),\n+\tBPF_FIB_LOOKUP_VLAN_INPUT = (1U \u003c\u003c 7),\n };\n \n enum {\n@@ -7340,6 +7383,7 @@ enum {\n \tBPF_FIB_LKUP_RET_NO_NEIGH,     /* no neighbor entry for nh */\n \tBPF_FIB_LKUP_RET_FRAG_NEEDED,  /* fragmentation required to fwd */\n \tBPF_FIB_LKUP_RET_NO_SRC_ADDR,  /* failed to derive IP src addr */\n+\tBPF_FIB_LKUP_RET_VLAN_FAILURE, /* VLAN egress, parent unresolvable */\n };\n \n struct bpf_fib_lookup {\n@@ -7393,7 +7437,13 @@ struct bpf_fib_lookup {\n \n \tunion {\n \t\tstruct {\n-\t\t\t/* output */\n+\t\t\t/*\n+\t\t\t * output with BPF_FIB_LOOKUP_VLAN: set from the\n+\t\t\t * resolved egress VLAN device (see the flag); zeroed\n+\t\t\t * on other successful lookups. input with\n+\t\t\t * BPF_FIB_LOOKUP_VLAN_INPUT: the VLAN tag to scope\n+\t\t\t * the lookup by.\n+\t\t\t */\n \t\t\t__be16\th_vlan_proto;\n \t\t\t__be16\th_vlan_TCI;\n \t\t};\ndiff --git a/net/core/filter.c b/net/core/filter.c\nindex 2e96b4b847ce12..0ea362fa4287e3 100644\n--- a/net/core/filter.c\n+++ b/net/core/filter.c\n@@ -6201,21 +6201,60 @@ static const struct bpf_func_proto bpf_skb_get_xfrm_state_proto = {\n #endif\n \n #if IS_ENABLED(CONFIG_INET) || IS_ENABLED(CONFIG_IPV6)\n-static int bpf_fib_set_fwd_params(struct bpf_fib_lookup *params, u32 mtu)\n+static int bpf_fib_set_fwd_params(struct net_device *dev,\n+\t\t\t\t  struct bpf_fib_lookup *params,\n+\t\t\t\t  u32 flags, u32 mtu, u32 in_ifindex)\n {\n \tparams-\u003eh_vlan_TCI = 0;\n \tparams-\u003eh_vlan_proto = 0;\n+\n+#if IS_ENABLED(CONFIG_VLAN_8021Q)\n+\tif ((flags \u0026 BPF_FIB_LOOKUP_VLAN) \u0026\u0026 is_vlan_dev(dev)) {\n+\t\tstruct net_device *real_dev = vlan_dev_priv(dev)-\u003ereal_dev;\n+\n+\t\tif (!is_vlan_dev(real_dev) \u0026\u0026\n+\t\t    net_eq(dev_net(real_dev), dev_net(dev))) {\n+\t\t\tparams-\u003eh_vlan_proto = vlan_dev_vlan_proto(dev);\n+\t\t\tparams-\u003eh_vlan_TCI = htons(vlan_dev_vlan_id(dev));\n+\t\t\tparams-\u003eifindex = real_dev-\u003eifindex;\n+\t\t} else {\n+\t\t\tparams-\u003eifindex = in_ifindex;\n+\t\t\treturn BPF_FIB_LKUP_RET_VLAN_FAILURE;\n+\t\t}\n+\t}\n+#endif\n+\n \tif (mtu)\n \t\tparams-\u003emtu_result = mtu; /* union with tot_len */\n \n \treturn 0;\n }\n+\n+static struct net_device *bpf_fib_vlan_input_dev(struct net_device *dev,\n+\t\t\t\t\t\t const struct bpf_fib_lookup *params)\n+{\n+\t__be16 proto = params-\u003eh_vlan_proto;\n+\tstruct net_device *vlan_dev;\n+\tu16 vid;\n+\n+\tif (proto != htons(ETH_P_8021Q) \u0026\u0026 proto != htons(ETH_P_8021AD))\n+\t\treturn ERR_PTR(-EINVAL);\n+\n+\tvid = ntohs(params-\u003eh_vlan_TCI) \u0026 VLAN_VID_MASK;\n+\tvlan_dev = __vlan_find_dev_deep_rcu(dev, proto, vid);\n+\tif (!vlan_dev || !(vlan_dev-\u003eflags \u0026 IFF_UP) ||\n+\t    !net_eq(dev_net(vlan_dev), dev_net(dev)))\n+\t\treturn NULL;\n+\n+\treturn vlan_dev;\n+}\n #endif\n \n #if IS_ENABLED(CONFIG_INET)\n static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n \t\t\t       u32 flags, bool check_mtu)\n {\n+\tu32 in_ifindex = params-\u003eifindex;\n \tstruct neighbour *neigh = NULL;\n \tstruct fib_nh_common *nhc;\n \tstruct in_device *in_dev;\n@@ -6229,6 +6268,14 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n \tif (unlikely(!dev))\n \t\treturn -ENODEV;\n \n+\tif (flags \u0026 BPF_FIB_LOOKUP_VLAN_INPUT) {\n+\t\tdev = bpf_fib_vlan_input_dev(dev, params);\n+\t\tif (IS_ERR(dev))\n+\t\t\treturn PTR_ERR(dev);\n+\t\tif (!dev)\n+\t\t\treturn BPF_FIB_LKUP_RET_NOT_FWDED;\n+\t}\n+\n \t/* verify forwarding is enabled on this interface */\n \tin_dev = __in_dev_get_rcu(dev);\n \tif (unlikely(!in_dev || !IN_DEV_FORWARD(in_dev)))\n@@ -6238,7 +6285,11 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n \t\tfl4.flowi4_iif = 1;\n \t\tfl4.flowi4_oif = params-\u003eifindex;\n \t} else {\n-\t\tfl4.flowi4_iif = params-\u003eifindex;\n+\t\t/*\n+\t\t * dev-\u003eifindex, not params-\u003eifindex: VLAN_INPUT may have\n+\t\t * resolved dev to a subinterface above.\n+\t\t */\n+\t\tfl4.flowi4_iif = dev-\u003eifindex;\n \t\tfl4.flowi4_oif = 0;\n \t}\n \tfl4.flowi4_dscp = inet_dsfield_to_dscp(params-\u003etos);\n@@ -6347,7 +6398,7 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n \tmemcpy(params-\u003esmac, dev-\u003edev_addr, ETH_ALEN);\n \n set_fwd_params:\n-\treturn bpf_fib_set_fwd_params(params, mtu);\n+\treturn bpf_fib_set_fwd_params(dev, params, flags, mtu, in_ifindex);\n }\n #endif\n \n@@ -6357,6 +6408,7 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n {\n \tstruct in6_addr *src = (struct in6_addr *) params-\u003eipv6_src;\n \tstruct in6_addr *dst = (struct in6_addr *) params-\u003eipv6_dst;\n+\tu32 in_ifindex = params-\u003eifindex;\n \tstruct fib6_result res = {};\n \tstruct neighbour *neigh;\n \tstruct net_device *dev;\n@@ -6374,6 +6426,14 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n \tif (unlikely(!dev))\n \t\treturn -ENODEV;\n \n+\tif (flags \u0026 BPF_FIB_LOOKUP_VLAN_INPUT) {\n+\t\tdev = bpf_fib_vlan_input_dev(dev, params);\n+\t\tif (IS_ERR(dev))\n+\t\t\treturn PTR_ERR(dev);\n+\t\tif (!dev)\n+\t\t\treturn BPF_FIB_LKUP_RET_NOT_FWDED;\n+\t}\n+\n \tidev = __in6_dev_get_safely(dev);\n \tif (unlikely(!idev || !READ_ONCE(idev-\u003ecnf.forwarding)))\n \t\treturn BPF_FIB_LKUP_RET_FWD_DISABLED;\n@@ -6382,7 +6442,12 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n \t\tfl6.flowi6_iif = 1;\n \t\toif = fl6.flowi6_oif = params-\u003eifindex;\n \t} else {\n-\t\toif = fl6.flowi6_iif = params-\u003eifindex;\n+\t\t/*\n+\t\t * dev-\u003eifindex, not params-\u003eifindex: VLAN_INPUT may have\n+\t\t * resolved dev to a subinterface above.\n+\t\t */\n+\t\toif = dev-\u003eifindex;\n+\t\tfl6.flowi6_iif = oif;\n \t\tfl6.flowi6_oif = 0;\n \t\tstrict = RT6_LOOKUP_F_HAS_SADDR;\n \t}\n@@ -6486,13 +6551,26 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n \tmemcpy(params-\u003esmac, dev-\u003edev_addr, ETH_ALEN);\n \n set_fwd_params:\n-\treturn bpf_fib_set_fwd_params(params, mtu);\n+\treturn bpf_fib_set_fwd_params(dev, params, flags, mtu, in_ifindex);\n }\n #endif\n \n #define BPF_FIB_LOOKUP_MASK (BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_OUTPUT | \\\n \t\t\t     BPF_FIB_LOOKUP_SKIP_NEIGH | BPF_FIB_LOOKUP_TBID | \\\n-\t\t\t     BPF_FIB_LOOKUP_SRC | BPF_FIB_LOOKUP_MARK)\n+\t\t\t     BPF_FIB_LOOKUP_SRC | BPF_FIB_LOOKUP_MARK | \\\n+\t\t\t     BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_VLAN_INPUT)\n+\n+static bool bpf_fib_lookup_flags_ok(u32 flags)\n+{\n+\tif (flags \u0026 ~BPF_FIB_LOOKUP_MASK)\n+\t\treturn false;\n+\n+\tif ((flags \u0026 BPF_FIB_LOOKUP_VLAN_INPUT) \u0026\u0026\n+\t    (flags \u0026 (BPF_FIB_LOOKUP_TBID | BPF_FIB_LOOKUP_OUTPUT)))\n+\t\treturn false;\n+\n+\treturn true;\n+}\n \n BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx,\n \t   struct bpf_fib_lookup *, params, int, plen, u32, flags)\n@@ -6500,7 +6578,7 @@ BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx,\n \tif (plen \u003c sizeof(*params))\n \t\treturn -EINVAL;\n \n-\tif (flags \u0026 ~BPF_FIB_LOOKUP_MASK)\n+\tif (!bpf_fib_lookup_flags_ok(flags))\n \t\treturn -EINVAL;\n \n \tswitch (params-\u003efamily) {\n@@ -6538,7 +6616,10 @@ BPF_CALL_4(bpf_skb_fib_lookup, struct sk_buff *, skb,\n \tif (plen \u003c sizeof(*params))\n \t\treturn -EINVAL;\n \n-\tif (flags \u0026 ~BPF_FIB_LOOKUP_MASK)\n+\tif (!bpf_fib_lookup_flags_ok(flags))\n+\t\treturn -EINVAL;\n+\n+\tif (flags \u0026 BPF_FIB_LOOKUP_VLAN)\n \t\treturn -EINVAL;\n \n \tif (params-\u003etot_len)\ndiff --git a/tools/include/uapi/linux/bpf.h b/tools/include/uapi/linux/bpf.h\nindex 89b36de5fdbb6d..ff03019abe3554 100644\n--- a/tools/include/uapi/linux/bpf.h\n+++ b/tools/include/uapi/linux/bpf.h\n@@ -3532,6 +3532,47 @@ union bpf_attr {\n  *\t\t\tUse the mark present in *params*-\u003emark for the fib lookup.\n  *\t\t\tThis option should not be used with BPF_FIB_LOOKUP_DIRECT,\n  *\t\t\tas it only has meaning for full lookups.\n+ *\t\t**BPF_FIB_LOOKUP_VLAN**\n+ *\t\t\tIf the fib lookup resolves to a VLAN device whose\n+ *\t\t\tparent is a real (non-VLAN) device, set\n+ *\t\t\t*params*-\u003eh_vlan_proto and *params*-\u003eh_vlan_TCI from\n+ *\t\t\tthe VLAN device and replace *params*-\u003eifindex with the\n+ *\t\t\tparent's ifindex. *params*-\u003eh_vlan_TCI carries the VID\n+ *\t\t\tonly, with PCP and DEI bits zero; a consumer wanting to\n+ *\t\t\tset egress priority writes PCP itself. *params*-\u003esmac is\n+ *\t\t\tthe VLAN device's own address, which can differ from the\n+ *\t\t\tparent's. Only the immediate parent is resolved; if it\n+ *\t\t\tis itself a VLAN device (QinQ) or in another namespace,\n+ *\t\t\tthe egress cannot be reduced to a physical device plus\n+ *\t\t\tone tag and the lookup returns\n+ *\t\t\t**BPF_FIB_LKUP_RET_VLAN_FAILURE** with *params*-\u003eifindex\n+ *\t\t\tleft at the input. To obtain the VLAN device's own\n+ *\t\t\tifindex, repeat the lookup without\n+ *\t\t\t**BPF_FIB_LOOKUP_VLAN**, re-initializing *params*\n+ *\t\t\tfirst: output fields overwrite the inputs they share\n+ *\t\t\tstorage with. The swap and the vlan fields\n+ *\t\t\tare written only on success; other output fields keep\n+ *\t\t\tthe helper's existing behaviour, so a frag-needed result\n+ *\t\t\tstill reports the route mtu in *params*-\u003emtu_result.\n+ *\t\t\tThis flag is only valid for XDP programs; tc programs\n+ *\t\t\treceive -EINVAL since they can redirect to the VLAN\n+ *\t\t\tdevice directly.\n+ *\t\t**BPF_FIB_LOOKUP_VLAN_INPUT**\n+ *\t\t\tTreat *params*-\u003eh_vlan_proto and *params*-\u003eh_vlan_TCI\n+ *\t\t\tas an input VLAN tag and run the lookup as if ingress\n+ *\t\t\thad happened on the VLAN subinterface carrying that tag\n+ *\t\t\ton *params*-\u003eifindex. The VID is the low 12 bits of\n+ *\t\t\t*params*-\u003eh_vlan_TCI; *params*-\u003eh_vlan_proto must be\n+ *\t\t\tETH_P_8021Q or ETH_P_8021AD in network byte order, else\n+ *\t\t\t**-EINVAL**. If *params*-\u003eifindex is itself a VLAN\n+ *\t\t\tdevice, its inner (QinQ) subinterface is matched; for a\n+ *\t\t\tbond or team, pass the master's ifindex. An unmatched\n+ *\t\t\ttag, a down device, or one in another namespace returns\n+ *\t\t\t**BPF_FIB_LKUP_RET_NOT_FWDED**, mirroring real ingress.\n+ *\t\t\tA VID of 0 is looked up literally, so do not set this\n+ *\t\t\tflag for priority-tagged frames. Cannot be combined with\n+ *\t\t\t**BPF_FIB_LOOKUP_TBID** or **BPF_FIB_LOOKUP_OUTPUT**\n+ *\t\t\t(returns **-EINVAL**).\n  *\n  *\t\t*ctx* is either **struct xdp_md** for XDP programs or\n  *\t\t**struct sk_buff** tc cls_act programs.\n@@ -7327,6 +7368,8 @@ enum {\n \tBPF_FIB_LOOKUP_TBID    = (1U \u003c\u003c 3),\n \tBPF_FIB_LOOKUP_SRC     = (1U \u003c\u003c 4),\n \tBPF_FIB_LOOKUP_MARK    = (1U \u003c\u003c 5),\n+\tBPF_FIB_LOOKUP_VLAN    = (1U \u003c\u003c 6),\n+\tBPF_FIB_LOOKUP_VLAN_INPUT = (1U \u003c\u003c 7),\n };\n \n enum {\n@@ -7340,6 +7383,7 @@ enum {\n \tBPF_FIB_LKUP_RET_NO_NEIGH,     /* no neighbor entry for nh */\n \tBPF_FIB_LKUP_RET_FRAG_NEEDED,  /* fragmentation required to fwd */\n \tBPF_FIB_LKUP_RET_NO_SRC_ADDR,  /* failed to derive IP src addr */\n+\tBPF_FIB_LKUP_RET_VLAN_FAILURE, /* VLAN egress, parent unresolvable */\n };\n \n struct bpf_fib_lookup {\n@@ -7393,7 +7437,13 @@ struct bpf_fib_lookup {\n \n \tunion {\n \t\tstruct {\n-\t\t\t/* output */\n+\t\t\t/*\n+\t\t\t * output with BPF_FIB_LOOKUP_VLAN: set from the\n+\t\t\t * resolved egress VLAN device (see the flag); zeroed\n+\t\t\t * on other successful lookups. input with\n+\t\t\t * BPF_FIB_LOOKUP_VLAN_INPUT: the VLAN tag to scope\n+\t\t\t * the lookup by.\n+\t\t\t */\n \t\t\t__be16\th_vlan_proto;\n \t\t\t__be16\th_vlan_TCI;\n \t\t};\ndiff --git a/tools/testing/selftests/bpf/prog_tests/fib_lookup.c b/tools/testing/selftests/bpf/prog_tests/fib_lookup.c\nindex bd76589580041f..f7361f9a34592f 100644\n--- a/tools/testing/selftests/bpf/prog_tests/fib_lookup.c\n+++ b/tools/testing/selftests/bpf/prog_tests/fib_lookup.c\n@@ -2,6 +2,7 @@\n /* Copyright (c) 2023 Meta Platforms, Inc. and affiliates. */\n \n #include \u003clinux/rtnetlink.h\u003e\n+#include \u003clinux/if_ether.h\u003e\n #include \u003csys/types.h\u003e\n #include \u003cnet/if.h\u003e\n \n@@ -23,6 +24,7 @@\n #define IPV4_TBID_ADDR\t\t\"172.0.0.254\"\n #define IPV4_TBID_NET\t\t\"172.0.0.0\"\n #define IPV4_TBID_DST\t\t\"172.0.0.2\"\n+#define IPV4_TBID_NONEIGH_DST\t\"172.0.0.5\"\n #define IPV6_TBID_ADDR\t\t\"fd00::FFFF\"\n #define IPV6_TBID_NET\t\t\"fd00::\"\n #define IPV6_TBID_DST\t\t\"fd00::2\"\n@@ -37,6 +39,41 @@\n #define IPV6_LOCAL\t\t\"fd01::3\"\n #define IPV6_GW1\t\t\"fd01::1\"\n #define IPV6_GW2\t\t\"fd01::2\"\n+#define VLAN_ID\t\t\t100\n+#define VLAN_IFACE\t\t\"veth1.100\"\n+#define VLAN_ID_DOWN\t\t102\n+#define VLAN_IFACE_DOWN\t\t\"veth1.102\"\n+#define QINQ_OUTER_IFACE\t\"veth1.200\"\n+#define QINQ_INNER_IFACE\t\"veth1.200.300\"\n+#define VLAN_TABLE\t\t\"300\"\n+#define IPV4_VLAN_IFACE_ADDR\t\"10.5.0.254\"\n+#define IPV4_VLAN_EGRESS_DST\t\"10.5.0.2\"\n+#define IPV4_QINQ_DST\t\t\"10.7.0.2\"\n+#define IPV4_VLAN_DST\t\t\"10.6.0.2\"\n+#define IPV4_VLAN_GW\t\t\"10.5.0.1\"\n+#define IPV6_VLAN_IFACE_ADDR\t\"fd02::254\"\n+#define IPV6_VLAN_EGRESS_DST\t\"fd02::2\"\n+#define IPV6_VLAN_DST\t\t\"fd03::2\"\n+#define IPV6_VLAN_GW\t\t\"fd02::1\"\n+#define VLAN_VID_UNUSED\t\t999\n+#define VRF_IFACE\t\t\"vrf-blue\"\n+#define VRF_TABLE\t\t\"1000\"\n+#define VRF_VLAN_ID\t\t101\n+#define VRF_VLAN_IFACE\t\t\"veth1.101\"\n+#define IPV4_VRF_IFACE_ADDR\t\"10.8.0.254\"\n+#define IPV4_VRF_GW\t\t\"10.8.0.1\"\n+#define IPV4_VRF_DST\t\t\"10.9.0.2\"\n+#define TBID_VLAN_ID\t\t50\n+#define TBID_VLAN_IFACE\t\t\"veth2.50\"\n+#define IPV4_TBID_VLAN_DST\t\"172.2.0.2\"\n+#define IPV4_BOND_VLAN_DST\t\"10.11.0.2\"\n+#define IPV4_VLAN_MTU_DST\t\"10.5.9.2\"\n+#define QINQ_AD_VLAN_ID\t\t200\n+#define QINQ_INNER_VLAN_ID\t300\n+#define BOND_IFACE\t\t\"bond99\"\n+#define BOND_PORT\t\t\"veth3\"\n+#define BOND_PORT_PEER\t\t\"veth4\"\n+#define BOND_VLAN_ID\t\t500\n #define DMAC\t\t\t\"11:11:11:11:11:11\"\n #define DMAC_INIT { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, }\n #define DMAC2\t\t\t\"01:01:01:01:01:01\"\n@@ -52,6 +89,17 @@ struct fib_lookup_test {\n \t__u32 tbid;\n \t__u8 dmac[6];\n \t__u32 mark;\n+\t/*\n+\t * input tag with BPF_FIB_LOOKUP_VLAN_INPUT; expected output tag\n+\t * with BPF_FIB_LOOKUP_VLAN (checked when check_vlan is set)\n+\t */\n+\t__u16 vlan_proto;\n+\t__u16 vlan_id;\n+\tbool check_vlan;\n+\tconst char *expected_dev; /* expected params-\u003eifindex after lookup */\n+\tconst char *iif;\t  /* override the default veth1 input device */\n+\t__u16 tot_len;\t\t  /* triggers the in-lookup mtu check when set */\n+\t__u16 expected_mtu;\t  /* expected mtu_result (union with tot_len) */\n };\n \n static const struct fib_lookup_test tests[] = {\n@@ -79,6 +127,17 @@ static const struct fib_lookup_test tests[] = {\n \t  .daddr = IPV4_TBID_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n \t  .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID, .tbid = 100,\n \t  .dmac = DMAC_INIT2, },\n+\t/*\n+\t * An error that returns after the egress device is resolved must\n+\t * report the egress ifindex, not the input. This routes from input\n+\t * veth1 via veth2 (table 100) to a dst with no neighbour, so\n+\t * input != egress, pinning NO_NEIGH to the egress device.\n+\t */\n+\t{ .desc = \"IPv4 NO_NEIGH reports the egress ifindex, not the input\",\n+\t  .daddr = IPV4_TBID_NONEIGH_DST,\n+\t  .expected_ret = BPF_FIB_LKUP_RET_NO_NEIGH,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID, .tbid = 100,\n+\t  .expected_dev = \"veth2\", },\n \t{ .desc = \"IPv6 TBID lookup failure\",\n \t  .daddr = IPV6_TBID_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,\n \t  .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID,\n@@ -142,6 +201,218 @@ static const struct fib_lookup_test tests[] = {\n \t  .expected_dst = IPV6_GW1,\n \t  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,\n \t  .mark = MARK, },\n+\t/* vlan egress resolution */\n+\t/*\n+\t * Invariant the VLAN-egress arms jointly enforce: a\n+\t * BPF_FIB_LOOKUP_VLAN SUCCESS always carries a physical,\n+\t * xmit-capable ifindex; no SUCCESS ever returns a VLAN-device\n+\t * ifindex. Reducible arms pin ifindex == the physical parent; the\n+\t * QinQ and foreign-netns arms pin VLAN_FAILURE with params-\u003eifindex\n+\t * left at the input, so a regression to best-effort (SUCCESS + the\n+\t * VLAN ifindex) fails one.\n+\t */\n+\t{ .desc = \"IPv4 VLAN egress, no flag\",\n+\t  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = VLAN_IFACE, .check_vlan = true, },\n+\t{ .desc = \"IPv4 VLAN egress, single VLAN\",\n+\t  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t/*\n+\t * skb path without tot_len: mtu_result is the VLAN device's mtu\n+\t * (1400), not the parent's (1500)\n+\t */\n+\t{ .desc = \"IPv4 VLAN egress, skb-path mtu is the VLAN device's without the flag\",\n+\t  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = VLAN_IFACE, .check_vlan = true, .expected_mtu = 1400, },\n+\t{ .desc = \"IPv4 VLAN egress, flag set but egress is not a VLAN\",\n+\t  .daddr = IPV4_NUD_FAILED_ADDR, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = \"veth1\", .check_vlan = true, },\n+\t{ .desc = \"IPv4 VLAN egress, QinQ not reducible (VLAN_FAILURE)\",\n+\t  .daddr = IPV4_QINQ_DST,\n+\t  .expected_ret = BPF_FIB_LKUP_RET_VLAN_FAILURE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = \"veth1\", .check_vlan = true, },\n+\t{ .desc = \"IPv4 QinQ egress without the flag (escape hatch)\",\n+\t  .daddr = IPV4_QINQ_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = QINQ_INNER_IFACE, },\n+\t{ .desc = \"IPv6 VLAN egress, single VLAN\",\n+\t  .daddr = IPV6_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN egress, neighbour on the VLAN device\",\n+\t  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, .dmac = DMAC_INIT, },\n+\t{ .desc = \"IPv4 VLAN egress in OUTPUT mode\",\n+\t  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .iif = VLAN_IFACE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_OUTPUT | BPF_FIB_LOOKUP_VLAN |\n+\t\t\t  BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN egress over a bond\",\n+\t  .daddr = IPV4_BOND_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = BOND_IFACE, .check_vlan = true,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN egress via TBID table\",\n+\t  .daddr = IPV4_TBID_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID |\n+\t\t\t  BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .tbid = 100,\n+\t  .expected_dev = \"veth2\", .check_vlan = true,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = TBID_VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN egress, success writes mtu_result with the swap\",\n+\t  .daddr = IPV4_VLAN_MTU_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .tot_len = 500, .expected_mtu = 1000,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN egress, FRAG_NEEDED reports mtu, swap unwritten\",\n+\t  .daddr = IPV4_VLAN_MTU_DST, .expected_ret = BPF_FIB_LKUP_RET_FRAG_NEEDED,\n+\t  .tot_len = 1400, .expected_mtu = 1000,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .expected_dev = \"veth1\", .check_vlan = true, },\n+\t/* vlan tag as lookup input */\n+\t{ .desc = \"IPv4 VLAN input, no flag\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV4_GW1,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH, },\n+\t{ .desc = \"IPv4 VLAN input, tag selects subinterface route\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV4_VLAN_GW, .expected_dev = VLAN_IFACE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv6 VLAN input, tag selects subinterface route\",\n+\t  .daddr = IPV6_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV6_VLAN_GW, .expected_dev = VLAN_IFACE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input and egress combined\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV4_VLAN_GW, .expected_dev = \"veth1\",\n+\t  .check_vlan = true,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_VLAN |\n+\t\t\t  BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input, neighbour resolved on the route\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV4_VLAN_GW, .expected_dev = VLAN_IFACE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, .dmac = DMAC_INIT2, },\n+\t{ .desc = \"IPv4 VLAN input, source address from the subinterface\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_src = IPV4_VLAN_IFACE_ADDR,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SRC |\n+\t\t\t  BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t/*\n+\t * VRF: the resolved subinterface is enslaved, so the l3mdev rule\n+\t * (full lookup) and l3mdev_fib_table_rcu() (DIRECT) must select\n+\t * the VRF table from the resolved ingress\n+\t */\n+\t{ .desc = \"IPv4 VLAN input, VRF subinterface, no flag\",\n+\t  .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV4_GW1,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH, },\n+\t{ .desc = \"IPv4 VLAN input, tag selects VRF table\",\n+\t  .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV4_VRF_GW, .expected_dev = VRF_VLAN_IFACE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VRF_VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input, DIRECT uses VRF table from resolved ingress\",\n+\t  .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV4_VRF_GW, .expected_dev = VRF_VLAN_IFACE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_DIRECT |\n+\t\t\t  BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VRF_VLAN_ID, },\n+\t/*\n+\t * failure arms also assert params is left untouched: ifindex still\n+\t * names the physical device and the input tag bytes survive\n+\t */\n+\t{ .desc = \"IPv4 VLAN input, invalid proto\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = 0x1234, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input, unmatched VID\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_VID_UNUSED, },\n+\t{ .desc = \"IPv4 VLAN input, subinterface down\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID_DOWN, },\n+\t/*\n+\t * the resolver runs before the forwarding check, so on devices\n+\t * with forwarding off FWD_DISABLED (not NOT_FWDED) proves the tag\n+\t * resolved to that device and the lookup used it as ingress\n+\t */\n+\t{ .desc = \"IPv4 VLAN input, 802.1ad tag\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021AD, .vlan_id = QINQ_AD_VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input, PCP and DEI bits ignored in TCI\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,\n+\t  .expected_dst = IPV4_VLAN_GW,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = 0xe000 | VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input, inner QinQ device from VLAN ifindex\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,\n+\t  .iif = QINQ_OUTER_IFACE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = QINQ_INNER_VLAN_ID, },\n+\t/*\n+\t * bonding: the VLANs live on the master, as on receive, where the\n+\t * frame is steered to the master before VLAN processing; a port\n+\t * ifindex does not match (ports carry vid state but no VLAN devs)\n+\t */\n+\t{ .desc = \"IPv4 VLAN input, tag on bond master resolves\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,\n+\t  .iif = BOND_IFACE,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input, tag on bond port does not match\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,\n+\t  .iif = BOND_PORT, .expected_dev = BOND_PORT, .check_vlan = true,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },\n+\t{ .desc = \"IPv6 VLAN input, invalid proto\",\n+\t  .daddr = IPV6_VLAN_DST, .expected_ret = -EINVAL,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = 0x1234, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input, VID 0 priority tag fails closed\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = 0, },\n+\t{ .desc = \"IPv6 VLAN input, unmatched VID\",\n+\t  .daddr = IPV6_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,\n+\t  .expected_dev = \"veth1\", .check_vlan = true,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_VID_UNUSED, },\n+\t{ .desc = \"unknown flag bit rejected\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,\n+\t  .lookup_flags = (1 \u003c\u003c 14) | BPF_FIB_LOOKUP_SKIP_NEIGH, },\n+\t{ .desc = \"IPv4 VLAN input rejected with TBID\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_TBID,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n+\t{ .desc = \"IPv4 VLAN input rejected with OUTPUT\",\n+\t  .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,\n+\t  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_OUTPUT,\n+\t  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },\n };\n \n static int setup_netns(void)\n@@ -204,6 +475,105 @@ static int setup_netns(void)\n \tSYS(fail, \"ip rule add prio 2 fwmark %d lookup %s\", MARK, MARK_TABLE);\n \tSYS(fail, \"ip -6 rule add prio 2 fwmark %d lookup %s\", MARK, MARK_TABLE);\n \n+\t/*\n+\t * Setup for vlan tests: a subinterface for egress resolution and\n+\t * tag-as-input, a QinQ stack, and an iif rule so the input tests\n+\t * observe which device the lookup used as ingress.\n+\t */\n+\tSYS(fail, \"ip link add link veth1 name %s type vlan id %d\",\n+\t    VLAN_IFACE, VLAN_ID);\n+\tSYS(fail, \"ip link set dev %s up\", VLAN_IFACE);\n+\t/*\n+\t * lower than the veth1 parent (1500): the skb-path mtu check uses the\n+\t * FIB result (VLAN) device, so mtu_result is this value, which the\n+\t * no-flag arm below pins\n+\t */\n+\tSYS(fail, \"ip link set dev %s mtu 1400\", VLAN_IFACE);\n+\tSYS(fail, \"ip addr add %s/24 dev %s\", IPV4_VLAN_IFACE_ADDR, VLAN_IFACE);\n+\tSYS(fail, \"ip addr add %s/64 dev %s nodad\", IPV6_VLAN_IFACE_ADDR, VLAN_IFACE);\n+\n+\t/*\n+\t * stays down: the input flag must treat its tag the way real\n+\t * ingress treats a frame arriving on a down VLAN device (drop)\n+\t */\n+\tSYS(fail, \"ip link add link veth1 name %s type vlan id %d\",\n+\t    VLAN_IFACE_DOWN, VLAN_ID_DOWN);\n+\n+\terr = write_sysctl(\"/proc/sys/net/ipv4/conf/\" VLAN_IFACE \"/forwarding\", \"1\");\n+\tif (!ASSERT_OK(err, \"write_sysctl(net.ipv4.conf.\" VLAN_IFACE \".forwarding)\"))\n+\t\tgoto fail;\n+\n+\terr = write_sysctl(\"/proc/sys/net/ipv6/conf/\" VLAN_IFACE \"/forwarding\", \"1\");\n+\tif (!ASSERT_OK(err, \"write_sysctl(net.ipv6.conf.\" VLAN_IFACE \".forwarding)\"))\n+\t\tgoto fail;\n+\n+\tSYS(fail, \"ip link add link veth1 name %s type vlan proto 802.1ad id 200\",\n+\t    QINQ_OUTER_IFACE);\n+\tSYS(fail, \"ip link add link %s name %s type vlan id 300\",\n+\t    QINQ_OUTER_IFACE, QINQ_INNER_IFACE);\n+\tSYS(fail, \"ip link set dev %s up\", QINQ_OUTER_IFACE);\n+\tSYS(fail, \"ip link set dev %s up\", QINQ_INNER_IFACE);\n+\tSYS(fail, \"ip route add %s/32 dev %s\", IPV4_QINQ_DST, QINQ_INNER_IFACE);\n+\n+\tSYS(fail, \"ip route add %s/32 via %s\", IPV4_VLAN_DST, IPV4_GW1);\n+\tSYS(fail, \"ip route add table %s %s/32 via %s\",\n+\t    VLAN_TABLE, IPV4_VLAN_DST, IPV4_VLAN_GW);\n+\tSYS(fail, \"ip rule add prio 3 iif %s lookup %s\", VLAN_IFACE, VLAN_TABLE);\n+\tSYS(fail, \"ip -6 route add %s/128 via %s\", IPV6_VLAN_DST, IPV6_GW1);\n+\tSYS(fail, \"ip -6 route add table %s %s/128 via %s\",\n+\t    VLAN_TABLE, IPV6_VLAN_DST, IPV6_VLAN_GW);\n+\tSYS(fail, \"ip -6 rule add prio 3 iif %s lookup %s\", VLAN_IFACE, VLAN_TABLE);\n+\n+\t/* a bond with one port and a VLAN on the bond */\n+\tSYS(fail, \"ip link add %s type bond\", BOND_IFACE);\n+\tSYS(fail, \"ip link add %s type veth peer name %s\", BOND_PORT, BOND_PORT_PEER);\n+\tSYS(fail, \"ip link set %s master %s\", BOND_PORT, BOND_IFACE);\n+\tSYS(fail, \"ip link set dev %s up\", BOND_IFACE);\n+\tSYS(fail, \"ip link set dev %s up\", BOND_PORT);\n+\tSYS(fail, \"ip link add link %s name %s.%d type vlan id %d\",\n+\t    BOND_IFACE, BOND_IFACE, BOND_VLAN_ID, BOND_VLAN_ID);\n+\tSYS(fail, \"ip link set dev %s.%d up\", BOND_IFACE, BOND_VLAN_ID);\n+\tSYS(fail, \"ip route add %s/32 dev %s.%d\",\n+\t    IPV4_BOND_VLAN_DST, BOND_IFACE, BOND_VLAN_ID);\n+\n+\t/*\n+\t * a VRF with its own dedicated subinterface (the iif rules above\n+\t * must not see it), for the table-selection-by-ingress cases\n+\t */\n+\tSYS(fail, \"ip link add %s type vrf table %s\", VRF_IFACE, VRF_TABLE);\n+\tSYS(fail, \"ip link set dev %s up\", VRF_IFACE);\n+\tSYS(fail, \"ip link add link veth1 name %s type vlan id %d\",\n+\t    VRF_VLAN_IFACE, VRF_VLAN_ID);\n+\tSYS(fail, \"ip link set %s master %s\", VRF_VLAN_IFACE, VRF_IFACE);\n+\tSYS(fail, \"ip link set dev %s up\", VRF_VLAN_IFACE);\n+\tSYS(fail, \"ip addr add %s/24 dev %s\", IPV4_VRF_IFACE_ADDR, VRF_VLAN_IFACE);\n+\terr = write_sysctl(\"/proc/sys/net/ipv4/conf/\" VRF_VLAN_IFACE \"/forwarding\", \"1\");\n+\tif (!ASSERT_OK(err, \"write_sysctl(net.ipv4.conf.\" VRF_VLAN_IFACE \".forwarding)\"))\n+\t\tgoto fail;\n+\tSYS(fail, \"ip route add %s/32 via %s\", IPV4_VRF_DST, IPV4_GW1);\n+\tSYS(fail, \"ip route add table %s %s/32 via %s\",\n+\t    VRF_TABLE, IPV4_VRF_DST, IPV4_VRF_GW);\n+\n+\t/* neighbours on the VLAN subinterface for the non-SKIP_NEIGH cases */\n+\terr = write_sysctl(\"/proc/sys/net/ipv4/neigh/\" VLAN_IFACE \"/gc_stale_time\", \"900\");\n+\tif (!ASSERT_OK(err, \"write_sysctl(net.ipv4.neigh.\" VLAN_IFACE \".gc_stale_time)\"))\n+\t\tgoto fail;\n+\tSYS(fail, \"ip neigh add %s dev %s lladdr %s nud stale\",\n+\t    IPV4_VLAN_EGRESS_DST, VLAN_IFACE, DMAC);\n+\tSYS(fail, \"ip neigh add %s dev %s lladdr %s nud stale\",\n+\t    IPV4_VLAN_GW, VLAN_IFACE, DMAC2);\n+\n+\t/* a VLAN on veth2 with a route in the tbid test table */\n+\tSYS(fail, \"ip link add link veth2 name %s type vlan id %d\",\n+\t    TBID_VLAN_IFACE, TBID_VLAN_ID);\n+\tSYS(fail, \"ip link set dev %s up\", TBID_VLAN_IFACE);\n+\tSYS(fail, \"ip route add table 100 %s/32 dev %s\",\n+\t    IPV4_TBID_VLAN_DST, TBID_VLAN_IFACE);\n+\n+\t/* a locked-mtu route via the subinterface for the FRAG_NEEDED case */\n+\tSYS(fail, \"ip route add %s/32 dev %s mtu lock 1000\",\n+\t    IPV4_VLAN_MTU_DST, VLAN_IFACE);\n+\n \treturn 0;\n fail:\n \treturn -1;\n@@ -218,9 +588,16 @@ static int set_lookup_params(struct bpf_fib_lookup *params,\n \tmemset(params, 0, sizeof(*params));\n \n \tparams-\u003el4_protocol = IPPROTO_TCP;\n-\tparams-\u003eifindex = ifindex;\n+\tparams-\u003eifindex = test-\u003eiif ? if_nametoindex(test-\u003eiif) : ifindex;\n \tparams-\u003etbid = test-\u003etbid;\n \tparams-\u003emark = test-\u003emark;\n+\tparams-\u003etot_len = test-\u003etot_len;\n+\n+\t/* h_vlan_proto/h_vlan_TCI union with tbid */\n+\tif (test-\u003elookup_flags \u0026 BPF_FIB_LOOKUP_VLAN_INPUT) {\n+\t\tparams-\u003eh_vlan_proto = htons(test-\u003evlan_proto);\n+\t\tparams-\u003eh_vlan_TCI = htons(test-\u003evlan_id);\n+\t}\n \n \tif (inet_pton(AF_INET6, test-\u003edaddr, params-\u003eipv6_dst) == 1) {\n \t\tparams-\u003efamily = AF_INET6;\n@@ -298,7 +675,7 @@ void test_fib_lookup(void)\n \tstruct nstoken *nstoken = NULL;\n \tstruct __sk_buff skb = { };\n \tstruct fib_lookup *skel;\n-\tint prog_fd, err, ret, i;\n+\tint prog_fd, xdp_fd, err, ret, i;\n \n \t/* The test does not use the skb-\u003edata, so\n \t * use pkt_v6 for both v6 and v4 test.\n@@ -309,11 +686,16 @@ void test_fib_lookup(void)\n \t\t    .ctx_in = \u0026skb,\n \t\t    .ctx_size_in = sizeof(skb),\n \t);\n+\tLIBBPF_OPTS(bpf_test_run_opts, xdp_opts,\n+\t\t    .data_in = \u0026pkt_v6,\n+\t\t    .data_size_in = sizeof(pkt_v6),\n+\t);\n \n \tskel = fib_lookup__open_and_load();\n \tif (!ASSERT_OK_PTR(skel, \"skel open_and_load\"))\n \t\treturn;\n \tprog_fd = bpf_program__fd(skel-\u003eprogs.fib_lookup);\n+\txdp_fd = bpf_program__fd(skel-\u003eprogs.fib_lookup_xdp);\n \n \tSYS(fail, \"ip netns add %s\", NS_TEST);\n \n@@ -343,6 +725,16 @@ void test_fib_lookup(void)\n \t\tif (!ASSERT_OK(err, \"bpf_prog_test_run_opts\"))\n \t\t\tcontinue;\n \n+\t\t/*\n+\t\t * BPF_FIB_LOOKUP_VLAN is XDP-only; the tc helper rejects it.\n+\t\t * These cases are exercised on the XDP path below.\n+\t\t */\n+\t\tif (tests[i].lookup_flags \u0026 BPF_FIB_LOOKUP_VLAN) {\n+\t\t\tASSERT_EQ(skel-\u003ebss-\u003efib_lookup_ret, -EINVAL,\n+\t\t\t\t  \"tc rejects BPF_FIB_LOOKUP_VLAN\");\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tASSERT_EQ(skel-\u003ebss-\u003efib_lookup_ret, tests[i].expected_ret,\n \t\t\t  \"fib_lookup_ret\");\n \n@@ -352,6 +744,21 @@ void test_fib_lookup(void)\n \t\tif (tests[i].expected_dst)\n \t\t\tassert_dst_ip(fib_params, tests[i].expected_dst);\n \n+\t\tif (tests[i].expected_dev)\n+\t\t\tASSERT_EQ(fib_params-\u003eifindex,\n+\t\t\t\t  if_nametoindex(tests[i].expected_dev), \"ifindex\");\n+\n+\t\tif (tests[i].expected_mtu)\n+\t\t\tASSERT_EQ(fib_params-\u003emtu_result, tests[i].expected_mtu,\n+\t\t\t\t  \"mtu_result\");\n+\n+\t\tif (tests[i].check_vlan) {\n+\t\t\tASSERT_EQ(fib_params-\u003eh_vlan_proto,\n+\t\t\t\t  htons(tests[i].vlan_proto), \"h_vlan_proto\");\n+\t\t\tASSERT_EQ(fib_params-\u003eh_vlan_TCI,\n+\t\t\t\t  htons(tests[i].vlan_id), \"h_vlan_TCI\");\n+\t\t}\n+\n \t\tret = memcmp(tests[i].dmac, fib_params-\u003edmac, sizeof(tests[i].dmac));\n \t\tif (!ASSERT_EQ(ret, 0, \"dmac not match\")) {\n \t\t\tchar expected[18], actual[18];\n@@ -361,17 +768,322 @@ void test_fib_lookup(void)\n \t\t\tprintf(\"dmac expected %s actual %s \", expected, actual);\n \t\t}\n \n-\t\t// ensure tbid is zero'd out after fib lookup.\n-\t\tif (tests[i].lookup_flags \u0026 BPF_FIB_LOOKUP_DIRECT) {\n+\t\t/*\n+\t\t * ensure tbid is zero'd out after fib lookup. With\n+\t\t * BPF_FIB_LOOKUP_VLAN the union holds the packed vlan\n+\t\t * fields instead, so skip the check for those.\n+\t\t */\n+\t\tif ((tests[i].lookup_flags \u0026 BPF_FIB_LOOKUP_DIRECT) \u0026\u0026\n+\t\t    !(tests[i].lookup_flags \u0026 BPF_FIB_LOOKUP_VLAN)) {\n \t\t\tif (!ASSERT_EQ(skel-\u003ebss-\u003efib_params.tbid, 0,\n \t\t\t\t\t\"expected fib_params.tbid to be zero\"))\n \t\t\t\tgoto fail;\n \t\t}\n \t}\n \n+\t/*\n+\t * Re-run the cases through bpf_xdp_fib_lookup(). test_run uses the\n+\t * current netns' loopback for ctx-\u003erxq-\u003edev, so dev_net() is NS_TEST\n+\t * and the lookup runs against its FIB. The path-independent results\n+\t * (return code, swapped ifindex, vlan tag, gateway) must match the skb\n+\t * path; the no-tot_len mtu_result is skb-specific and not rechecked.\n+\t */\n+\tfor (i = 0; i \u003c ARRAY_SIZE(tests); i++) {\n+\t\tif (set_lookup_params(fib_params, \u0026tests[i], skb.ifindex))\n+\t\t\tcontinue;\n+\n+\t\tskel-\u003ebss-\u003efib_lookup_ret = -1;\n+\t\tskel-\u003ebss-\u003elookup_flags = tests[i].lookup_flags;\n+\n+\t\terr = bpf_prog_test_run_opts(xdp_fd, \u0026xdp_opts);\n+\t\tif (!ASSERT_OK(err, \"xdp test_run\"))\n+\t\t\tcontinue;\n+\n+\t\tif (!ASSERT_EQ(skel-\u003ebss-\u003efib_lookup_ret, tests[i].expected_ret,\n+\t\t\t       \"xdp fib_lookup_ret\"))\n+\t\t\tprintf(\"(xdp) %s\\n\", tests[i].desc);\n+\n+\t\tif (tests[i].expected_dev)\n+\t\t\tASSERT_EQ(fib_params-\u003eifindex,\n+\t\t\t\t  if_nametoindex(tests[i].expected_dev),\n+\t\t\t\t  \"xdp ifindex\");\n+\n+\t\tif (tests[i].expected_dst)\n+\t\t\tassert_dst_ip(fib_params, tests[i].expected_dst);\n+\n+\t\tif (tests[i].check_vlan) {\n+\t\t\tASSERT_EQ(fib_params-\u003eh_vlan_proto,\n+\t\t\t\t  htons(tests[i].vlan_proto), \"xdp h_vlan_proto\");\n+\t\t\tASSERT_EQ(fib_params-\u003eh_vlan_TCI,\n+\t\t\t\t  htons(tests[i].vlan_id), \"xdp h_vlan_TCI\");\n+\t\t}\n+\n+\t\tret = memcmp(tests[i].dmac, fib_params-\u003edmac, sizeof(tests[i].dmac));\n+\t\tASSERT_EQ(ret, 0, \"xdp dmac\");\n+\n+\t\t/*\n+\t\t * mtu_result from a tot_len lookup is the route mtu and is\n+\t\t * path-independent; the no-tot_len arm reads dev-\u003emtu and is\n+\t\t * skb-only, so gate on tot_len\n+\t\t */\n+\t\tif (tests[i].expected_mtu \u0026\u0026 tests[i].tot_len)\n+\t\t\tASSERT_EQ(fib_params-\u003emtu_result, tests[i].expected_mtu,\n+\t\t\t\t  \"xdp mtu_result\");\n+\t}\n+\n fail:\n \tif (nstoken)\n \t\tclose_netns(nstoken);\n \tSYS_NOFAIL(\"ip netns del \" NS_TEST);\n \tfib_lookup__destroy(skel);\n }\n+\n+#define NS_VLAN_A\t\"fib_lookup_vlan_ns_a\"\n+#define NS_VLAN_B\t\"fib_lookup_vlan_ns_b\"\n+#define IPV4_VLAN_NETNS_ADDR\t\"10.66.0.1\"\n+#define IPV4_VLAN_NETNS_DST\t\"10.66.0.2\"\n+\n+/*\n+ * A VLAN device can be moved to another netns while staying registered\n+ * on its parent. Neither direction may then cross the boundary: the\n+ * egress flag must not publish the foreign parent's ifindex, and the\n+ * input flag must fail closed rather than use a foreign ingress.\n+ */\n+void test_fib_lookup_vlan_netns(void)\n+{\n+\tstruct bpf_fib_lookup *fib_params;\n+\tstruct nstoken *nstoken = NULL;\n+\tstruct __sk_buff skb = { };\n+\tstruct fib_lookup *skel = NULL;\n+\tint prog_fd, xdp_fd, err, parent_idx, vlan_idx;\n+\n+\tLIBBPF_OPTS(bpf_test_run_opts, run_opts,\n+\t\t    .data_in = \u0026pkt_v6,\n+\t\t    .data_size_in = sizeof(pkt_v6),\n+\t\t    .ctx_in = \u0026skb,\n+\t\t    .ctx_size_in = sizeof(skb),\n+\t);\n+\tLIBBPF_OPTS(bpf_test_run_opts, xdp_opts,\n+\t\t    .data_in = \u0026pkt_v6,\n+\t\t    .data_size_in = sizeof(pkt_v6),\n+\t);\n+\n+\tskel = fib_lookup__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"skel open_and_load\"))\n+\t\treturn;\n+\tprog_fd = bpf_program__fd(skel-\u003eprogs.fib_lookup);\n+\txdp_fd = bpf_program__fd(skel-\u003eprogs.fib_lookup_xdp);\n+\tfib_params = \u0026skel-\u003ebss-\u003efib_params;\n+\n+\tSYS(fail, \"ip netns add %s\", NS_VLAN_A);\n+\tSYS(fail, \"ip netns add %s\", NS_VLAN_B);\n+\n+\tnstoken = open_netns(NS_VLAN_A);\n+\tif (!ASSERT_OK_PTR(nstoken, \"open_netns(a)\"))\n+\t\tgoto fail;\n+\n+\tSYS(fail, \"ip link add veth7 type veth peer name veth8\");\n+\tSYS(fail, \"ip link set dev veth7 up\");\n+\tSYS(fail, \"ip link add link veth7 name veth7.66 type vlan id 66\");\n+\tSYS(fail, \"ip link set veth7.66 netns %s\", NS_VLAN_B);\n+\t/*\n+\t * up it in B before the input lookup: the move closed it, and a\n+\t * down device fails the resolver on IFF_UP before reaching the\n+\t * netns check this subtest exists to pin\n+\t */\n+\tSYS(fail, \"ip -n %s link set dev veth7.66 up\", NS_VLAN_B);\n+\n+\tparent_idx = if_nametoindex(\"veth7\");\n+\tif (!ASSERT_NEQ(parent_idx, 0, \"if_nametoindex(veth7)\"))\n+\t\tgoto fail;\n+\n+\t/*\n+\t * input: the moved device is still in veth7's VLAN group, but it\n+\t * lives in another netns, so the lookup must fail closed\n+\t */\n+\tskb.ifindex = parent_idx;\n+\tmemset(fib_params, 0, sizeof(*fib_params));\n+\tfib_params-\u003efamily = AF_INET;\n+\tfib_params-\u003el4_protocol = IPPROTO_TCP;\n+\tfib_params-\u003eifindex = parent_idx;\n+\tfib_params-\u003eh_vlan_proto = htons(ETH_P_8021Q);\n+\tfib_params-\u003eh_vlan_TCI = htons(66);\n+\tif (!ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_NETNS_DST, \u0026fib_params-\u003eipv4_dst),\n+\t\t       1, \"inet_pton(dst)\"))\n+\t\tgoto fail;\n+\n+\tskel-\u003ebss-\u003efib_lookup_ret = -1;\n+\tskel-\u003ebss-\u003elookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT |\n+\t\t\t\t  BPF_FIB_LOOKUP_SKIP_NEIGH;\n+\terr = bpf_prog_test_run_opts(prog_fd, \u0026run_opts);\n+\tif (!ASSERT_OK(err, \"test_run(input)\"))\n+\t\tgoto fail;\n+\tASSERT_EQ(skel-\u003ebss-\u003efib_lookup_ret, BPF_FIB_LKUP_RET_NOT_FWDED,\n+\t\t  \"input across netns fails closed\");\n+\tASSERT_EQ(fib_params-\u003eifindex, parent_idx, \"ifindex untouched\");\n+\tASSERT_EQ(fib_params-\u003eh_vlan_TCI, htons(66), \"tag untouched\");\n+\n+\tclose_netns(nstoken);\n+\tnstoken = open_netns(NS_VLAN_B);\n+\tif (!ASSERT_OK_PTR(nstoken, \"open_netns(b)\"))\n+\t\tgoto fail;\n+\n+\t/*\n+\t * egress: the fib result is the VLAN device here, but its parent\n+\t * is in the other netns, so the swap must not happen\n+\t */\n+\tSYS(fail, \"ip addr add %s/24 dev veth7.66\", IPV4_VLAN_NETNS_ADDR);\n+\terr = write_sysctl(\"/proc/sys/net/ipv4/conf/veth7.66/forwarding\", \"1\");\n+\tif (!ASSERT_OK(err, \"write_sysctl(forwarding)\"))\n+\t\tgoto fail;\n+\n+\tvlan_idx = if_nametoindex(\"veth7.66\");\n+\tif (!ASSERT_NEQ(vlan_idx, 0, \"if_nametoindex(veth7.66)\"))\n+\t\tgoto fail;\n+\n+\tmemset(fib_params, 0, sizeof(*fib_params));\n+\tfib_params-\u003efamily = AF_INET;\n+\tfib_params-\u003el4_protocol = IPPROTO_TCP;\n+\tfib_params-\u003eifindex = vlan_idx;\n+\tif (!ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_NETNS_DST, \u0026fib_params-\u003eipv4_dst),\n+\t\t       1, \"inet_pton(dst)\") ||\n+\t    !ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_NETNS_ADDR, \u0026fib_params-\u003eipv4_src),\n+\t\t       1, \"inet_pton(src)\"))\n+\t\tgoto fail;\n+\n+\tskel-\u003ebss-\u003efib_lookup_ret = -1;\n+\tskel-\u003ebss-\u003elookup_flags = BPF_FIB_LOOKUP_VLAN |\n+\t\t\t\t  BPF_FIB_LOOKUP_SKIP_NEIGH;\n+\terr = bpf_prog_test_run_opts(xdp_fd, \u0026xdp_opts);\n+\tif (!ASSERT_OK(err, \"test_run(egress)\"))\n+\t\tgoto fail;\n+\tASSERT_EQ(skel-\u003ebss-\u003efib_lookup_ret, BPF_FIB_LKUP_RET_VLAN_FAILURE,\n+\t\t  \"egress returns VLAN_FAILURE\");\n+\tASSERT_EQ(fib_params-\u003eifindex, vlan_idx,\n+\t\t  \"foreign parent not published\");\n+\tASSERT_EQ(fib_params-\u003eh_vlan_TCI, 0, \"vlan fields zero\");\n+\n+fail:\n+\tif (nstoken)\n+\t\tclose_netns(nstoken);\n+\tSYS_NOFAIL(\"ip netns del \" NS_VLAN_A);\n+\tSYS_NOFAIL(\"ip netns del \" NS_VLAN_B);\n+\tfib_lookup__destroy(skel);\n+}\n+\n+#define REDIRECT_NPKTS 1000\n+#define NS_REDIRECT \"fib_lookup_redirect_ns\"\n+\n+/*\n+ * The egress flag exists so an XDP program can redirect to the physical\n+ * parent. A redirect that lands on a VLAN device is dropped at\n+ * xdp_do_flush(), because a VLAN device has no ndo_xdp_xmit. Drive real\n+ * frames with BPF_F_TEST_XDP_LIVE_FRAMES, which runs the native\n+ * xdp_do_redirect() + xdp_do_flush() path: a reducible VLAN egress\n+ * resolves to veth1 and is delivered to its peer veth2, while a QinQ\n+ * egress returns VLAN_FAILURE and is passed to the stack instead of\n+ * redirected to a device that would silently drop it.\n+ */\n+void test_fib_lookup_vlan_redirect(void)\n+{\n+\tint redirect_fd, err, veth1_idx, veth2_idx = -1;\n+\tstruct bpf_fib_lookup *fib_params;\n+\tstruct nstoken *nstoken = NULL;\n+\tstruct fib_lookup *skel = NULL;\n+\tbool xdp_attached = false;\n+\n+\tLIBBPF_OPTS(bpf_test_run_opts, lf_opts,\n+\t\t    .data_in = \u0026pkt_v4,\n+\t\t    .data_size_in = sizeof(pkt_v4),\n+\t\t    .flags = BPF_F_TEST_XDP_LIVE_FRAMES,\n+\t\t    .repeat = REDIRECT_NPKTS,\n+\t);\n+\n+\tskel = fib_lookup__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"skel open_and_load\"))\n+\t\treturn;\n+\tredirect_fd = bpf_program__fd(skel-\u003eprogs.fib_lookup_redirect);\n+\tfib_params = \u0026skel-\u003ebss-\u003efib_params;\n+\n+\tSYS(fail, \"ip netns add %s\", NS_REDIRECT);\n+\tnstoken = open_netns(NS_REDIRECT);\n+\tif (!ASSERT_OK_PTR(nstoken, \"open_netns\"))\n+\t\tgoto fail;\n+\tif (setup_netns())\n+\t\tgoto fail;\n+\n+\tveth1_idx = if_nametoindex(\"veth1\");\n+\tveth2_idx = if_nametoindex(\"veth2\");\n+\tif (!ASSERT_NEQ(veth1_idx, 0, \"if_nametoindex(veth1)\") ||\n+\t    !ASSERT_NEQ(veth2_idx, 0, \"if_nametoindex(veth2)\"))\n+\t\tgoto fail;\n+\n+\t/*\n+\t * A redirect to veth1 is delivered to its peer veth2. veth_xdp_xmit()\n+\t * only accepts the frame if veth2's NAPI is up, which on veth means\n+\t * veth2 carries an XDP program; xdp_count tallies what arrives.\n+\t */\n+\terr = bpf_xdp_attach(veth2_idx, bpf_program__fd(skel-\u003eprogs.xdp_count),\n+\t\t\t     XDP_FLAGS_DRV_MODE, NULL);\n+\tif (!ASSERT_OK(err, \"attach xdp_count on veth2\"))\n+\t\tgoto fail;\n+\txdp_attached = true;\n+\n+\t/* reducible VLAN egress: resolves to the physical parent veth1 */\n+\tmemset(fib_params, 0, sizeof(*fib_params));\n+\tfib_params-\u003efamily = AF_INET;\n+\tfib_params-\u003el4_protocol = IPPROTO_TCP;\n+\tfib_params-\u003eifindex = veth1_idx;\n+\tif (!ASSERT_EQ(inet_pton(AF_INET, IPV4_IFACE_ADDR, \u0026fib_params-\u003eipv4_src),\n+\t\t       1, \"inet_pton(src)\") ||\n+\t    !ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_EGRESS_DST, \u0026fib_params-\u003eipv4_dst),\n+\t\t       1, \"inet_pton(reducible dst)\"))\n+\t\tgoto fail;\n+\tskel-\u003ebss-\u003elookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH;\n+\tskel-\u003ebss-\u003eredirected = 0;\n+\tskel-\u003ebss-\u003epassed = 0;\n+\tskel-\u003ebss-\u003edelivered = 0;\n+\n+\terr = bpf_prog_test_run_opts(redirect_fd, \u0026lf_opts);\n+\tif (!ASSERT_OK(err, \"test_run(reducible egress)\"))\n+\t\tgoto fail;\n+\tASSERT_EQ(skel-\u003ebss-\u003eredirected, REDIRECT_NPKTS, \"reducible egress redirected\");\n+\tASSERT_EQ(skel-\u003ebss-\u003epassed, 0, \"reducible egress not passed\");\n+\tASSERT_GT(skel-\u003ebss-\u003edelivered, 0, \"reducible egress delivered to veth2\");\n+\n+\t/*\n+\t * QinQ egress: not reducible, so the lookup returns VLAN_FAILURE and\n+\t * the program passes the frame instead of redirecting to the inner\n+\t * VLAN device. redirected == 0 is the assertion that matters: the\n+\t * program did not redirect to a device that would drop the frame at\n+\t * xdp_do_flush(). veth2's delivered count is not checked here, since\n+\t * a passed frame can still reach veth2 through the stack's forwarding\n+\t * path, which is unrelated to the redirect under test.\n+\t */\n+\tmemset(fib_params, 0, sizeof(*fib_params));\n+\tfib_params-\u003efamily = AF_INET;\n+\tfib_params-\u003el4_protocol = IPPROTO_TCP;\n+\tfib_params-\u003eifindex = veth1_idx;\n+\tif (!ASSERT_EQ(inet_pton(AF_INET, IPV4_IFACE_ADDR, \u0026fib_params-\u003eipv4_src),\n+\t\t       1, \"inet_pton(src)\") ||\n+\t    !ASSERT_EQ(inet_pton(AF_INET, IPV4_QINQ_DST, \u0026fib_params-\u003eipv4_dst),\n+\t\t       1, \"inet_pton(qinq dst)\"))\n+\t\tgoto fail;\n+\tskel-\u003ebss-\u003elookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH;\n+\tskel-\u003ebss-\u003eredirected = 0;\n+\tskel-\u003ebss-\u003epassed = 0;\n+\n+\terr = bpf_prog_test_run_opts(redirect_fd, \u0026lf_opts);\n+\tif (!ASSERT_OK(err, \"test_run(qinq egress)\"))\n+\t\tgoto fail;\n+\tASSERT_EQ(skel-\u003ebss-\u003epassed, REDIRECT_NPKTS, \"qinq egress passed\");\n+\tASSERT_EQ(skel-\u003ebss-\u003eredirected, 0, \"qinq egress not redirected\");\n+\n+fail:\n+\tif (xdp_attached)\n+\t\tbpf_xdp_detach(veth2_idx, XDP_FLAGS_DRV_MODE, NULL);\n+\tif (nstoken)\n+\t\tclose_netns(nstoken);\n+\tSYS_NOFAIL(\"ip netns del \" NS_REDIRECT);\n+\tfib_lookup__destroy(skel);\n+}\ndiff --git a/tools/testing/selftests/bpf/progs/fib_lookup.c b/tools/testing/selftests/bpf/progs/fib_lookup.c\nindex 7b5dd2214ff46a..36b7218d9ae2ec 100644\n--- a/tools/testing/selftests/bpf/progs/fib_lookup.c\n+++ b/tools/testing/selftests/bpf/progs/fib_lookup.c\n@@ -4,7 +4,11 @@\n #include \u003clinux/types.h\u003e\n #include \u003clinux/bpf.h\u003e\n #include \u003clinux/pkt_cls.h\u003e\n+#include \u003clinux/if_ether.h\u003e\n+#include \u003clinux/ip.h\u003e\n+#include \u003clinux/in.h\u003e\n #include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_endian.h\u003e\n \n struct bpf_fib_lookup fib_params = {};\n int fib_lookup_ret = 0;\n@@ -19,4 +23,57 @@ int fib_lookup(struct __sk_buff *skb)\n \treturn TC_ACT_SHOT;\n }\n \n+SEC(\"xdp\")\n+int fib_lookup_xdp(struct xdp_md *ctx)\n+{\n+\tfib_lookup_ret = bpf_fib_lookup(ctx, \u0026fib_params, sizeof(fib_params),\n+\t\t\t\t\tlookup_flags);\n+\n+\treturn XDP_DROP;\n+}\n+\n+int redirected = 0;\n+int passed = 0;\n+int delivered = 0;\n+\n+SEC(\"xdp\")\n+int fib_lookup_redirect(struct xdp_md *ctx)\n+{\n+\tstruct bpf_fib_lookup params = fib_params;\n+\tlong ret;\n+\n+\tret = bpf_fib_lookup(ctx, \u0026params, sizeof(params), lookup_flags);\n+\tif (ret == BPF_FIB_LKUP_RET_SUCCESS) {\n+\t\tredirected++;\n+\t\treturn bpf_redirect(params.ifindex, 0);\n+\t}\n+\n+\tpassed++;\n+\treturn XDP_PASS;\n+}\n+\n+SEC(\"xdp\")\n+int xdp_count(struct xdp_md *ctx)\n+{\n+\tvoid *data = (void *)(long)ctx-\u003edata;\n+\tvoid *data_end = (void *)(long)ctx-\u003edata_end;\n+\tstruct ethhdr *eth = data;\n+\tstruct iphdr *iph;\n+\n+\t/*\n+\t * count only the test's TCP frames: the netns has live\n+\t * link-local traffic (DAD, MLD) that would satisfy a bare\n+\t * counter\n+\t */\n+\tif ((void *)(eth + 1) \u003e data_end ||\n+\t    eth-\u003eh_proto != bpf_htons(ETH_P_IP))\n+\t\treturn XDP_DROP;\n+\tiph = (void *)(eth + 1);\n+\tif ((void *)(iph + 1) \u003e data_end || iph-\u003eprotocol != IPPROTO_TCP)\n+\t\treturn XDP_DROP;\n+\n+\tdelivered++;\n+\treturn XDP_DROP;\n+}\n+\n char _license[] SEC(\"license\") = \"GPL\";\n"
}

2/1 2026/07/13 21:56 agent patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
{
  "EnableConfigs": [
    "VLAN_8021Q",
    "INET",
    "IPV6"
  ],
  "FocusSymbols": [
    "bpf_fib_vlan_input_dev",
    "bpf_fib_set_fwd_params",
    "bpf_fib_lookup_flags_ok",
    "bpf_xdp_fib_lookup",
    "bpf_skb_fib_lookup",
    "bpf_ipv4_fib_lookup",
    "bpf_ipv6_fib_lookup"
  ],
  "Reasoning": "The patch adds support for VLANs in the BPF FIB lookup helpers (`bpf_xdp_fib_lookup` and `bpf_skb_fib_lookup`). It introduces new flags `BPF_FIB_LOOKUP_VLAN` and `BPF_FIB_LOOKUP_VLAN_INPUT` to handle VLAN egress and ingress resolution. This modifies core networking and BPF logic, which is reachable from BPF programs and can be fuzzed by syzkaller.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Do not list too generic configs (we already have them enabled). Only list those that
   specifically cover the modified code. List them in the EnableConfigs output array,
   and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 4a7f1fa08dce1e1c25400be0f3cf11db30c9dc1b
Author: syz-cluster <triage@syzkaller.com>
Date:   Mon Jul 13 21:56:06 2026 +0000

    syz-cluster: applied patch under review

diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 89b36de5fdbb6d..ff03019abe3554 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -3532,6 +3532,47 @@ union bpf_attr {
  *			Use the mark present in *params*->mark for the fib lookup.
  *			This option should not be used with BPF_FIB_LOOKUP_DIRECT,
  *			as it only has meaning for full lookups.
+ *		**BPF_FIB_LOOKUP_VLAN**
+ *			If the fib lookup resolves to a VLAN device whose
+ *			parent is a real (non-VLAN) device, set
+ *			*params*->h_vlan_proto and *params*->h_vlan_TCI from
+ *			the VLAN device and replace *params*->ifindex with the
+ *			parent's ifindex. *params*->h_vlan_TCI carries the VID
+ *			only, with PCP and DEI bits zero; a consumer wanting to
+ *			set egress priority writes PCP itself. *params*->smac is
+ *			the VLAN device's own address, which can differ from the
+ *			parent's. Only the immediate parent is resolved; if it
+ *			is itself a VLAN device (QinQ) or in another namespace,
+ *			the egress cannot be reduced to a physical device plus
+ *			one tag and the lookup returns
+ *			**BPF_FIB_LKUP_RET_VLAN_FAILURE** with *params*->ifindex
+ *			left at the input. To obtain the VLAN device's own
+ *			ifindex, repeat the lookup without
+ *			**BPF_FIB_LOOKUP_VLAN**, re-initializing *params*
+ *			first: output fields overwrite the inputs they share
+ *			storage with. The swap and the vlan fields
+ *			are written only on success; other output fields keep
+ *			the helper's existing behaviour, so a frag-needed result
+ *			still reports the route mtu in *params*->mtu_result.
+ *			This flag is only valid for XDP programs; tc programs
+ *			receive -EINVAL since they can redirect to the VLAN
+ *			device directly.
+ *		**BPF_FIB_LOOKUP_VLAN_INPUT**
+ *			Treat *params*->h_vlan_proto and *params*->h_vlan_TCI
+ *			as an input VLAN tag and run the lookup as if ingress
+ *			had happened on the VLAN subinterface carrying that tag
+ *			on *params*->ifindex. The VID is the low 12 bits of
+ *			*params*->h_vlan_TCI; *params*->h_vlan_proto must be
+ *			ETH_P_8021Q or ETH_P_8021AD in network byte order, else
+ *			**-EINVAL**. If *params*->ifindex is itself a VLAN
+ *			device, its inner (QinQ) subinterface is matched; for a
+ *			bond or team, pass the master's ifindex. An unmatched
+ *			tag, a down device, or one in another namespace returns
+ *			**BPF_FIB_LKUP_RET_NOT_FWDED**, mirroring real ingress.
+ *			A VID of 0 is looked up literally, so do not set this
+ *			flag for priority-tagged frames. Cannot be combined with
+ *			**BPF_FIB_LOOKUP_TBID** or **BPF_FIB_LOOKUP_OUTPUT**
+ *			(returns **-EINVAL**).
  *
  *		*ctx* is either **struct xdp_md** for XDP programs or
  *		**struct sk_buff** tc cls_act programs.
@@ -7327,6 +7368,8 @@ enum {
 	BPF_FIB_LOOKUP_TBID    = (1U << 3),
 	BPF_FIB_LOOKUP_SRC     = (1U << 4),
 	BPF_FIB_LOOKUP_MARK    = (1U << 5),
+	BPF_FIB_LOOKUP_VLAN    = (1U << 6),
+	BPF_FIB_LOOKUP_VLAN_INPUT = (1U << 7),
 };
 
 enum {
@@ -7340,6 +7383,7 @@ enum {
 	BPF_FIB_LKUP_RET_NO_NEIGH,     /* no neighbor entry for nh */
 	BPF_FIB_LKUP_RET_FRAG_NEEDED,  /* fragmentation required to fwd */
 	BPF_FIB_LKUP_RET_NO_SRC_ADDR,  /* failed to derive IP src addr */
+	BPF_FIB_LKUP_RET_VLAN_FAILURE, /* VLAN egress, parent unresolvable */
 };
 
 struct bpf_fib_lookup {
@@ -7393,7 +7437,13 @@ struct bpf_fib_lookup {
 
 	union {
 		struct {
-			/* output */
+			/*
+			 * output with BPF_FIB_LOOKUP_VLAN: set from the
+			 * resolved egress VLAN device (see the flag); zeroed
+			 * on other successful lookups. input with
+			 * BPF_FIB_LOOKUP_VLAN_INPUT: the VLAN tag to scope
+			 * the lookup by.
+			 */
 			__be16	h_vlan_proto;
 			__be16	h_vlan_TCI;
 		};
diff --git a/net/core/filter.c b/net/core/filter.c
index 2e96b4b847ce12..0ea362fa4287e3 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -6201,21 +6201,60 @@ static const struct bpf_func_proto bpf_skb_get_xfrm_state_proto = {
 #endif
 
 #if IS_ENABLED(CONFIG_INET) || IS_ENABLED(CONFIG_IPV6)
-static int bpf_fib_set_fwd_params(struct bpf_fib_lookup *params, u32 mtu)
+static int bpf_fib_set_fwd_params(struct net_device *dev,
+				  struct bpf_fib_lookup *params,
+				  u32 flags, u32 mtu, u32 in_ifindex)
 {
 	params->h_vlan_TCI = 0;
 	params->h_vlan_proto = 0;
+
+#if IS_ENABLED(CONFIG_VLAN_8021Q)
+	if ((flags & BPF_FIB_LOOKUP_VLAN) && is_vlan_dev(dev)) {
+		struct net_device *real_dev = vlan_dev_priv(dev)->real_dev;
+
+		if (!is_vlan_dev(real_dev) &&
+		    net_eq(dev_net(real_dev), dev_net(dev))) {
+			params->h_vlan_proto = vlan_dev_vlan_proto(dev);
+			params->h_vlan_TCI = htons(vlan_dev_vlan_id(dev));
+			params->ifindex = real_dev->ifindex;
+		} else {
+			params->ifindex = in_ifindex;
+			return BPF_FIB_LKUP_RET_VLAN_FAILURE;
+		}
+	}
+#endif
+
 	if (mtu)
 		params->mtu_result = mtu; /* union with tot_len */
 
 	return 0;
 }
+
+static struct net_device *bpf_fib_vlan_input_dev(struct net_device *dev,
+						 const struct bpf_fib_lookup *params)
+{
+	__be16 proto = params->h_vlan_proto;
+	struct net_device *vlan_dev;
+	u16 vid;
+
+	if (proto != htons(ETH_P_8021Q) && proto != htons(ETH_P_8021AD))
+		return ERR_PTR(-EINVAL);
+
+	vid = ntohs(params->h_vlan_TCI) & VLAN_VID_MASK;
+	vlan_dev = __vlan_find_dev_deep_rcu(dev, proto, vid);
+	if (!vlan_dev || !(vlan_dev->flags & IFF_UP) ||
+	    !net_eq(dev_net(vlan_dev), dev_net(dev)))
+		return NULL;
+
+	return vlan_dev;
+}
 #endif
 
 #if IS_ENABLED(CONFIG_INET)
 static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
 			       u32 flags, bool check_mtu)
 {
+	u32 in_ifindex = params->ifindex;
 	struct neighbour *neigh = NULL;
 	struct fib_nh_common *nhc;
 	struct in_device *in_dev;
@@ -6229,6 +6268,14 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
 	if (unlikely(!dev))
 		return -ENODEV;
 
+	if (flags & BPF_FIB_LOOKUP_VLAN_INPUT) {
+		dev = bpf_fib_vlan_input_dev(dev, params);
+		if (IS_ERR(dev))
+			return PTR_ERR(dev);
+		if (!dev)
+			return BPF_FIB_LKUP_RET_NOT_FWDED;
+	}
+
 	/* verify forwarding is enabled on this interface */
 	in_dev = __in_dev_get_rcu(dev);
 	if (unlikely(!in_dev || !IN_DEV_FORWARD(in_dev)))
@@ -6238,7 +6285,11 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
 		fl4.flowi4_iif = 1;
 		fl4.flowi4_oif = params->ifindex;
 	} else {
-		fl4.flowi4_iif = params->ifindex;
+		/*
+		 * dev->ifindex, not params->ifindex: VLAN_INPUT may have
+		 * resolved dev to a subinterface above.
+		 */
+		fl4.flowi4_iif = dev->ifindex;
 		fl4.flowi4_oif = 0;
 	}
 	fl4.flowi4_dscp = inet_dsfield_to_dscp(params->tos);
@@ -6347,7 +6398,7 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
 	memcpy(params->smac, dev->dev_addr, ETH_ALEN);
 
 set_fwd_params:
-	return bpf_fib_set_fwd_params(params, mtu);
+	return bpf_fib_set_fwd_params(dev, params, flags, mtu, in_ifindex);
 }
 #endif
 
@@ -6357,6 +6408,7 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
 {
 	struct in6_addr *src = (struct in6_addr *) params->ipv6_src;
 	struct in6_addr *dst = (struct in6_addr *) params->ipv6_dst;
+	u32 in_ifindex = params->ifindex;
 	struct fib6_result res = {};
 	struct neighbour *neigh;
 	struct net_device *dev;
@@ -6374,6 +6426,14 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
 	if (unlikely(!dev))
 		return -ENODEV;
 
+	if (flags & BPF_FIB_LOOKUP_VLAN_INPUT) {
+		dev = bpf_fib_vlan_input_dev(dev, params);
+		if (IS_ERR(dev))
+			return PTR_ERR(dev);
+		if (!dev)
+			return BPF_FIB_LKUP_RET_NOT_FWDED;
+	}
+
 	idev = __in6_dev_get_safely(dev);
 	if (unlikely(!idev || !READ_ONCE(idev->cnf.forwarding)))
 		return BPF_FIB_LKUP_RET_FWD_DISABLED;
@@ -6382,7 +6442,12 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
 		fl6.flowi6_iif = 1;
 		oif = fl6.flowi6_oif = params->ifindex;
 	} else {
-		oif = fl6.flowi6_iif = params->ifindex;
+		/*
+		 * dev->ifindex, not params->ifindex: VLAN_INPUT may have
+		 * resolved dev to a subinterface above.
+		 */
+		oif = dev->ifindex;
+		fl6.flowi6_iif = oif;
 		fl6.flowi6_oif = 0;
 		strict = RT6_LOOKUP_F_HAS_SADDR;
 	}
@@ -6486,13 +6551,26 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
 	memcpy(params->smac, dev->dev_addr, ETH_ALEN);
 
 set_fwd_params:
-	return bpf_fib_set_fwd_params(params, mtu);
+	return bpf_fib_set_fwd_params(dev, params, flags, mtu, in_ifindex);
 }
 #endif
 
 #define BPF_FIB_LOOKUP_MASK (BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_OUTPUT | \
 			     BPF_FIB_LOOKUP_SKIP_NEIGH | BPF_FIB_LOOKUP_TBID | \
-			     BPF_FIB_LOOKUP_SRC | BPF_FIB_LOOKUP_MARK)
+			     BPF_FIB_LOOKUP_SRC | BPF_FIB_LOOKUP_MARK | \
+			     BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_VLAN_INPUT)
+
+static bool bpf_fib_lookup_flags_ok(u32 flags)
+{
+	if (flags & ~BPF_FIB_LOOKUP_MASK)
+		return false;
+
+	if ((flags & BPF_FIB_LOOKUP_VLAN_INPUT) &&
+	    (flags & (BPF_FIB_LOOKUP_TBID | BPF_FIB_LOOKUP_OUTPUT)))
+		return false;
+
+	return true;
+}
 
 BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx,
 	   struct bpf_fib_lookup *, params, int, plen, u32, flags)
@@ -6500,7 +6578,7 @@ BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx,
 	if (plen < sizeof(*params))
 		return -EINVAL;
 
-	if (flags & ~BPF_FIB_LOOKUP_MASK)
+	if (!bpf_fib_lookup_flags_ok(flags))
 		return -EINVAL;
 
 	switch (params->family) {
@@ -6538,7 +6616,10 @@ BPF_CALL_4(bpf_skb_fib_lookup, struct sk_buff *, skb,
 	if (plen < sizeof(*params))
 		return -EINVAL;
 
-	if (flags & ~BPF_FIB_LOOKUP_MASK)
+	if (!bpf_fib_lookup_flags_ok(flags))
+		return -EINVAL;
+
+	if (flags & BPF_FIB_LOOKUP_VLAN)
 		return -EINVAL;
 
 	if (params->tot_len)
diff --git a/tools/include/uapi/linux/bpf.h b/tools/include/uapi/linux/bpf.h
index 89b36de5fdbb6d..ff03019abe3554 100644
--- a/tools/include/uapi/linux/bpf.h
+++ b/tools/include/uapi/linux/bpf.h
@@ -3532,6 +3532,47 @@ union bpf_attr {
  *			Use the mark present in *params*->mark for the fib lookup.
  *			This option should not be used with BPF_FIB_LOOKUP_DIRECT,
  *			as it only has meaning for full lookups.
+ *		**BPF_FIB_LOOKUP_VLAN**
+ *			If the fib lookup resolves to a VLAN device whose
+ *			parent is a real (non-VLAN) device, set
+ *			*params*->h_vlan_proto and *params*->h_vlan_TCI from
+ *			the VLAN device and replace *params*->ifindex with the
+ *			parent's ifindex. *params*->h_vlan_TCI carries the VID
+ *			only, with PCP and DEI bits zero; a consumer wanting to
+ *			set egress priority writes PCP itself. *params*->smac is
+ *			the VLAN device's own address, which can differ from the
+ *			parent's. Only the immediate parent is resolved; if it
+ *			is itself a VLAN device (QinQ) or in another namespace,
+ *			the egress cannot be reduced to a physical device plus
+ *			one tag and the lookup returns
+ *			**BPF_FIB_LKUP_RET_VLAN_FAILURE** with *params*->ifindex
+ *			left at the input. To obtain the VLAN device's own
+ *			ifindex, repeat the lookup without
+ *			**BPF_FIB_LOOKUP_VLAN**, re-initializing *params*
+ *			first: output fields overwrite the inputs they share
+ *			storage with. The swap and the vlan fields
+ *			are written only on success; other output fields keep
+ *			the helper's existing behaviour, so a frag-needed result
+ *			still reports the route mtu in *params*->mtu_result.
+ *			This flag is only valid for XDP programs; tc programs
+ *			receive -EINVAL since they can redirect to the VLAN
+ *			device directly.
+ *		**BPF_FIB_LOOKUP_VLAN_INPUT**
+ *			Treat *params*->h_vlan_proto and *params*->h_vlan_TCI
+ *			as an input VLAN tag and run the lookup as if ingress
+ *			had happened on the VLAN subinterface carrying that tag
+ *			on *params*->ifindex. The VID is the low 12 bits of
+ *			*params*->h_vlan_TCI; *params*->h_vlan_proto must be
+ *			ETH_P_8021Q or ETH_P_8021AD in network byte order, else
+ *			**-EINVAL**. If *params*->ifindex is itself a VLAN
+ *			device, its inner (QinQ) subinterface is matched; for a
+ *			bond or team, pass the master's ifindex. An unmatched
+ *			tag, a down device, or one in another namespace returns
+ *			**BPF_FIB_LKUP_RET_NOT_FWDED**, mirroring real ingress.
+ *			A VID of 0 is looked up literally, so do not set this
+ *			flag for priority-tagged frames. Cannot be combined with
+ *			**BPF_FIB_LOOKUP_TBID** or **BPF_FIB_LOOKUP_OUTPUT**
+ *			(returns **-EINVAL**).
  *
  *		*ctx* is either **struct xdp_md** for XDP programs or
  *		**struct sk_buff** tc cls_act programs.
@@ -7327,6 +7368,8 @@ enum {
 	BPF_FIB_LOOKUP_TBID    = (1U << 3),
 	BPF_FIB_LOOKUP_SRC     = (1U << 4),
 	BPF_FIB_LOOKUP_MARK    = (1U << 5),
+	BPF_FIB_LOOKUP_VLAN    = (1U << 6),
+	BPF_FIB_LOOKUP_VLAN_INPUT = (1U << 7),
 };
 
 enum {
@@ -7340,6 +7383,7 @@ enum {
 	BPF_FIB_LKUP_RET_NO_NEIGH,     /* no neighbor entry for nh */
 	BPF_FIB_LKUP_RET_FRAG_NEEDED,  /* fragmentation required to fwd */
 	BPF_FIB_LKUP_RET_NO_SRC_ADDR,  /* failed to derive IP src addr */
+	BPF_FIB_LKUP_RET_VLAN_FAILURE, /* VLAN egress, parent unresolvable */
 };
 
 struct bpf_fib_lookup {
@@ -7393,7 +7437,13 @@ struct bpf_fib_lookup {
 
 	union {
 		struct {
-			/* output */
+			/*
+			 * output with BPF_FIB_LOOKUP_VLAN: set from the
+			 * resolved egress VLAN device (see the flag); zeroed
+			 * on other successful lookups. input with
+			 * BPF_FIB_LOOKUP_VLAN_INPUT: the VLAN tag to scope
+			 * the lookup by.
+			 */
 			__be16	h_vlan_proto;
 			__be16	h_vlan_TCI;
 		};
diff --git a/tools/testing/selftests/bpf/prog_tests/fib_lookup.c b/tools/testing/selftests/bpf/prog_tests/fib_lookup.c
index bd76589580041f..f7361f9a34592f 100644
--- a/tools/testing/selftests/bpf/prog_tests/fib_lookup.c
+++ b/tools/testing/selftests/bpf/prog_tests/fib_lookup.c
@@ -2,6 +2,7 @@
 /* Copyright (c) 2023 Meta Platforms, Inc. and affiliates. */
 
 #include <linux/rtnetlink.h>
+#include <linux/if_ether.h>
 #include <sys/types.h>
 #include <net/if.h>
 
@@ -23,6 +24,7 @@
 #define IPV4_TBID_ADDR		"172.0.0.254"
 #define IPV4_TBID_NET		"172.0.0.0"
 #define IPV4_TBID_DST		"172.0.0.2"
+#define IPV4_TBID_NONEIGH_DST	"172.0.0.5"
 #define IPV6_TBID_ADDR		"fd00::FFFF"
 #define IPV6_TBID_NET		"fd00::"
 #define IPV6_TBID_DST		"fd00::2"
@@ -37,6 +39,41 @@
 #define IPV6_LOCAL		"fd01::3"
 #define IPV6_GW1		"fd01::1"
 #define IPV6_GW2		"fd01::2"
+#define VLAN_ID			100
+#define VLAN_IFACE		"veth1.100"
+#define VLAN_ID_DOWN		102
+#define VLAN_IFACE_DOWN		"veth1.102"
+#define QINQ_OUTER_IFACE	"veth1.200"
+#define QINQ_INNER_IFACE	"veth1.200.300"
+#define VLAN_TABLE		"300"
+#define IPV4_VLAN_IFACE_ADDR	"10.5.0.254"
+#define IPV4_VLAN_EGRESS_DST	"10.5.0.2"
+#define IPV4_QINQ_DST		"10.7.0.2"
+#define IPV4_VLAN_DST		"10.6.0.2"
+#define IPV4_VLAN_GW		"10.5.0.1"
+#define IPV6_VLAN_IFACE_ADDR	"fd02::254"
+#define IPV6_VLAN_EGRESS_DST	"fd02::2"
+#define IPV6_VLAN_DST		"fd03::2"
+#define IPV6_VLAN_GW		"fd02::1"
+#define VLAN_VID_UNUSED		999
+#define VRF_IFACE		"vrf-blue"
+#define VRF_TABLE		"1000"
+#define VRF_VLAN_ID		101
+#define VRF_VLAN_IFACE		"veth1.101"
+#define IPV4_VRF_IFACE_ADDR	"10.8.0.254"
+#define IPV4_VRF_GW		"10.8.0.1"
+#define IPV4_VRF_DST		"10.9.0.2"
+#define TBID_VLAN_ID		50
+#define TBID_VLAN_IFACE		"veth2.50"
+#define IPV4_TBID_VLAN_DST	"172.2.0.2"
+#define IPV4_BOND_VLAN_DST	"10.11.0.2"
+#define IPV4_VLAN_MTU_DST	"10.5.9.2"
+#define QINQ_AD_VLAN_ID		200
+#define QINQ_INNER_VLAN_ID	300
+#define BOND_IFACE		"bond99"
+#define BOND_PORT		"veth3"
+#define BOND_PORT_PEER		"veth4"
+#define BOND_VLAN_ID		500
 #define DMAC			"11:11:11:11:11:11"
 #define DMAC_INIT { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, }
 #define DMAC2			"01:01:01:01:01:01"
@@ -52,6 +89,17 @@ struct fib_lookup_test {
 	__u32 tbid;
 	__u8 dmac[6];
 	__u32 mark;
+	/*
+	 * input tag with BPF_FIB_LOOKUP_VLAN_INPUT; expected output tag
+	 * with BPF_FIB_LOOKUP_VLAN (checked when check_vlan is set)
+	 */
+	__u16 vlan_proto;
+	__u16 vlan_id;
+	bool check_vlan;
+	const char *expected_dev; /* expected params->ifindex after lookup */
+	const char *iif;	  /* override the default veth1 input device */
+	__u16 tot_len;		  /* triggers the in-lookup mtu check when set */
+	__u16 expected_mtu;	  /* expected mtu_result (union with tot_len) */
 };
 
 static const struct fib_lookup_test tests[] = {
@@ -79,6 +127,17 @@ static const struct fib_lookup_test tests[] = {
 	  .daddr = IPV4_TBID_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
 	  .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID, .tbid = 100,
 	  .dmac = DMAC_INIT2, },
+	/*
+	 * An error that returns after the egress device is resolved must
+	 * report the egress ifindex, not the input. This routes from input
+	 * veth1 via veth2 (table 100) to a dst with no neighbour, so
+	 * input != egress, pinning NO_NEIGH to the egress device.
+	 */
+	{ .desc = "IPv4 NO_NEIGH reports the egress ifindex, not the input",
+	  .daddr = IPV4_TBID_NONEIGH_DST,
+	  .expected_ret = BPF_FIB_LKUP_RET_NO_NEIGH,
+	  .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID, .tbid = 100,
+	  .expected_dev = "veth2", },
 	{ .desc = "IPv6 TBID lookup failure",
 	  .daddr = IPV6_TBID_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
 	  .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID,
@@ -142,6 +201,218 @@ static const struct fib_lookup_test tests[] = {
 	  .expected_dst = IPV6_GW1,
 	  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,
 	  .mark = MARK, },
+	/* vlan egress resolution */
+	/*
+	 * Invariant the VLAN-egress arms jointly enforce: a
+	 * BPF_FIB_LOOKUP_VLAN SUCCESS always carries a physical,
+	 * xmit-capable ifindex; no SUCCESS ever returns a VLAN-device
+	 * ifindex. Reducible arms pin ifindex == the physical parent; the
+	 * QinQ and foreign-netns arms pin VLAN_FAILURE with params->ifindex
+	 * left at the input, so a regression to best-effort (SUCCESS + the
+	 * VLAN ifindex) fails one.
+	 */
+	{ .desc = "IPv4 VLAN egress, no flag",
+	  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = VLAN_IFACE, .check_vlan = true, },
+	{ .desc = "IPv4 VLAN egress, single VLAN",
+	  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	/*
+	 * skb path without tot_len: mtu_result is the VLAN device's mtu
+	 * (1400), not the parent's (1500)
+	 */
+	{ .desc = "IPv4 VLAN egress, skb-path mtu is the VLAN device's without the flag",
+	  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = VLAN_IFACE, .check_vlan = true, .expected_mtu = 1400, },
+	{ .desc = "IPv4 VLAN egress, flag set but egress is not a VLAN",
+	  .daddr = IPV4_NUD_FAILED_ADDR, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = "veth1", .check_vlan = true, },
+	{ .desc = "IPv4 VLAN egress, QinQ not reducible (VLAN_FAILURE)",
+	  .daddr = IPV4_QINQ_DST,
+	  .expected_ret = BPF_FIB_LKUP_RET_VLAN_FAILURE,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = "veth1", .check_vlan = true, },
+	{ .desc = "IPv4 QinQ egress without the flag (escape hatch)",
+	  .daddr = IPV4_QINQ_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = QINQ_INNER_IFACE, },
+	{ .desc = "IPv6 VLAN egress, single VLAN",
+	  .daddr = IPV6_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv4 VLAN egress, neighbour on the VLAN device",
+	  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, .dmac = DMAC_INIT, },
+	{ .desc = "IPv4 VLAN egress in OUTPUT mode",
+	  .daddr = IPV4_VLAN_EGRESS_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .iif = VLAN_IFACE,
+	  .lookup_flags = BPF_FIB_LOOKUP_OUTPUT | BPF_FIB_LOOKUP_VLAN |
+			  BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv4 VLAN egress over a bond",
+	  .daddr = IPV4_BOND_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = BOND_IFACE, .check_vlan = true,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },
+	{ .desc = "IPv4 VLAN egress via TBID table",
+	  .daddr = IPV4_TBID_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .lookup_flags = BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_TBID |
+			  BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .tbid = 100,
+	  .expected_dev = "veth2", .check_vlan = true,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = TBID_VLAN_ID, },
+	{ .desc = "IPv4 VLAN egress, success writes mtu_result with the swap",
+	  .daddr = IPV4_VLAN_MTU_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .tot_len = 500, .expected_mtu = 1000,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv4 VLAN egress, FRAG_NEEDED reports mtu, swap unwritten",
+	  .daddr = IPV4_VLAN_MTU_DST, .expected_ret = BPF_FIB_LKUP_RET_FRAG_NEEDED,
+	  .tot_len = 1400, .expected_mtu = 1000,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .expected_dev = "veth1", .check_vlan = true, },
+	/* vlan tag as lookup input */
+	{ .desc = "IPv4 VLAN input, no flag",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV4_GW1,
+	  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH, },
+	{ .desc = "IPv4 VLAN input, tag selects subinterface route",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV4_VLAN_GW, .expected_dev = VLAN_IFACE,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv6 VLAN input, tag selects subinterface route",
+	  .daddr = IPV6_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV6_VLAN_GW, .expected_dev = VLAN_IFACE,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv4 VLAN input and egress combined",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV4_VLAN_GW, .expected_dev = "veth1",
+	  .check_vlan = true,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_VLAN |
+			  BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv4 VLAN input, neighbour resolved on the route",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV4_VLAN_GW, .expected_dev = VLAN_IFACE,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, .dmac = DMAC_INIT2, },
+	{ .desc = "IPv4 VLAN input, source address from the subinterface",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_src = IPV4_VLAN_IFACE_ADDR,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SRC |
+			  BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	/*
+	 * VRF: the resolved subinterface is enslaved, so the l3mdev rule
+	 * (full lookup) and l3mdev_fib_table_rcu() (DIRECT) must select
+	 * the VRF table from the resolved ingress
+	 */
+	{ .desc = "IPv4 VLAN input, VRF subinterface, no flag",
+	  .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV4_GW1,
+	  .lookup_flags = BPF_FIB_LOOKUP_SKIP_NEIGH, },
+	{ .desc = "IPv4 VLAN input, tag selects VRF table",
+	  .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV4_VRF_GW, .expected_dev = VRF_VLAN_IFACE,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VRF_VLAN_ID, },
+	{ .desc = "IPv4 VLAN input, DIRECT uses VRF table from resolved ingress",
+	  .daddr = IPV4_VRF_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV4_VRF_GW, .expected_dev = VRF_VLAN_IFACE,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_DIRECT |
+			  BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VRF_VLAN_ID, },
+	/*
+	 * failure arms also assert params is left untouched: ifindex still
+	 * names the physical device and the input tag bytes survive
+	 */
+	{ .desc = "IPv4 VLAN input, invalid proto",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = 0x1234, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv4 VLAN input, unmatched VID",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_VID_UNUSED, },
+	{ .desc = "IPv4 VLAN input, subinterface down",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID_DOWN, },
+	/*
+	 * the resolver runs before the forwarding check, so on devices
+	 * with forwarding off FWD_DISABLED (not NOT_FWDED) proves the tag
+	 * resolved to that device and the lookup used it as ingress
+	 */
+	{ .desc = "IPv4 VLAN input, 802.1ad tag",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021AD, .vlan_id = QINQ_AD_VLAN_ID, },
+	{ .desc = "IPv4 VLAN input, PCP and DEI bits ignored in TCI",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_SUCCESS,
+	  .expected_dst = IPV4_VLAN_GW,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = 0xe000 | VLAN_ID, },
+	{ .desc = "IPv4 VLAN input, inner QinQ device from VLAN ifindex",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,
+	  .iif = QINQ_OUTER_IFACE,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = QINQ_INNER_VLAN_ID, },
+	/*
+	 * bonding: the VLANs live on the master, as on receive, where the
+	 * frame is steered to the master before VLAN processing; a port
+	 * ifindex does not match (ports carry vid state but no VLAN devs)
+	 */
+	{ .desc = "IPv4 VLAN input, tag on bond master resolves",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_FWD_DISABLED,
+	  .iif = BOND_IFACE,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },
+	{ .desc = "IPv4 VLAN input, tag on bond port does not match",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+	  .iif = BOND_PORT, .expected_dev = BOND_PORT, .check_vlan = true,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = BOND_VLAN_ID, },
+	{ .desc = "IPv6 VLAN input, invalid proto",
+	  .daddr = IPV6_VLAN_DST, .expected_ret = -EINVAL,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = 0x1234, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv4 VLAN input, VID 0 priority tag fails closed",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = 0, },
+	{ .desc = "IPv6 VLAN input, unmatched VID",
+	  .daddr = IPV6_VLAN_DST, .expected_ret = BPF_FIB_LKUP_RET_NOT_FWDED,
+	  .expected_dev = "veth1", .check_vlan = true,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_SKIP_NEIGH,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_VID_UNUSED, },
+	{ .desc = "unknown flag bit rejected",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,
+	  .lookup_flags = (1 << 14) | BPF_FIB_LOOKUP_SKIP_NEIGH, },
+	{ .desc = "IPv4 VLAN input rejected with TBID",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_TBID,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
+	{ .desc = "IPv4 VLAN input rejected with OUTPUT",
+	  .daddr = IPV4_VLAN_DST, .expected_ret = -EINVAL,
+	  .lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT | BPF_FIB_LOOKUP_OUTPUT,
+	  .vlan_proto = ETH_P_8021Q, .vlan_id = VLAN_ID, },
 };
 
 static int setup_netns(void)
@@ -204,6 +475,105 @@ static int setup_netns(void)
 	SYS(fail, "ip rule add prio 2 fwmark %d lookup %s", MARK, MARK_TABLE);
 	SYS(fail, "ip -6 rule add prio 2 fwmark %d lookup %s", MARK, MARK_TABLE);
 
+	/*
+	 * Setup for vlan tests: a subinterface for egress resolution and
+	 * tag-as-input, a QinQ stack, and an iif rule so the input tests
+	 * observe which device the lookup used as ingress.
+	 */
+	SYS(fail, "ip link add link veth1 name %s type vlan id %d",
+	    VLAN_IFACE, VLAN_ID);
+	SYS(fail, "ip link set dev %s up", VLAN_IFACE);
+	/*
+	 * lower than the veth1 parent (1500): the skb-path mtu check uses the
+	 * FIB result (VLAN) device, so mtu_result is this value, which the
+	 * no-flag arm below pins
+	 */
+	SYS(fail, "ip link set dev %s mtu 1400", VLAN_IFACE);
+	SYS(fail, "ip addr add %s/24 dev %s", IPV4_VLAN_IFACE_ADDR, VLAN_IFACE);
+	SYS(fail, "ip addr add %s/64 dev %s nodad", IPV6_VLAN_IFACE_ADDR, VLAN_IFACE);
+
+	/*
+	 * stays down: the input flag must treat its tag the way real
+	 * ingress treats a frame arriving on a down VLAN device (drop)
+	 */
+	SYS(fail, "ip link add link veth1 name %s type vlan id %d",
+	    VLAN_IFACE_DOWN, VLAN_ID_DOWN);
+
+	err = write_sysctl("/proc/sys/net/ipv4/conf/" VLAN_IFACE "/forwarding", "1");
+	if (!ASSERT_OK(err, "write_sysctl(net.ipv4.conf." VLAN_IFACE ".forwarding)"))
+		goto fail;
+
+	err = write_sysctl("/proc/sys/net/ipv6/conf/" VLAN_IFACE "/forwarding", "1");
+	if (!ASSERT_OK(err, "write_sysctl(net.ipv6.conf." VLAN_IFACE ".forwarding)"))
+		goto fail;
+
+	SYS(fail, "ip link add link veth1 name %s type vlan proto 802.1ad id 200",
+	    QINQ_OUTER_IFACE);
+	SYS(fail, "ip link add link %s name %s type vlan id 300",
+	    QINQ_OUTER_IFACE, QINQ_INNER_IFACE);
+	SYS(fail, "ip link set dev %s up", QINQ_OUTER_IFACE);
+	SYS(fail, "ip link set dev %s up", QINQ_INNER_IFACE);
+	SYS(fail, "ip route add %s/32 dev %s", IPV4_QINQ_DST, QINQ_INNER_IFACE);
+
+	SYS(fail, "ip route add %s/32 via %s", IPV4_VLAN_DST, IPV4_GW1);
+	SYS(fail, "ip route add table %s %s/32 via %s",
+	    VLAN_TABLE, IPV4_VLAN_DST, IPV4_VLAN_GW);
+	SYS(fail, "ip rule add prio 3 iif %s lookup %s", VLAN_IFACE, VLAN_TABLE);
+	SYS(fail, "ip -6 route add %s/128 via %s", IPV6_VLAN_DST, IPV6_GW1);
+	SYS(fail, "ip -6 route add table %s %s/128 via %s",
+	    VLAN_TABLE, IPV6_VLAN_DST, IPV6_VLAN_GW);
+	SYS(fail, "ip -6 rule add prio 3 iif %s lookup %s", VLAN_IFACE, VLAN_TABLE);
+
+	/* a bond with one port and a VLAN on the bond */
+	SYS(fail, "ip link add %s type bond", BOND_IFACE);
+	SYS(fail, "ip link add %s type veth peer name %s", BOND_PORT, BOND_PORT_PEER);
+	SYS(fail, "ip link set %s master %s", BOND_PORT, BOND_IFACE);
+	SYS(fail, "ip link set dev %s up", BOND_IFACE);
+	SYS(fail, "ip link set dev %s up", BOND_PORT);
+	SYS(fail, "ip link add link %s name %s.%d type vlan id %d",
+	    BOND_IFACE, BOND_IFACE, BOND_VLAN_ID, BOND_VLAN_ID);
+	SYS(fail, "ip link set dev %s.%d up", BOND_IFACE, BOND_VLAN_ID);
+	SYS(fail, "ip route add %s/32 dev %s.%d",
+	    IPV4_BOND_VLAN_DST, BOND_IFACE, BOND_VLAN_ID);
+
+	/*
+	 * a VRF with its own dedicated subinterface (the iif rules above
+	 * must not see it), for the table-selection-by-ingress cases
+	 */
+	SYS(fail, "ip link add %s type vrf table %s", VRF_IFACE, VRF_TABLE);
+	SYS(fail, "ip link set dev %s up", VRF_IFACE);
+	SYS(fail, "ip link add link veth1 name %s type vlan id %d",
+	    VRF_VLAN_IFACE, VRF_VLAN_ID);
+	SYS(fail, "ip link set %s master %s", VRF_VLAN_IFACE, VRF_IFACE);
+	SYS(fail, "ip link set dev %s up", VRF_VLAN_IFACE);
+	SYS(fail, "ip addr add %s/24 dev %s", IPV4_VRF_IFACE_ADDR, VRF_VLAN_IFACE);
+	err = write_sysctl("/proc/sys/net/ipv4/conf/" VRF_VLAN_IFACE "/forwarding", "1");
+	if (!ASSERT_OK(err, "write_sysctl(net.ipv4.conf." VRF_VLAN_IFACE ".forwarding)"))
+		goto fail;
+	SYS(fail, "ip route add %s/32 via %s", IPV4_VRF_DST, IPV4_GW1);
+	SYS(fail, "ip route add table %s %s/32 via %s",
+	    VRF_TABLE, IPV4_VRF_DST, IPV4_VRF_GW);
+
+	/* neighbours on the VLAN subinterface for the non-SKIP_NEIGH cases */
+	err = write_sysctl("/proc/sys/net/ipv4/neigh/" VLAN_IFACE "/gc_stale_time", "900");
+	if (!ASSERT_OK(err, "write_sysctl(net.ipv4.neigh." VLAN_IFACE ".gc_stale_time)"))
+		goto fail;
+	SYS(fail, "ip neigh add %s dev %s lladdr %s nud stale",
+	    IPV4_VLAN_EGRESS_DST, VLAN_IFACE, DMAC);
+	SYS(fail, "ip neigh add %s dev %s lladdr %s nud stale",
+	    IPV4_VLAN_GW, VLAN_IFACE, DMAC2);
+
+	/* a VLAN on veth2 with a route in the tbid test table */
+	SYS(fail, "ip link add link veth2 name %s type vlan id %d",
+	    TBID_VLAN_IFACE, TBID_VLAN_ID);
+	SYS(fail, "ip link set dev %s up", TBID_VLAN_IFACE);
+	SYS(fail, "ip route add table 100 %s/32 dev %s",
+	    IPV4_TBID_VLAN_DST, TBID_VLAN_IFACE);
+
+	/* a locked-mtu route via the subinterface for the FRAG_NEEDED case */
+	SYS(fail, "ip route add %s/32 dev %s mtu lock 1000",
+	    IPV4_VLAN_MTU_DST, VLAN_IFACE);
+
 	return 0;
 fail:
 	return -1;
@@ -218,9 +588,16 @@ static int set_lookup_params(struct bpf_fib_lookup *params,
 	memset(params, 0, sizeof(*params));
 
 	params->l4_protocol = IPPROTO_TCP;
-	params->ifindex = ifindex;
+	params->ifindex = test->iif ? if_nametoindex(test->iif) : ifindex;
 	params->tbid = test->tbid;
 	params->mark = test->mark;
+	params->tot_len = test->tot_len;
+
+	/* h_vlan_proto/h_vlan_TCI union with tbid */
+	if (test->lookup_flags & BPF_FIB_LOOKUP_VLAN_INPUT) {
+		params->h_vlan_proto = htons(test->vlan_proto);
+		params->h_vlan_TCI = htons(test->vlan_id);
+	}
 
 	if (inet_pton(AF_INET6, test->daddr, params->ipv6_dst) == 1) {
 		params->family = AF_INET6;
@@ -298,7 +675,7 @@ void test_fib_lookup(void)
 	struct nstoken *nstoken = NULL;
 	struct __sk_buff skb = { };
 	struct fib_lookup *skel;
-	int prog_fd, err, ret, i;
+	int prog_fd, xdp_fd, err, ret, i;
 
 	/* The test does not use the skb->data, so
 	 * use pkt_v6 for both v6 and v4 test.
@@ -309,11 +686,16 @@ void test_fib_lookup(void)
 		    .ctx_in = &skb,
 		    .ctx_size_in = sizeof(skb),
 	);
+	LIBBPF_OPTS(bpf_test_run_opts, xdp_opts,
+		    .data_in = &pkt_v6,
+		    .data_size_in = sizeof(pkt_v6),
+	);
 
 	skel = fib_lookup__open_and_load();
 	if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
 		return;
 	prog_fd = bpf_program__fd(skel->progs.fib_lookup);
+	xdp_fd = bpf_program__fd(skel->progs.fib_lookup_xdp);
 
 	SYS(fail, "ip netns add %s", NS_TEST);
 
@@ -343,6 +725,16 @@ void test_fib_lookup(void)
 		if (!ASSERT_OK(err, "bpf_prog_test_run_opts"))
 			continue;
 
+		/*
+		 * BPF_FIB_LOOKUP_VLAN is XDP-only; the tc helper rejects it.
+		 * These cases are exercised on the XDP path below.
+		 */
+		if (tests[i].lookup_flags & BPF_FIB_LOOKUP_VLAN) {
+			ASSERT_EQ(skel->bss->fib_lookup_ret, -EINVAL,
+				  "tc rejects BPF_FIB_LOOKUP_VLAN");
+			continue;
+		}
+
 		ASSERT_EQ(skel->bss->fib_lookup_ret, tests[i].expected_ret,
 			  "fib_lookup_ret");
 
@@ -352,6 +744,21 @@ void test_fib_lookup(void)
 		if (tests[i].expected_dst)
 			assert_dst_ip(fib_params, tests[i].expected_dst);
 
+		if (tests[i].expected_dev)
+			ASSERT_EQ(fib_params->ifindex,
+				  if_nametoindex(tests[i].expected_dev), "ifindex");
+
+		if (tests[i].expected_mtu)
+			ASSERT_EQ(fib_params->mtu_result, tests[i].expected_mtu,
+				  "mtu_result");
+
+		if (tests[i].check_vlan) {
+			ASSERT_EQ(fib_params->h_vlan_proto,
+				  htons(tests[i].vlan_proto), "h_vlan_proto");
+			ASSERT_EQ(fib_params->h_vlan_TCI,
+				  htons(tests[i].vlan_id), "h_vlan_TCI");
+		}
+
 		ret = memcmp(tests[i].dmac, fib_params->dmac, sizeof(tests[i].dmac));
 		if (!ASSERT_EQ(ret, 0, "dmac not match")) {
 			char expected[18], actual[18];
@@ -361,17 +768,322 @@ void test_fib_lookup(void)
 			printf("dmac expected %s actual %s ", expected, actual);
 		}
 
-		// ensure tbid is zero'd out after fib lookup.
-		if (tests[i].lookup_flags & BPF_FIB_LOOKUP_DIRECT) {
+		/*
+		 * ensure tbid is zero'd out after fib lookup. With
+		 * BPF_FIB_LOOKUP_VLAN the union holds the packed vlan
+		 * fields instead, so skip the check for those.
+		 */
+		if ((tests[i].lookup_flags & BPF_FIB_LOOKUP_DIRECT) &&
+		    !(tests[i].lookup_flags & BPF_FIB_LOOKUP_VLAN)) {
 			if (!ASSERT_EQ(skel->bss->fib_params.tbid, 0,
 					"expected fib_params.tbid to be zero"))
 				goto fail;
 		}
 	}
 
+	/*
+	 * Re-run the cases through bpf_xdp_fib_lookup(). test_run uses the
+	 * current netns' loopback for ctx->rxq->dev, so dev_net() is NS_TEST
+	 * and the lookup runs against its FIB. The path-independent results
+	 * (return code, swapped ifindex, vlan tag, gateway) must match the skb
+	 * path; the no-tot_len mtu_result is skb-specific and not rechecked.
+	 */
+	for (i = 0; i < ARRAY_SIZE(tests); i++) {
+		if (set_lookup_params(fib_params, &tests[i], skb.ifindex))
+			continue;
+
+		skel->bss->fib_lookup_ret = -1;
+		skel->bss->lookup_flags = tests[i].lookup_flags;
+
+		err = bpf_prog_test_run_opts(xdp_fd, &xdp_opts);
+		if (!ASSERT_OK(err, "xdp test_run"))
+			continue;
+
+		if (!ASSERT_EQ(skel->bss->fib_lookup_ret, tests[i].expected_ret,
+			       "xdp fib_lookup_ret"))
+			printf("(xdp) %s\n", tests[i].desc);
+
+		if (tests[i].expected_dev)
+			ASSERT_EQ(fib_params->ifindex,
+				  if_nametoindex(tests[i].expected_dev),
+				  "xdp ifindex");
+
+		if (tests[i].expected_dst)
+			assert_dst_ip(fib_params, tests[i].expected_dst);
+
+		if (tests[i].check_vlan) {
+			ASSERT_EQ(fib_params->h_vlan_proto,
+				  htons(tests[i].vlan_proto), "xdp h_vlan_proto");
+			ASSERT_EQ(fib_params->h_vlan_TCI,
+				  htons(tests[i].vlan_id), "xdp h_vlan_TCI");
+		}
+
+		ret = memcmp(tests[i].dmac, fib_params->dmac, sizeof(tests[i].dmac));
+		ASSERT_EQ(ret, 0, "xdp dmac");
+
+		/*
+		 * mtu_result from a tot_len lookup is the route mtu and is
+		 * path-independent; the no-tot_len arm reads dev->mtu and is
+		 * skb-only, so gate on tot_len
+		 */
+		if (tests[i].expected_mtu && tests[i].tot_len)
+			ASSERT_EQ(fib_params->mtu_result, tests[i].expected_mtu,
+				  "xdp mtu_result");
+	}
+
 fail:
 	if (nstoken)
 		close_netns(nstoken);
 	SYS_NOFAIL("ip netns del " NS_TEST);
 	fib_lookup__destroy(skel);
 }
+
+#define NS_VLAN_A	"fib_lookup_vlan_ns_a"
+#define NS_VLAN_B	"fib_lookup_vlan_ns_b"
+#define IPV4_VLAN_NETNS_ADDR	"10.66.0.1"
+#define IPV4_VLAN_NETNS_DST	"10.66.0.2"
+
+/*
+ * A VLAN device can be moved to another netns while staying registered
+ * on its parent. Neither direction may then cross the boundary: the
+ * egress flag must not publish the foreign parent's ifindex, and the
+ * input flag must fail closed rather than use a foreign ingress.
+ */
+void test_fib_lookup_vlan_netns(void)
+{
+	struct bpf_fib_lookup *fib_params;
+	struct nstoken *nstoken = NULL;
+	struct __sk_buff skb = { };
+	struct fib_lookup *skel = NULL;
+	int prog_fd, xdp_fd, err, parent_idx, vlan_idx;
+
+	LIBBPF_OPTS(bpf_test_run_opts, run_opts,
+		    .data_in = &pkt_v6,
+		    .data_size_in = sizeof(pkt_v6),
+		    .ctx_in = &skb,
+		    .ctx_size_in = sizeof(skb),
+	);
+	LIBBPF_OPTS(bpf_test_run_opts, xdp_opts,
+		    .data_in = &pkt_v6,
+		    .data_size_in = sizeof(pkt_v6),
+	);
+
+	skel = fib_lookup__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
+		return;
+	prog_fd = bpf_program__fd(skel->progs.fib_lookup);
+	xdp_fd = bpf_program__fd(skel->progs.fib_lookup_xdp);
+	fib_params = &skel->bss->fib_params;
+
+	SYS(fail, "ip netns add %s", NS_VLAN_A);
+	SYS(fail, "ip netns add %s", NS_VLAN_B);
+
+	nstoken = open_netns(NS_VLAN_A);
+	if (!ASSERT_OK_PTR(nstoken, "open_netns(a)"))
+		goto fail;
+
+	SYS(fail, "ip link add veth7 type veth peer name veth8");
+	SYS(fail, "ip link set dev veth7 up");
+	SYS(fail, "ip link add link veth7 name veth7.66 type vlan id 66");
+	SYS(fail, "ip link set veth7.66 netns %s", NS_VLAN_B);
+	/*
+	 * up it in B before the input lookup: the move closed it, and a
+	 * down device fails the resolver on IFF_UP before reaching the
+	 * netns check this subtest exists to pin
+	 */
+	SYS(fail, "ip -n %s link set dev veth7.66 up", NS_VLAN_B);
+
+	parent_idx = if_nametoindex("veth7");
+	if (!ASSERT_NEQ(parent_idx, 0, "if_nametoindex(veth7)"))
+		goto fail;
+
+	/*
+	 * input: the moved device is still in veth7's VLAN group, but it
+	 * lives in another netns, so the lookup must fail closed
+	 */
+	skb.ifindex = parent_idx;
+	memset(fib_params, 0, sizeof(*fib_params));
+	fib_params->family = AF_INET;
+	fib_params->l4_protocol = IPPROTO_TCP;
+	fib_params->ifindex = parent_idx;
+	fib_params->h_vlan_proto = htons(ETH_P_8021Q);
+	fib_params->h_vlan_TCI = htons(66);
+	if (!ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_NETNS_DST, &fib_params->ipv4_dst),
+		       1, "inet_pton(dst)"))
+		goto fail;
+
+	skel->bss->fib_lookup_ret = -1;
+	skel->bss->lookup_flags = BPF_FIB_LOOKUP_VLAN_INPUT |
+				  BPF_FIB_LOOKUP_SKIP_NEIGH;
+	err = bpf_prog_test_run_opts(prog_fd, &run_opts);
+	if (!ASSERT_OK(err, "test_run(input)"))
+		goto fail;
+	ASSERT_EQ(skel->bss->fib_lookup_ret, BPF_FIB_LKUP_RET_NOT_FWDED,
+		  "input across netns fails closed");
+	ASSERT_EQ(fib_params->ifindex, parent_idx, "ifindex untouched");
+	ASSERT_EQ(fib_params->h_vlan_TCI, htons(66), "tag untouched");
+
+	close_netns(nstoken);
+	nstoken = open_netns(NS_VLAN_B);
+	if (!ASSERT_OK_PTR(nstoken, "open_netns(b)"))
+		goto fail;
+
+	/*
+	 * egress: the fib result is the VLAN device here, but its parent
+	 * is in the other netns, so the swap must not happen
+	 */
+	SYS(fail, "ip addr add %s/24 dev veth7.66", IPV4_VLAN_NETNS_ADDR);
+	err = write_sysctl("/proc/sys/net/ipv4/conf/veth7.66/forwarding", "1");
+	if (!ASSERT_OK(err, "write_sysctl(forwarding)"))
+		goto fail;
+
+	vlan_idx = if_nametoindex("veth7.66");
+	if (!ASSERT_NEQ(vlan_idx, 0, "if_nametoindex(veth7.66)"))
+		goto fail;
+
+	memset(fib_params, 0, sizeof(*fib_params));
+	fib_params->family = AF_INET;
+	fib_params->l4_protocol = IPPROTO_TCP;
+	fib_params->ifindex = vlan_idx;
+	if (!ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_NETNS_DST, &fib_params->ipv4_dst),
+		       1, "inet_pton(dst)") ||
+	    !ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_NETNS_ADDR, &fib_params->ipv4_src),
+		       1, "inet_pton(src)"))
+		goto fail;
+
+	skel->bss->fib_lookup_ret = -1;
+	skel->bss->lookup_flags = BPF_FIB_LOOKUP_VLAN |
+				  BPF_FIB_LOOKUP_SKIP_NEIGH;
+	err = bpf_prog_test_run_opts(xdp_fd, &xdp_opts);
+	if (!ASSERT_OK(err, "test_run(egress)"))
+		goto fail;
+	ASSERT_EQ(skel->bss->fib_lookup_ret, BPF_FIB_LKUP_RET_VLAN_FAILURE,
+		  "egress returns VLAN_FAILURE");
+	ASSERT_EQ(fib_params->ifindex, vlan_idx,
+		  "foreign parent not published");
+	ASSERT_EQ(fib_params->h_vlan_TCI, 0, "vlan fields zero");
+
+fail:
+	if (nstoken)
+		close_netns(nstoken);
+	SYS_NOFAIL("ip netns del " NS_VLAN_A);
+	SYS_NOFAIL("ip netns del " NS_VLAN_B);
+	fib_lookup__destroy(skel);
+}
+
+#define REDIRECT_NPKTS 1000
+#define NS_REDIRECT "fib_lookup_redirect_ns"
+
+/*
+ * The egress flag exists so an XDP program can redirect to the physical
+ * parent. A redirect that lands on a VLAN device is dropped at
+ * xdp_do_flush(), because a VLAN device has no ndo_xdp_xmit. Drive real
+ * frames with BPF_F_TEST_XDP_LIVE_FRAMES, which runs the native
+ * xdp_do_redirect() + xdp_do_flush() path: a reducible VLAN egress
+ * resolves to veth1 and is delivered to its peer veth2, while a QinQ
+ * egress returns VLAN_FAILURE and is passed to the stack instead of
+ * redirected to a device that would silently drop it.
+ */
+void test_fib_lookup_vlan_redirect(void)
+{
+	int redirect_fd, err, veth1_idx, veth2_idx = -1;
+	struct bpf_fib_lookup *fib_params;
+	struct nstoken *nstoken = NULL;
+	struct fib_lookup *skel = NULL;
+	bool xdp_attached = false;
+
+	LIBBPF_OPTS(bpf_test_run_opts, lf_opts,
+		    .data_in = &pkt_v4,
+		    .data_size_in = sizeof(pkt_v4),
+		    .flags = BPF_F_TEST_XDP_LIVE_FRAMES,
+		    .repeat = REDIRECT_NPKTS,
+	);
+
+	skel = fib_lookup__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
+		return;
+	redirect_fd = bpf_program__fd(skel->progs.fib_lookup_redirect);
+	fib_params = &skel->bss->fib_params;
+
+	SYS(fail, "ip netns add %s", NS_REDIRECT);
+	nstoken = open_netns(NS_REDIRECT);
+	if (!ASSERT_OK_PTR(nstoken, "open_netns"))
+		goto fail;
+	if (setup_netns())
+		goto fail;
+
+	veth1_idx = if_nametoindex("veth1");
+	veth2_idx = if_nametoindex("veth2");
+	if (!ASSERT_NEQ(veth1_idx, 0, "if_nametoindex(veth1)") ||
+	    !ASSERT_NEQ(veth2_idx, 0, "if_nametoindex(veth2)"))
+		goto fail;
+
+	/*
+	 * A redirect to veth1 is delivered to its peer veth2. veth_xdp_xmit()
+	 * only accepts the frame if veth2's NAPI is up, which on veth means
+	 * veth2 carries an XDP program; xdp_count tallies what arrives.
+	 */
+	err = bpf_xdp_attach(veth2_idx, bpf_program__fd(skel->progs.xdp_count),
+			     XDP_FLAGS_DRV_MODE, NULL);
+	if (!ASSERT_OK(err, "attach xdp_count on veth2"))
+		goto fail;
+	xdp_attached = true;
+
+	/* reducible VLAN egress: resolves to the physical parent veth1 */
+	memset(fib_params, 0, sizeof(*fib_params));
+	fib_params->family = AF_INET;
+	fib_params->l4_protocol = IPPROTO_TCP;
+	fib_params->ifindex = veth1_idx;
+	if (!ASSERT_EQ(inet_pton(AF_INET, IPV4_IFACE_ADDR, &fib_params->ipv4_src),
+		       1, "inet_pton(src)") ||
+	    !ASSERT_EQ(inet_pton(AF_INET, IPV4_VLAN_EGRESS_DST, &fib_params->ipv4_dst),
+		       1, "inet_pton(reducible dst)"))
+		goto fail;
+	skel->bss->lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH;
+	skel->bss->redirected = 0;
+	skel->bss->passed = 0;
+	skel->bss->delivered = 0;
+
+	err = bpf_prog_test_run_opts(redirect_fd, &lf_opts);
+	if (!ASSERT_OK(err, "test_run(reducible egress)"))
+		goto fail;
+	ASSERT_EQ(skel->bss->redirected, REDIRECT_NPKTS, "reducible egress redirected");
+	ASSERT_EQ(skel->bss->passed, 0, "reducible egress not passed");
+	ASSERT_GT(skel->bss->delivered, 0, "reducible egress delivered to veth2");
+
+	/*
+	 * QinQ egress: not reducible, so the lookup returns VLAN_FAILURE and
+	 * the program passes the frame instead of redirecting to the inner
+	 * VLAN device. redirected == 0 is the assertion that matters: the
+	 * program did not redirect to a device that would drop the frame at
+	 * xdp_do_flush(). veth2's delivered count is not checked here, since
+	 * a passed frame can still reach veth2 through the stack's forwarding
+	 * path, which is unrelated to the redirect under test.
+	 */
+	memset(fib_params, 0, sizeof(*fib_params));
+	fib_params->family = AF_INET;
+	fib_params->l4_protocol = IPPROTO_TCP;
+	fib_params->ifindex = veth1_idx;
+	if (!ASSERT_EQ(inet_pton(AF_INET, IPV4_IFACE_ADDR, &fib_params->ipv4_src),
+		       1, "inet_pton(src)") ||
+	    !ASSERT_EQ(inet_pton(AF_INET, IPV4_QINQ_DST, &fib_params->ipv4_dst),
+		       1, "inet_pton(qinq dst)"))
+		goto fail;
+	skel->bss->lookup_flags = BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_SKIP_NEIGH;
+	skel->bss->redirected = 0;
+	skel->bss->passed = 0;
+
+	err = bpf_prog_test_run_opts(redirect_fd, &lf_opts);
+	if (!ASSERT_OK(err, "test_run(qinq egress)"))
+		goto fail;
+	ASSERT_EQ(skel->bss->passed, REDIRECT_NPKTS, "qinq egress passed");
+	ASSERT_EQ(skel->bss->redirected, 0, "qinq egress not redirected");
+
+fail:
+	if (xdp_attached)
+		bpf_xdp_detach(veth2_idx, XDP_FLAGS_DRV_MODE, NULL);
+	if (nstoken)
+		close_netns(nstoken);
+	SYS_NOFAIL("ip netns del " NS_REDIRECT);
+	fib_lookup__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/fib_lookup.c b/tools/testing/selftests/bpf/progs/fib_lookup.c
index 7b5dd2214ff46a..36b7218d9ae2ec 100644
--- a/tools/testing/selftests/bpf/progs/fib_lookup.c
+++ b/tools/testing/selftests/bpf/progs/fib_lookup.c
@@ -4,7 +4,11 @@
 #include <linux/types.h>
 #include <linux/bpf.h>
 #include <linux/pkt_cls.h>
+#include <linux/if_ether.h>
+#include <linux/ip.h>
+#include <linux/in.h>
 #include <bpf/bpf_helpers.h>
+#include <bpf/bpf_endian.h>
 
 struct bpf_fib_lookup fib_params = {};
 int fib_lookup_ret = 0;
@@ -19,4 +23,57 @@ int fib_lookup(struct __sk_buff *skb)
 	return TC_ACT_SHOT;
 }
 
+SEC("xdp")
+int fib_lookup_xdp(struct xdp_md *ctx)
+{
+	fib_lookup_ret = bpf_fib_lookup(ctx, &fib_params, sizeof(fib_params),
+					lookup_flags);
+
+	return XDP_DROP;
+}
+
+int redirected = 0;
+int passed = 0;
+int delivered = 0;
+
+SEC("xdp")
+int fib_lookup_redirect(struct xdp_md *ctx)
+{
+	struct bpf_fib_lookup params = fib_params;
+	long ret;
+
+	ret = bpf_fib_lookup(ctx, &params, sizeof(params), lookup_flags);
+	if (ret == BPF_FIB_LKUP_RET_SUCCESS) {
+		redirected++;
+		return bpf_redirect(params.ifindex, 0);
+	}
+
+	passed++;
+	return XDP_PASS;
+}
+
+SEC("xdp")
+int xdp_count(struct xdp_md *ctx)
+{
+	void *data = (void *)(long)ctx->data;
+	void *data_end = (void *)(long)ctx->data_end;
+	struct ethhdr *eth = data;
+	struct iphdr *iph;
+
+	/*
+	 * count only the test's TCP frames: the netns has live
+	 * link-local traffic (DAD, MLD) that would satisfy a bare
+	 * counter
+	 */
+	if ((void *)(eth + 1) > data_end ||
+	    eth->h_proto != bpf_htons(ETH_P_IP))
+		return XDP_DROP;
+	iph = (void *)(eth + 1);
+	if ((void *)(iph + 1) > data_end || iph->protocol != IPPROTO_TCP)
+		return XDP_DROP;
+
+	delivered++;
+	return XDP_DROP;
+}
+
 char _license[] SEC("license") = "GPL";

3/2 2026/07/13 21:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 23675
							output: 37
							thoughts: 437
						

Thoughts:
**Analyzing the VLAN BPF FIB Lookup Patch**

Okay, so I've got this patch that introduces VLAN support to the `bpf_fib_lookup` helper. It looks like it's adding some new flags, `BPF_FIB_LOOKUP_VLAN` and `BPF_FIB_LOOKUP_VLAN_INPUT`. This is a pretty significant functional change within the kernel's networking/BPF subsystem, so fuzzing this is going to be crucial.

My initial focus will be on the modified and newly introduced functions. The most immediate targets that jump out are:

*   `bpf_fib_set_fwd_params`: This one's likely to be core to the forwarding logic.
*   `bpf_fib_vlan_input_dev`: Being a new function dedicated to VLAN input device handling, it's definitely going to be a prime target for scrutiny.
*   `bpf_ipv4_fib_lookup`, and `bpf_ipv6_fib_lookup`: These are hot paths in the FIB lookup, so the modifications are important.
*   `bpf_xdp_fib_lookup` and `bpf_skb_fib_lookup`: These are key BPF helpers, so they will be important to analyze.

I'll also need to make sure I've got the right kernel configuration. The code relies on `IS_ENABLED(CONFIG_VLAN_8021Q)`, so that's the key one to enable. I'll need to double-check that `CONFIG_INET` and `CONFIG_IPV6` are also configured, but those are usually standard. I'll also check `net/core/filter.c` to verify the configuration requirement.



4/2 2026/07/13 21:56 tool grepper
0m Args:
{
  "Expression": "CONFIG_VLAN_8021Q",
  "PathPrefix": "net/core/filter.c"
}

Results:
{
  "Output": "net/core/filter.c=6204=static int bpf_fib_set_fwd_params(struct net_device *dev,\n--\nnet/core/filter.c-6210-\nnet/core/filter.c:6211:#if IS_ENABLED(CONFIG_VLAN_8021Q)\nnet/core/filter.c-6212-\tif ((flags \u0026 BPF_FIB_LOOKUP_VLAN) \u0026\u0026 is_vlan_dev(dev)) {\n"
}

5/2 2026/07/13 21:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24662
							output: 38
							thoughts: 54
						

Thoughts:
**Analyzing the Network Filter Patch**

Okay, so the primary requirement here seems to be support for `VLAN_8021Q`. I need to make sure I understand the scope of the change. Before I proceed, are there any other configuration parameters I need to consider besides this? I've seen a patch applied to `net/core/filter.c`, which is interesting. I'll need to drill down into the function `bpf_fib_vlan_input_dev` to see exactly how this VLAN handling is being implemented and how it integrates with the existing BPF infrastructure. I need to understand this deeply to gauge the effectiveness and potential impact of this change.



6/2 2026/07/13 21:56 tool grepper
0m Args:
{
  "Expression": "bpf_fib_vlan_input_dev",
  "PathPrefix": "net/core/filter.c"
}

Results:
{
  "Output": "net/core/filter.c=6204=static int bpf_fib_set_fwd_params(struct net_device *dev,\n--\nnet/core/filter.c-6232-\nnet/core/filter.c:6233:static struct net_device *bpf_fib_vlan_input_dev(struct net_device *dev,\nnet/core/filter.c-6234-\t\t\t\t\t\t const struct bpf_fib_lookup *params)\n--\nnet/core/filter.c=6254=static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n--\nnet/core/filter.c-6271-\tif (flags \u0026 BPF_FIB_LOOKUP_VLAN_INPUT) {\nnet/core/filter.c:6272:\t\tdev = bpf_fib_vlan_input_dev(dev, params);\nnet/core/filter.c-6273-\t\tif (IS_ERR(dev))\n--\nnet/core/filter.c=6406=static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n--\nnet/core/filter.c-6429-\tif (flags \u0026 BPF_FIB_LOOKUP_VLAN_INPUT) {\nnet/core/filter.c:6430:\t\tdev = bpf_fib_vlan_input_dev(dev, params);\nnet/core/filter.c-6431-\t\tif (IS_ERR(dev))\n"
}

7/2 2026/07/13 21:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 25295
							output: 32
							thoughts: 255
						

Thoughts:
**Analyzing the BPF FIB Patch**

Okay, so I'm looking at this BPF FIB patch and immediately my brain is zeroing in on the key functions being touched. Specifically, the ones directly related to VLAN and forwarding: `bpf_fib_vlan_input_dev`, `bpf_fib_set_fwd_params`, and the lookup functions, `bpf_xdp_fib_lookup` and `bpf_skb_fib_lookup`. Of course, we also have the ipv4 and ipv6 lookup variations getting tweaked, `bpf_ipv4_fib_lookup` and `bpf_ipv6_fib_lookup`. That's the core of the forwarding logic being modified, so those are the functions to really dig into.

Now, let's see, the patch is also checking if we only have `VLAN_8021Q` configured. Right, it's leveraging a suite of VLAN-related helpers: `is_vlan_dev`, `vlan_dev_priv`, `vlan_dev_vlan_proto`, `vlan_dev_vlan_id`, and that RCU function, `__vlan_find_dev_deep_rcu`. Okay, the VLAN code should be fairly standard from here, but I wonder, are all of these dependent on `CONFIG_VLAN_8021Q` or potentially the module version, `CONFIG_VLAN_8021Q_MODULE`? We're going to want to make sure the VLAN support is either built-in or enabled. Let's make sure that's how this is structured. And that RCU helper? I seem to remember that being in `net/8021q/vlan_core.c` or something similar, right? Time to confirm that, and also make sure nothing unusual is being done with the VLAN-specific code paths.



8/2 2026/07/13 21:56 tool grepper
0m Args:
{
  "Expression": "__vlan_find_dev_deep_rcu",
  "PathPrefix": ""
}

Results:
{
  "Output": "drivers/infiniband/hw/bnxt_re/ib_verbs.c=4068=static bool bnxt_re_check_if_vlan_valid(struct bnxt_re_dev *rdev,\n--\ndrivers/infiniband/hw/bnxt_re/ib_verbs.c-4074-\t */\ndrivers/infiniband/hw/bnxt_re/ib_verbs.c:4075:\tif (!__vlan_find_dev_deep_rcu(rdev-\u003enetdev,\ndrivers/infiniband/hw/bnxt_re/ib_verbs.c-4076-\t\t\t\t      htons(ETH_P_8021Q), vlan_id))\n--\ndrivers/net/ethernet/chelsio/cxgb3/cxgb3_offload.c=176=static struct net_device *get_iff_from_mac(struct adapter *adapter,\n--\ndrivers/net/ethernet/chelsio/cxgb3/cxgb3_offload.c-187-\t\t\tif (vlan \u0026\u0026 vlan != VLAN_VID_MASK) {\ndrivers/net/ethernet/chelsio/cxgb3/cxgb3_offload.c:188:\t\t\t\tdev = __vlan_find_dev_deep_rcu(dev, htons(ETH_P_8021Q), vlan);\ndrivers/net/ethernet/chelsio/cxgb3/cxgb3_offload.c-189-\t\t\t} else if (netif_is_bond_slave(dev)) {\n--\ndrivers/net/ethernet/chelsio/cxgb4/clip_tbl.c=218=int cxgb4_update_root_dev_clip(struct net_device *dev)\n--\ndrivers/net/ethernet/chelsio/cxgb4/clip_tbl.c-236-\tfor (i = 0; i \u003c VLAN_N_VID; i++) {\ndrivers/net/ethernet/chelsio/cxgb4/clip_tbl.c:237:\t\troot_dev = __vlan_find_dev_deep_rcu(dev, htons(ETH_P_8021Q), i);\ndrivers/net/ethernet/chelsio/cxgb4/clip_tbl.c-238-\t\tif (!root_dev)\n--\ndrivers/net/ethernet/mellanox/mlxsw/spectrum_router.c=8743=static void mlxsw_sp_rif_destroy_vlan_upper(struct mlxsw_sp *mlxsw_sp,\n--\ndrivers/net/ethernet/mellanox/mlxsw/spectrum_router.c-8750-\trcu_read_lock();\ndrivers/net/ethernet/mellanox/mlxsw/spectrum_router.c:8751:\tupper_dev = __vlan_find_dev_deep_rcu(br_dev, htons(ETH_P_8021Q), vid);\ndrivers/net/ethernet/mellanox/mlxsw/spectrum_router.c-8752-\trcu_read_unlock();\n--\ndrivers/net/ethernet/mellanox/mlxsw/spectrum_router.c=8770=int mlxsw_sp_router_bridge_vlan_add(struct mlxsw_sp *mlxsw_sp,\n--\ndrivers/net/ethernet/mellanox/mlxsw/spectrum_router.c-8831-\t\trcu_read_lock();\ndrivers/net/ethernet/mellanox/mlxsw/spectrum_router.c:8832:\t\tupper_dev = __vlan_find_dev_deep_rcu(br_dev, htons(ETH_P_8021Q),\ndrivers/net/ethernet/mellanox/mlxsw/spectrum_router.c-8833-\t\t\t\t\t\t     old_pvid);\n--\ndrivers/net/ethernet/qlogic/qlcnic/qlcnic_main.c=4082=void qlcnic_restore_indev_addr(struct net_device *netdev, unsigned long event)\n--\ndrivers/net/ethernet/qlogic/qlcnic/qlcnic_main.c-4091-\tfor_each_set_bit(vid, adapter-\u003evlans, VLAN_N_VID) {\ndrivers/net/ethernet/qlogic/qlcnic/qlcnic_main.c:4092:\t\tdev = __vlan_find_dev_deep_rcu(netdev, htons(ETH_P_8021Q), vid);\ndrivers/net/ethernet/qlogic/qlcnic/qlcnic_main.c-4093-\t\tif (!dev)\n--\ndrivers/net/usb/cdc_mbim.c=309=static void do_neigh_solicit(struct usbnet *dev, u8 *buf, u16 tci)\n--\ndrivers/net/usb/cdc_mbim.c-326-\tif (tci) {\ndrivers/net/usb/cdc_mbim.c:327:\t\tnetdev = __vlan_find_dev_deep_rcu(dev-\u003enet, htons(ETH_P_8021Q),\ndrivers/net/usb/cdc_mbim.c-328-\t\t\t\t\t\t  tci);\n--\ninclude/linux/if_vlan.h=123=struct vlan_pcpu_stats {\n--\ninclude/linux/if_vlan.h-135-\ninclude/linux/if_vlan.h:136:extern struct net_device *__vlan_find_dev_deep_rcu(struct net_device *real_dev,\ninclude/linux/if_vlan.h-137-\t\t\t\t\t       __be16 vlan_proto, u16 vlan_id);\n--\ninclude/linux/if_vlan.h=252=static inline struct net_device *\ninclude/linux/if_vlan.h:253:__vlan_find_dev_deep_rcu(struct net_device *real_dev,\ninclude/linux/if_vlan.h-254-\t\t     __be16 vlan_proto, u16 vlan_id)\n--\nnet/8021q/vlan_core.c=10=bool vlan_do_receive(struct sk_buff **skbp)\n--\nnet/8021q/vlan_core.c-75-/* Must be invoked with rcu_read_lock. */\nnet/8021q/vlan_core.c:76:struct net_device *__vlan_find_dev_deep_rcu(struct net_device *dev,\nnet/8021q/vlan_core.c-77-\t\t\t\t\t__be16 vlan_proto, u16 vlan_id)\n--\nnet/8021q/vlan_core.c-93-\t\tif (upper_dev)\nnet/8021q/vlan_core.c:94:\t\t\treturn __vlan_find_dev_deep_rcu(upper_dev,\nnet/8021q/vlan_core.c-95-\t\t\t\t\t\t    vlan_proto, vlan_id);\n--\nnet/8021q/vlan_core.c-99-}\nnet/8021q/vlan_core.c:100:EXPORT_SYMBOL(__vlan_find_dev_deep_rcu);\nnet/8021q/vlan_core.c-101-\n--\nnet/bridge/br_arp_nd_proxy.c=124=void br_do_proxy_suppress_arp(struct sk_buff *skb, struct net_bridge *br,\n--\nnet/bridge/br_arp_nd_proxy.c-179-\tif (vid != 0) {\nnet/bridge/br_arp_nd_proxy.c:180:\t\tvlandev = __vlan_find_dev_deep_rcu(br-\u003edev, skb-\u003evlan_proto,\nnet/bridge/br_arp_nd_proxy.c-181-\t\t\t\t\t\t   vid);\n--\nnet/bridge/br_arp_nd_proxy.c=414=void br_do_suppress_nd(struct sk_buff *skb, struct net_bridge *br,\n--\nnet/bridge/br_arp_nd_proxy.c-455-\t\t/* build neigh table lookup on the vlan device */\nnet/bridge/br_arp_nd_proxy.c:456:\t\tvlandev = __vlan_find_dev_deep_rcu(br-\u003edev, skb-\u003evlan_proto,\nnet/bridge/br_arp_nd_proxy.c-457-\t\t\t\t\t\t   vid);\n--\nnet/bridge/br_netfilter_hooks.c=435=static struct net_device *brnf_get_logical_dev(struct sk_buff *skb,\n--\nnet/bridge/br_netfilter_hooks.c-446-\nnet/bridge/br_netfilter_hooks.c:447:\tvlan = __vlan_find_dev_deep_rcu(br, skb-\u003evlan_proto,\nnet/bridge/br_netfilter_hooks.c-448-\t\t\t\t    skb_vlan_tag_get(skb) \u0026 VLAN_VID_MASK);\n--\nnet/core/filter.c=6233=static struct net_device *bpf_fib_vlan_input_dev(struct net_device *dev,\n--\nnet/core/filter.c-6243-\tvid = ntohs(params-\u003eh_vlan_TCI) \u0026 VLAN_VID_MASK;\nnet/core/filter.c:6244:\tvlan_dev = __vlan_find_dev_deep_rcu(dev, proto, vid);\nnet/core/filter.c-6245-\tif (!vlan_dev || !(vlan_dev-\u003eflags \u0026 IFF_UP) ||\n--\nnet/dsa/tag.h=104=static inline void dsa_software_untag_vlan_unaware_bridge(struct sk_buff *skb,\n--\nnet/dsa/tag.h-131-\t */\nnet/dsa/tag.h:132:\tupper_dev = __vlan_find_dev_deep_rcu(br, htons(proto), vid);\nnet/dsa/tag.h-133-\tif (!upper_dev)\n"
}

9/2 2026/07/13 21:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 28572
							output: 37
							thoughts: 513
						

Thoughts:
**Analyzing the Dependencies of This BPF/VLAN Patch**

Okay, so `__vlan_find_dev_deep_rcu` is definitely available – that's a good start. My focus needs to be on the configuration requirements. The crucial one seems to be `VLAN_8021Q`, which translates to the configuration option `CONFIG_VLAN_8021Q`. I should probably double-check that this is the *only* necessary config related to VLAN.

The patch also touches `bpf_ipv4_fib_lookup` and `bpf_ipv6_fib_lookup`, which suggests I'll need `CONFIG_INET` and `CONFIG_IPV6` enabled. Nothing unexpected there. Of course, BPF requires `CONFIG_BPF_SYSCALL`, but that's a given in these sorts of patches. I'll need to confirm if there are any other configuration dependencies lurking about. The code uses `IS_ENABLED(CONFIG_VLAN_8021Q)`, so `VLAN_8021Q` is the main one, as expected.

Now, let's nail down the key functions this patch impacts:

*   `bpf_fib_vlan_input_dev`
*   `bpf_fib_set_fwd_params`
*   `bpf_xdp_fib_lookup`
*   `bpf_skb_fib_lookup`
*   `bpf_ipv4_fib_lookup`
*   `bpf_ipv6_fib_lookup`

Wait, a second…  `bpf_ipv4_fib_lookup` and `bpf_ipv6_fib_lookup` are hot paths, right? Yes, they're called by `bpf_xdp_fib_lookup` and `bpf_skb_fib_lookup`. And they're *specifically* modified here. They're definitely important. It looks like `bpf_fib_lookup_flags_ok` is also relevant. I need to make sure `bpf_fib_lookup_flags_ok` is also a function.

So, here's my revised list to focus on:

*   `bpf_fib_vlan_input_dev`
*   `bpf_fib_set_fwd_params`
*   `bpf_xdp_fib_lookup`
*   `bpf_skb_fib_lookup`
*   `bpf_ipv4_fib_lookup`
*   `bpf_ipv6_fib_lookup`
*   `bpf_fib_lookup_flags_ok`



10/2 2026/07/13 21:56 tool grepper
0m Args:
{
  "Expression": "bpf_fib_lookup_flags_ok",
  "PathPrefix": "net/core/filter.c"
}

Results:
{
  "Output": "net/core/filter.c=6406=static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,\n--\nnet/core/filter.c-6562-\nnet/core/filter.c:6563:static bool bpf_fib_lookup_flags_ok(u32 flags)\nnet/core/filter.c-6564-{\n--\nnet/core/filter.c=6575=BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx,\n--\nnet/core/filter.c-6580-\nnet/core/filter.c:6581:\tif (!bpf_fib_lookup_flags_ok(flags))\nnet/core/filter.c-6582-\t\treturn -EINVAL;\n--\nnet/core/filter.c=6609=BPF_CALL_4(bpf_skb_fib_lookup, struct sk_buff *, skb,\n--\nnet/core/filter.c-6618-\nnet/core/filter.c:6619:\tif (!bpf_fib_lookup_flags_ok(flags))\nnet/core/filter.c-6620-\t\treturn -EINVAL;\n"
}

11/2 2026/07/13 21:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 30033
							output: 243
							thoughts: 521
						

Thoughts:
**Analyzing the BPF FIB Lookup VLAN Support Patch**

Okay, so this patch introduces a new function, `bpf_fib_lookup_flags_ok`. That's important to note. My immediate focus then needs to be on the functions affected, which are: `bpf_fib_vlan_input_dev`, `bpf_fib_set_fwd_params`, `bpf_fib_lookup_flags_ok`, `bpf_xdp_fib_lookup`, `bpf_skb_fib_lookup`, `bpf_ipv4_fib_lookup`, and `bpf_ipv6_fib_lookup`. It's worth including `bpf_ipv4_fib_lookup` and `bpf_ipv6_fib_lookup` because they're specific to BPF FIB lookups, which differentiates them from the general IPv4/IPv6 routing lookups.

The configuration options that seem relevant here are `VLAN_8021Q`, `INET`, and `IPV6`. While `INET` and `IPV6` are standard, including them doesn't hurt. However, `VLAN_8021Q` is the most specific. Let me double check if any other configs are being used. Okay, just `CONFIG_VLAN_8021Q`. Good.

The patch's core impact is adding VLAN support to `bpf_xdp_fib_lookup` and `bpf_skb_fib_lookup`, implementing `BPF_FIB_LOOKUP_VLAN` and `BPF_FIB_LOOKUP_VLAN_INPUT` flags. This is networking and BPF core logic, which makes it particularly interesting from a fuzzing perspective, especially since syzkaller can reach it. I think the original list of focus functions is appropriate, so the list remains: `bpf_fib_vlan_input_dev`, `bpf_fib_set_fwd_params`, `bpf_fib_lookup_flags_ok`, `bpf_xdp_fib_lookup`, `bpf_skb_fib_lookup`, `bpf_ipv4_fib_lookup`, and `bpf_ipv6_fib_lookup`. Alright, let's call `set-results`.



12/2 2026/07/13 21:56 tool set-results
0m Args:
{
  "EnableConfigs": [
    "VLAN_8021Q",
    "INET",
    "IPV6"
  ],
  "FocusSymbols": [
    "bpf_fib_vlan_input_dev",
    "bpf_fib_set_fwd_params",
    "bpf_fib_lookup_flags_ok",
    "bpf_xdp_fib_lookup",
    "bpf_skb_fib_lookup",
    "bpf_ipv4_fib_lookup",
    "bpf_ipv6_fib_lookup"
  ],
  "Reasoning": "The patch adds support for VLANs in the BPF FIB lookup helpers (`bpf_xdp_fib_lookup` and `bpf_skb_fib_lookup`). It introduces new flags `BPF_FIB_LOOKUP_VLAN` and `BPF_FIB_LOOKUP_VLAN_INPUT` to handle VLAN egress and ingress resolution. This modifies core networking and BPF logic, which is reachable from BPF programs and can be fuzzed by syzkaller.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "VLAN_8021Q",
    "INET",
    "IPV6"
  ],
  "FocusSymbols": [
    "bpf_fib_vlan_input_dev",
    "bpf_fib_set_fwd_params",
    "bpf_fib_lookup_flags_ok",
    "bpf_xdp_fib_lookup",
    "bpf_skb_fib_lookup",
    "bpf_ipv4_fib_lookup",
    "bpf_ipv6_fib_lookup"
  ],
  "Reasoning": "The patch adds support for VLANs in the BPF FIB lookup helpers (`bpf_xdp_fib_lookup` and `bpf_skb_fib_lookup`). It introduces new flags `BPF_FIB_LOOKUP_VLAN` and `BPF_FIB_LOOKUP_VLAN_INPUT` to handle VLAN egress and ingress resolution. This modifies core networking and BPF logic, which is reachable from BPF programs and can be fuzzed by syzkaller.",
  "WorthFuzzing": true
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)