Netdev List
 help / color / mirror / Atom feed
From: Jakub Kicinski <kuba@kernel.org>
To: davem@davemloft.net
Cc: netdev@vger.kernel.org, edumazet@google.com, pabeni@redhat.com,
	andrew+netdev@lunn.ch, horms@kernel.org,
	Jakub Kicinski <kuba@kernel.org>
Subject: [PATCH net 2/2] selftests: net: nl_nlctrl: check the op ids in the policy map
Date: Fri, 18 Sep 2026 15:29:49 -0700	[thread overview]
Message-ID: <20260918222949.4190284-2-kuba@kernel.org> (raw)
In-Reply-To: <20260918222949.4190284-1-kuba@kernel.org>

Validate that the op map in the policy dump is correct.

Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
 tools/testing/selftests/net/nl_nlctrl.py | 116 +++++++++++++++++++----
 1 file changed, 99 insertions(+), 17 deletions(-)

diff --git a/tools/testing/selftests/net/nl_nlctrl.py b/tools/testing/selftests/net/nl_nlctrl.py
index fe1f66dc9435..237b3d273260 100755
--- a/tools/testing/selftests/net/nl_nlctrl.py
+++ b/tools/testing/selftests/net/nl_nlctrl.py
@@ -9,40 +9,86 @@ from lib.py import ksft_run, ksft_exit
 from lib.py import ksft_eq, ksft_ge, ksft_true, ksft_in, ksft_not_in
 from lib.py import NetdevFamily, EthtoolFamily, NlctrlFamily
 
+# Families we can expect to always be around, and which between them
+# cover ops with a do, with a dump, and with both.
+FAMILIES = ('nlctrl', 'netdev')
 
-def getfamily_do(ctrl) -> None:
-    """Query a single family by name and validate its ops."""
-    fam = ctrl.getfamily({'family-name': 'netdev'})
-    ksft_eq(fam['family-name'], 'netdev')
+
+def _get_ops(ctrl, name):
+    """Get the ops of a family, keyed by command id."""
+    fam = ctrl.getfamily({'family-name': name})
+    ksft_eq(fam['family-name'], name)
     ksft_true(fam['family-id'] > 0)
 
     # The format of ops is quite odd, [{$idx: {"id"...}}, {$idx: {"id"...}}]
     # Discard the indices and re-key by command id.
     ops_by_id = {v['id']: v for op in fam['ops'] for v in op.values()}
-    ksft_eq(len(ops_by_id), len(fam['ops']))
+    ksft_eq(len(ops_by_id), len(fam['ops']),
+            comment=f"{name} lists a command twice")
+    return ops_by_id
 
-    # All ops should have a policy (either do or dump has one)
-    for op in ops_by_id.values():
-        ksft_in('cmd-cap-haspol', op['flags'],
-                comment=f"op {op['id']} missing haspol")
+
+def _get_policy_map(ctrl, req):
+    """
+    The policy map in the Netlink replies looks like this:
+
+         [{'family-id': 16, 'op-policy': {'do': 0, 'dump': 0, 'op-id': 3}},
+          {'family-id': 16, 'op-policy': {'dump': 1, 'op-id': 4}}, ...]
+
+    Return the mapping:
+
+         {3:{'do','dump'}, 4:{'dump'}}
+
+    The policy itself is discarded here, only return which command has policy.
+    """
+    pol_map = {}
+    for msg in ctrl.getpolicy(req, dump=True):
+        if 'op-policy' not in msg:
+            continue
+        modes = dict(msg['op-policy'])
+        cmd = modes.pop('op-id')
+        ksft_not_in(cmd, pol_map, comment=f"command {cmd} reported twice")
+        pol_map[cmd] = set(modes.keys())
+    return pol_map
+
+
+def getfamily_do(ctrl) -> None:
+    """Query single families by name and validate their ops."""
+    ops = {name: _get_ops(ctrl, name) for name in FAMILIES}
+
+    for name, ops_by_id in ops.items():
+        for op in ops_by_id.values():
+            # All ops in nlctrl and netdev have a policy
+            ksft_in('cmd-cap-haspol', op['flags'],
+                    comment=f"{name} op {op['id']} missing haspol")
+            ksft_true(op['flags'] & {'cmd-cap-do', 'cmd-cap-dump'},
+                      comment=f"{name} op {op['id']} has no handler")
+
+    # nlctrl getfamily (id 3) does both, getpolicy (id 10) is dump-only
+    ksft_in('cmd-cap-do', ops['nlctrl'][3]['flags'])
+    ksft_in('cmd-cap-dump', ops['nlctrl'][3]['flags'])
+    ksft_not_in('cmd-cap-do', ops['nlctrl'][10]['flags'])
+    ksft_in('cmd-cap-dump', ops['nlctrl'][10]['flags'])
+
+    netdev = ops['netdev']
 
     # dev-get (id 1) should support both do and dump
-    ksft_in('cmd-cap-do', ops_by_id[1]['flags'])
-    ksft_in('cmd-cap-dump', ops_by_id[1]['flags'])
+    ksft_in('cmd-cap-do', netdev[1]['flags'])
+    ksft_in('cmd-cap-dump', netdev[1]['flags'])
 
     # qstats-get (id 12) is dump-only
-    ksft_not_in('cmd-cap-do', ops_by_id[12]['flags'])
-    ksft_in('cmd-cap-dump', ops_by_id[12]['flags'])
+    ksft_not_in('cmd-cap-do', netdev[12]['flags'])
+    ksft_in('cmd-cap-dump', netdev[12]['flags'])
 
     # napi-set (id 14) is do-only and requires admin
-    ksft_in('cmd-cap-do', ops_by_id[14]['flags'])
-    ksft_not_in('cmd-cap-dump', ops_by_id[14]['flags'])
-    ksft_in('admin-perm', ops_by_id[14]['flags'])
+    ksft_in('cmd-cap-do', netdev[14]['flags'])
+    ksft_not_in('cmd-cap-dump', netdev[14]['flags'])
+    ksft_in('admin-perm', netdev[14]['flags'])
 
     # Notification-only commands (dev-add/del/change-ntf etc.) must
     # not appear in the ops list since they have no do/dump handlers.
     for ntf_id in [2, 3, 4, 6, 7, 8]:
-        ksft_not_in(ntf_id, ops_by_id,
+        ksft_not_in(ntf_id, netdev,
                     comment=f"ntf-only cmd {ntf_id} should not be in ops")
 
 
@@ -103,6 +149,41 @@ from lib.py import NetdevFamily, EthtoolFamily, NlctrlFamily
             comment="linkinfo-set should not have a dump policy")
 
 
+def getpolicy_op_map(ctrl) -> None:
+    """Check the op-to-policy map consistency. Each op with 'haspol' flag
+    has to have a policy. The policy back-references must name only
+    real ops that exist, have given modes (do vs dump) and have 'haspol'.
+    """
+    for name in FAMILIES:
+        ops_by_id = _get_ops(ctrl, name)
+        haspol = {cmd for cmd, op in ops_by_id.items()
+                  if 'cmd-cap-haspol' in op['flags']}
+
+        pol_map = _get_policy_map(ctrl, {'family-name': name})
+        ksft_eq(set(pol_map), haspol,
+                comment=f"{name} policy map does not match the op list")
+
+        # Walk the op list rather than the map, the map may be missing
+        # the very op we are after. Asking for a command the family does
+        # not have is an error, so it must not come from the map either.
+        for cmd in sorted(haspol):
+            modes = pol_map.get(cmd, set())
+
+            # The kernel only reports a mode the op actually has.
+            if 'do' in modes:
+                ksft_in('cmd-cap-do', ops_by_id[cmd]['flags'],
+                        comment=f"{name} cmd {cmd} has no do")
+            if 'dump' in modes:
+                ksft_in('cmd-cap-dump', ops_by_id[cmd]['flags'],
+                        comment=f"{name} cmd {cmd} has no dump")
+
+            # Asking for one op builds the map in a different place in
+            # the kernel, it has to report what the full dump did.
+            single = _get_policy_map(ctrl, {'family-name': name, 'op': cmd})
+            ksft_eq(single, {cmd: modes},
+                    comment=f"{name} cmd {cmd} policy differs from the dump")
+
+
 def getpolicy_by_op(_ctrl) -> None:
     """Query policy for specific ops, check attr names are resolved."""
     ndev = NetdevFamily()
@@ -122,6 +203,7 @@ from lib.py import NetdevFamily, EthtoolFamily, NlctrlFamily
     ksft_run([getfamily_do,
               getfamily_dump,
               getpolicy_dump,
+              getpolicy_op_map,
               getpolicy_by_op],
              args=(ctrl, ))
     ksft_exit()
-- 
2.55.0


  reply	other threads:[~2026-09-18 22:29 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-18 22:29 [PATCH net 1/2] genetlink: report the real command id for dump-only ops in policy dumps Jakub Kicinski
2026-09-18 22:29 ` Jakub Kicinski [this message]
2026-09-22 13:30 ` patchwork-bot+netdevbpf

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260918222949.4190284-2-kuba@kernel.org \
    --to=kuba@kernel.org \
    --cc=andrew+netdev@lunn.ch \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=horms@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox