nfsd: validate sockaddr length per family in listener_set
Subsystems (2)
Jeff Layton
maintainerPATCH v5 1/1 nfsd: validate sockaddr length per family in listener_set
nfsd_sock_nl_policy declares NFSD_A_SOCK_ADDR as a bare NLA_BINARY attribute with no minimum length. A CAP_NET_ADMIN caller can send a 16-byte NFSD_A_SOCK_ADDR with sa_family=AF_INET6, causing a 12-byte OOB read across three consumers (rpc_cmp_addr_port, svc_find_listener, kernel_bind).
nfsd_nl_listener_set_doit() also parsed and validated each listener entry inline in two separate loops, interleaved with mutating the running listener configuration. The validation was duplicated, used an open-coded "nla_len < sizeof(struct sockaddr)" check that was too short for AF_INET6, and handled a malformed entry inconsistently depending on which loop noticed it.
Add an nfsd_nl_validate_listeners() helper that walks the entire list once and confirms each entry parses, carries both an address and a transport name, and is long enough for its address family (sizeof(struct sockaddr_in) for AF_INET, sizeof(struct sockaddr_in6) for AF_INET6, -EAFNOSUPPORT otherwise). Call it before taking nfsd_mutex or creating the serv, so a malformed request fails cleanly with no side effects.
Since every entry is known valid by the time the two existing loops run, drop the redundant presence and per-family length checks from both, leaving only the nla_parse_nested() call needed to extract the data.
---
Changes in v5:
- Vet the entire listener list before making any changes
- Link to v4: https://lore.kernel.org/r/[email protected]
Changes in v4:
- Drop policy floor and do full inline validation
- Link to v3: https://lore.kernel.org/r/[email protected]
--- fs/nfsd/nfsctl.c | 77 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 12 deletions(-)
fs/nfsd/nfsctl.c +65 -12
@@ -1971,6 +1971,60 @@ int nfsd_nl_version_get_doit(struct sk_buff *skb, struct genl_info *info)return err; } +/** +* nfsd_nl_validate_listeners - sanity-check the listener list from userland +* @info: netlink metadata and command arguments +* +* Walk every NFSD_A_SERVER_SOCK_ADDR attribute and confirm that each entry +* is well-formed: it parses against the policy, carries both an address and +* a transport name, and the address is long enough for its family. Doing +* this up front lets the callers below assume every entry is valid and +* guarantees we make no changes when the request is malformed. +* +* Return 0 if every entry is valid, or a negative errno otherwise. +*/ +static int nfsd_nl_validate_listeners (struct genl_info *info) +{ +const struct nlattr *attr; +int rem; + +nlmsg_for_each_attr_type (attr,NFSD_A_SERVER_SOCK_ADDR , info->nlhdr, +GENL_HDRLEN , rem) { +struct nlattr *tb[NFSD_A_SOCK_MAX +1 ]; +struct sockaddr *sa; +int err; + + err =nla_parse_nested (tb,NFSD_A_SOCK_MAX , attr, + nfsd_sock_nl_policy, info->extack); +if (err <0 ) +return err; + +if (!tb[NFSD_A_SOCK_ADDR ] || !tb[NFSD_A_SOCK_TRANSPORT_NAME ]) +return -EINVAL ; + + sa =nla_data (tb[NFSD_A_SOCK_ADDR ]); +if (nla_len (tb[NFSD_A_SOCK_ADDR ]) <sizeof (sa->sa_family)) +return -EINVAL ; + +switch (sa->sa_family) { +case AF_INET : +if (nla_len (tb[NFSD_A_SOCK_ADDR ]) < +sizeof (struct sockaddr_in )) +return -EINVAL ; +break ; +case AF_INET6 : +if (nla_len (tb[NFSD_A_SOCK_ADDR ]) < +sizeof (struct sockaddr_in6 )) +return -EINVAL ; +break ; +default : +return -EAFNOSUPPORT ; + } + } + +return 0 ; +} +/** * nfsd_nl_listener_set_doit - set the nfs running sockets * @skb: reply buffer
@@ -1989,6 +2043,15 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)bool delete = false;int err, rem; +/* +* Validate the entire listener list before making any changes, so a +* malformed request fails cleanly without creating a serv or touching +* the existing listeners. +*/ + err =nfsd_nl_validate_listeners (info); +if (err) +return err; +mutex_lock (&nfsd_mutex); err =nfsd_create_serv (net);
@@ -2015,16 +2078,11 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)const char *xcl_name;struct sockaddr *sa; +/* validated up front in nfsd_nl_validate_listeners() */ if (nla_parse_nested (tb,NFSD_A_SOCK_MAX , attr, nfsd_sock_nl_policy, info->extack) <0 )continue ; -if (!tb[NFSD_A_SOCK_ADDR ] || !tb[NFSD_A_SOCK_TRANSPORT_NAME ]) -continue ; - -if (nla_len (tb[NFSD_A_SOCK_ADDR ]) <sizeof (*sa)) -continue ; - xcl_name =nla_data (tb[NFSD_A_SOCK_TRANSPORT_NAME ]); sa =nla_data (tb[NFSD_A_SOCK_ADDR ]);
@@ -2076,16 +2134,11 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)struct sockaddr *sa;int ret; +/* validated up front in nfsd_nl_validate_listeners() */ if (nla_parse_nested (tb,NFSD_A_SOCK_MAX , attr, nfsd_sock_nl_policy, info->extack) <0 )continue ; -if (!tb[NFSD_A_SOCK_ADDR ] || !tb[NFSD_A_SOCK_TRANSPORT_NAME ]) -continue ; - -if (nla_len (tb[NFSD_A_SOCK_ADDR ]) <sizeof (*sa)) -continue ; - xcl_name =nla_data (tb[NFSD_A_SOCK_TRANSPORT_NAME ]); sa =nla_data (tb[NFSD_A_SOCK_ADDR ]);
---
base-commit: 332e2f4f37b213f231be1ab5ddc17e2052383b60
change-id: 20260608-nfsd-testing-688a82433c50
Best regards,
Signature · 1 line
Jeff Layton <[email protected]>
Chuck Lever
maintainerQuoted 6 lines · Jeff Layton
nfsd_sock_nl_policydeclaresNFSD_A_SOCK_ADDRas a bareNLA_BINARYattribute with no minimum length. ACAP_NET_ADMINcaller can send a 16-byteNFSD_A_SOCK_ADDRwith sa_family=AF_INET6, causing a 12-byte OOB read across three consumers (rpc_cmp_addr_port,svc_find_listener,kernel_bind).
Quoted 8 lines
nfsd_nl_listener_set_doit()also parsed and validated each listener entry inline in two separate loops, interleaved with mutating the running listener configuration. The validation was duplicated, used an open-coded "nla_len< sizeof(struct sockaddr)" check that was too short forAF_INET6, and handled a malformed entry inconsistently depending on which loop noticed it.[...]
Applied to nfsd-testing, thanks!
[1/1] nfsd: validate sockaddr length per family in listener_set
commit: 1f102d9ff00620b845546051ad1ee57976f2db88
--
Chuck Lever