FreeBSD · sys/net · six commits
A generic way to read and program a network card's receive-side scaling hash key and indirection table, wired through iflib, with aq(4) as the reference driver and ifconfig as the consumer.
Hardware-validated 2026-09-26 on bare metal, both silicon generations. Get and set of the hash key and the indirection table are proven on aq(4) A1 (AQC107) and A2 (AQC113): ifconfig rsstable (single, list, and range) and rsskey read back correctly and live-reprogram the card - with the default 0-7 table sixteen flows spread across all eight rx_queues, and with rsstable N every received packet landed on rx_queue N. ix(4)/ixgbe correctly returns EOPNOTSUPP (not converted). A2 additionally reports the ipv6ex/tcp6ex hash types. Proven on a real two-socket machine.
A card with more than one receive queue has to decide which queue each frame lands in. It hashes a tuple from the headers with a Toeplitz key, masks the result into an index, and looks the queue up in an indirection table. The key decides which bucket a flow falls in; the table decides which queue that bucket feeds, and therefore which CPU does the receive work.
All interface ioctls enter through ifhwioctl(), so privilege is settled before anything looks at the request. The setters join the group that already carries SIOCSIFMEDIA, which also means a successful set stamps if_lastchange for free. Validation then happens once, in iflib, so no driver repeats it.
An interface that cannot answer gives one of two different errors, and the difference is diagnostic rather than cosmetic: an iflib driver that simply has not implemented the methods answers from the default stub, while a driver outside iflib never had a case at all.
EOPNOTSUPP distinct from EINVAL is what lets a caller tell “this driver has not implemented it” from “this interface has no such concept”.The upstream get_rss_key and get_rss_hash methods went in on 2026-09-18 so that hn(4) could read a passed-through card's settings. This work carried a parallel five-method family of its own, which after the rebase left two case SIOCGIFRSSKEY labels in the same switch.
The resolution keeps the upstream methods untouched and adds ours beside them in the same naming, sharing one default stub per signature the way the file's own queue-setup and VLAN methods already do.
| ifdi method | Origin | Answers | Default |
|---|---|---|---|
| get_rss_key | upstream | SIOCGIFRSSKEY | null_rss_key_op |
| set_rss_key | this stack | SIOCSIFRSSKEY | null_rss_key_op |
| get_rss_hash | upstream | SIOCGIFRSSHASH | null_get_rss_hash |
| get_rss_table | this stack | SIOCGIFRSSTABLE | null_rss_table_op |
| set_rss_table | this stack | SIOCSIFRSSTABLE | null_rss_table_op |
The structure is fixed-size, so its length is encoded in the ioctl number and can never grow later. ice(4) selects its lookup table size from a firmware capability and its admin queue defines a two-thousand-entry table for physical functions, which is the largest any in-tree driver can ask for; the structure is sized for that and still lands well inside the kernel's ioctl argument limit.
| Quantity | Value | Why |
|---|---|---|
| Table entries | 2048 | ice's 2K lookup table; a fixed struct cannot grow past it |
| Struct size | 4116 B | malloc'd by the ioctl path, never on a stack, nothing per interface |
| Argument limit | 8192 B | the ceiling the ioctl command word can express |
| Entry width | uint16 | ice reports up to 256 queues, which a byte cannot count |
aq programs its key ten registers at a time and its table in register-sized pieces, each behind a handshake that can time out. A write that stops half way leaves a mixture of old and new entries in hardware while the driver's own copy still describes the old state.
The answer adds no new state. The driver keeps its copy unchanged, reports the error, and asks iflib for a reinit, because aq_if_init() already reprograms the key and the table from that copy on every path. The hardware is pulled back to the thing the getters have been reporting all along.
This was asked twice in review: every card already programs the same key, exported through a sysctl, so why add anything. The answer is that nothing changes until somebody sets something.
Six commits, each building on its own, cut against an unmodified upstream so every diff applies to head one at a time. The order is a real dependency chain, not a filing convention: the ABI needs no driver, iflib needs the ABI, and both driver commits rely on the attach gate iflib adds, the read side because it dropped its own readiness flag.
| Check | Scope | Result |
|---|---|---|
| Per-commit compile | kernel objects and the aq module at each of the six | clean |
| Kernel builds | GENERIC, an options RSS config, and the host's own config with debugging | no warnings |
| Userland | ifconfig with and without netlink, tests, the placement tool | clean |
| Manual pages | lint on three pages; no new findings in ifconfig's | clean |
| Kernel regression tests | five runnable cases on a guest booted from this stack; the hardware round trip skips without an interface name | 5 of 5 |
| ifconfig tests | status, both parsers, refusal, privilege | 5 of 5 |
| Dispatch on real interfaces | em0, vtnet0 and lo0 on the new kernel | as designed |
| Placement tool fixtures | re-run against the larger table | 11 of 11 |
| Code review | fifteen verified findings; eleven fixed in place, three decided, one pre-existing aq gap left for its own commit | done |
| Card steering and key A/B | needs the Atlantic card handed to the guest | not yet run |
One check waits on a decision rather than on work: the card steering and key A/B needs the Atlantic card handed through to the test guest, and PCI passthrough teardown has panicked the machine once before.
The scope was set by the person who has to review it, and the answer to “why not also” is written into the commit messages rather than left for a reviewer to ask.