Skip to content

tlshd: Fix netlink attribute handling - #160

Merged
chucklever merged 4 commits into
mainfrom
tlshd-netlink-attribute-handling
Aug 18, 2026
Merged

tlshd: Fix netlink attribute handling#160
chucklever merged 4 commits into
mainfrom
tlshd-netlink-attribute-handling

Conversation

@chucklever

Copy link
Copy Markdown
Member

Two of tlshd's responses to a handshake request are inverted.
tlshd_genl_valid_handler() reports a parse failure as NL_STOP, which
libnl hands back to its caller as success, so tlshd services the socket
with parameters it never finished filling in. The same handler fails the
request when getnameinfo() cannot name the peer, and that lookup is a
fallback for the NFSD and NVMe handshakes, which never read its result.

Relaxing that fallback exposes a null dereference.
HANDSHAKE_A_ACCEPT_PEERNAME is optional, so leaving the name unset hands
NULL to the strlen() in both TLS 1.3 client paths. That is why the
client-side check for a missing peer name comes first. After the series a
client handshake request that carries no peer name is rejected instead of
completing a session GnuTLS cannot tie to a peer.

The capability probe never reaches the kernel. It reads nl_send_auto()'s
return, which counts the bytes written to the socket, so every attribute
under test reads as supported. Session tags configured against a kernel
whose HANDSHAKE_CMD_DONE policy has no tag attribute produce a DONE
message the kernel rejects, and the handshake completion is lost. Asking
the generic netlink control family instead pulls CTRL_ATTR_POLICY
definitions from the build host's UAPI headers. Per-command policy
dumping arrived in v5.10 and the handshake family in v6.5, so any kernel
that offers the family can answer, but nothing else in tlshd has needed
headers that recent. configure.ac tests for them and README records the
requirement, so an older build host gets one diagnostic instead of a
compile failure in netlink.c.

tlshd_genl_valid_handler() returns NL_STOP when it cannot parse an
accept message. libnl reports NL_STOP to its caller as success.
nl_recvmsgs_default() returns zero, and
tlshd_genl_get_handshake_parms() hands back handshake parameters it
never finished filling in. tlshd then services the socket with a
sockfd it could not read a peer address from, or with ip_proto left
at -1.

Return a negative libnl error code instead. recvmsgs() propagates it
to nl_recvmsgs_default(), and tlshd_genl_get_handshake_parms()
already converts a negative return to EINVAL. tlshd_service_socket()
skips the handshake and reports the failure to the kernel.

tlshd_genl_event_handler() returns NL_SKIP and nothing else. Drop
the NL_OK and NL_STOP retvals its documentation still lists.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
HANDSHAKE_A_ACCEPT_PEERNAME is optional. The kernel emits it only
for a consumer that set ta_peername. Both TLS 1.3 client handshake
paths pass parms->peername to strlen() without checking. Only the
SUNRPC client requests a client-side handshake today, and it always
sets ta_peername, so the dereference is unreachable. Nothing in the
protocol keeps it that way.

Guarding the strlen() and continuing would trade the dereference
for a weaker session. The peer name also reaches
gnutls_session_set_verify_cert() and
gnutls_certificate_verify_peers3(). Given a NULL hostname, GnuTLS
checks the certificate chain but not who presented it. Nothing on
the kernel side makes up the difference, because
xs_tls_handshake_done() ignores the peerid. The QUIC client path
guards its strlen() already, and completes exactly that unverified
handshake.

Reject the request instead when the peer name is missing.
session_status is already EIO, so the early return fails the
kernel's request rather than stranding the socket. The QUIC path's
test for a peer name is then redundant and goes away.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
tlshd_genl_valid_handler() fails the handshake request when
getnameinfo() cannot map the peer address to a name. That lookup is
a fallback for a request that carries no
HANDSHAKE_A_ACCEPT_PEERNAME. Only the SUNRPC client sets
ta_peername, so the fallback runs for the NFSD and NVMe handshakes,
and none of them read the result. A resolver that cannot answer
must not fail those handshakes.

Leave the name unset and proceed. The client handshake paths are
the only ones that need a peer name, and they already reject a
request that arrives without one.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
tlshd_probe_attr() sends a HANDSHAKE_CMD_DONE message carrying the
attribute under test and a sockfd of -1, then reads nl_send_auto()'s
return to decide whether the kernel accepts that attribute. That
return is the count of bytes written to the socket. The kernel has
not seen the message yet, so every attribute reads as supported. An
administrator who configures session tags for a kernel whose
HANDSHAKE_CMD_DONE policy has no HANDSHAKE_A_DONE_TAG entry gets a
DONE message the kernel rejects. The handshake completion is lost.

Reading the reply does not rescue the probe. The sockfd of -1 that
makes the probe harmless is the one sockfd_lookup() rejects, so the
reply carries an error whether or not the kernel knows the
attribute.

Ask the generic netlink control family instead. CTRL_CMD_GETPOLICY
dumps the attribute policy for one command, and an attribute the
policy rejects is left out of the dump. Per-command policy dumping
arrived in v5.10 and the handshake family in v6.5, so every kernel
that offers the family can answer. Give the dump its own socket
rather than borrowing the notification socket, which by then has
joined the tlshd multicast group and has the event handler
installed.

These control attributes come from the build host's UAPI headers.
Nothing else in tlshd requires headers that recent, so a host with
pre-v5.10 headers builds the daemon today. Test for them at
configure time and record the requirement in README, so that host
gets one clear diagnostic instead of a compile failure in netlink.c.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
@chucklever
chucklever merged commit 5f6564f into main Aug 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant