git: fbe0257b5613 - main - libc/resolv: Reimplement the sortlist parser

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Mon, 06 Jul 2026 12:24:28 UTC
The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=fbe0257b5613f457af42f82efb2e3bc9395d0557

commit fbe0257b5613f457af42f82efb2e3bc9395d0557
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2026-07-06 12:23:37 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2026-07-06 12:23:37 +0000

    libc/resolv: Reimplement the sortlist parser
    
    When we switched from the BIND4 resolver to the BIND9 resolver, the
    sortlist parser was inadvertently disabled due to a missing #define, and
    nobody seemed to notice.  The sorting code remained enabled in the
    resolver, but there was no way to set a sort order.
    
    Reimplement the sortlist parser, but correctly, and update the manual
    accordingly.  The new parser accepts IPv4 and IPv6 addresses with or
    without a mask or prefix length, just like the old one, except IPv6
    support was a bit wonky in the original code.
    
    Fixes:          5342d17f09a8 ("Update the resolver in libc to BIND9's one.")
    Relnotes:       yes
    Reviewed by:    markj
    Differential Revision:  https://reviews.freebsd.org/D57925
---
 lib/libc/resolv/Makefile.inc |   1 +
 lib/libc/resolv/res_init.c   | 246 ++++++++++++++++++++++++-------------------
 share/man/man5/resolver.5    |  36 ++++---
 3 files changed, 160 insertions(+), 123 deletions(-)

diff --git a/lib/libc/resolv/Makefile.inc b/lib/libc/resolv/Makefile.inc
index be900e942435..ecb0c2462b08 100644
--- a/lib/libc/resolv/Makefile.inc
+++ b/lib/libc/resolv/Makefile.inc
@@ -7,4 +7,5 @@ SRCS+=	herror.c h_errno.c mtctxres.c res_comp.c res_data.c res_debug.c \
 
 SYM_MAPS+= ${LIBC_SRCTOP}/resolv/Symbol.map
 
+CFLAGS+=-I${LIBC_SRCTOP}/net
 CFLAGS+=-I${SRCTOP}/lib/libmd
diff --git a/lib/libc/resolv/res_init.c b/lib/libc/resolv/res_init.c
index bd3fd866c189..120b30fc74f5 100644
--- a/lib/libc/resolv/res_init.c
+++ b/lib/libc/resolv/res_init.c
@@ -95,6 +95,7 @@
 /* ensure that sockaddr_in6 and IN6ADDR_ANY_INIT are declared / defined */
 #include <resolv.h>
 
+#include "res_config.h"
 #include "res_private.h"
 
 static void res_readconf(res_state, const char *, int);
@@ -104,10 +105,8 @@ static int res_setnsaddr(res_state, int, const char *, const char *);
 static void res_setoptions(res_state, const char *, const char *);
 
 #ifdef RESOLVSORT
-static const char sort_mask[] = "/&";
-#define ISSORTMASK(ch) (strchr(sort_mask, ch) != NULL)
-static u_int32_t net_mask(struct in_addr);
-#endif
+static int res_setsortlist(res_state, int, const char *, const char *);
+#endif /* RESOLVSORT */
 
 #define res_space(ch)							\
 	isspace((unsigned char)(ch))
@@ -285,8 +284,7 @@ res_readconf(res_state statp, const char *resconf, int maxns)
 	size_t sz = 0;
 	int nserv = 0;
 #ifdef RESOLVSORT
-	int n, nsort = 0;
-	char *net;
+	int nsort = 0;
 #endif
 
 	if ((fp = fopen(resconf, "re")) == NULL)
@@ -357,101 +355,28 @@ res_readconf(res_state statp, const char *resconf, int maxns)
 			continue;
 		}
 #ifdef RESOLVSORT
+		/* read result sort order */
 		if (res_match(line, eow, "sortlist")) {
-			struct in_addr a;
-			struct in6_addr a6;
-			int m, i;
-			u_char *u;
-			struct __res_state_ext *ext = statp->_u._ext.ext;
-
-			cp = line + sizeof("sortlist") - 1;
 			while (nsort < MAXRESOLVSORT) {
-				while (*cp == ' ' || *cp == '\t')
+				cp = eow;
+				while (cp < eol && res_space(*cp))
 					cp++;
-				if (*cp == '\0' || *cp == '\n' || *cp == ';')
+				if (cp == eol)
 					break;
-				net = cp;
-				while (*cp && !ISSORTMASK(*cp) && *cp != ';' &&
-				    isascii(*cp) && !isspace((unsigned char)*cp))
-					cp++;
-				n = *cp;
-				*cp = 0;
-				if (inet_aton(net, &a)) {
-					statp->sort_list[nsort].addr = a;
-					if (ISSORTMASK(n)) {
-						*cp++ = n;
-						net = cp;
-						while (*cp && *cp != ';' &&
-						    isascii(*cp) &&
-						    !isspace((unsigned char)*cp))
-							cp++;
-						n = *cp;
-						*cp = 0;
-						if (inet_aton(net, &a)) {
-							statp->sort_list[nsort].mask = a.s_addr;
-						} else {
-							statp->sort_list[nsort].mask =
-							    net_mask(statp->sort_list[nsort].addr);
-						}
-					} else {
-						statp->sort_list[nsort].mask =
-						    net_mask(statp->sort_list[nsort].addr);
-					}
-					ext->sort_list[nsort].af = AF_INET;
-					ext->sort_list[nsort].addr.ina =
-					    statp->sort_list[nsort].addr;
-					ext->sort_list[nsort].mask.ina.s_addr =
-					    statp->sort_list[nsort].mask;
-					nsort++;
-				}
-				else if (inet_pton(AF_INET6, net, &a6) == 1) {
-
-					ext->sort_list[nsort].af = AF_INET6;
-					ext->sort_list[nsort].addr.in6a = a6;
-					u = (u_char *)&ext->sort_list[nsort].mask.in6a;
-					*cp++ = n;
-					net = cp;
-					while (*cp && *cp != ';' &&
-					    isascii(*cp) && !isspace(*cp))
-						cp++;
-					m = n;
-					n = *cp;
-					*cp = 0;
-					switch (m) {
-					case '/':
-						m = atoi(net);
-						break;
-					case '&':
-						if (inet_pton(AF_INET6, net, u) == 1) {
-							m = -1;
-							break;
-						}
-						/*FALLTHROUGH*/
-					default:
-						m = sizeof(struct in6_addr) * CHAR_BIT;
-						break;
-					}
-					if (m >= 0) {
-						for (i = 0; i < sizeof(struct in6_addr); i++) {
-							if (m <= 0) {
-								*u = 0;
-							} else {
-								m -= CHAR_BIT;
-								*u = (u_char)~0;
-								if (m < 0)
-									*u <<= -m;
-							}
-							u++;
-						}
-					}
-					statp->sort_list[nsort].addr.s_addr =
-					    (u_int32_t)0xffffffff;
-					statp->sort_list[nsort].mask =
-					    (u_int32_t)0xffffffff;
+				eow = cp;
+				while (eow < eol && !res_space(*eow))
+					eow++;
+				if (res_setsortlist(statp, nsort, cp, eow))
 					nsort++;
-				}
-				*cp = n;
 			}
+#ifdef DEBUG
+			while (cp < eol && res_space(*cp))
+				cp++;
+			if (cp < eol) {
+				printf(";; sortlist overflow: %.*s\n",
+				    (int)(eol - cp), cp);
+			}
+#endif
 			continue;
 		}
 #endif /* RESOLVSORT */
@@ -631,6 +556,122 @@ res_setnsaddr(res_state statp, int i, const char *str, const char *end)
 	return (ret);
 }
 
+#ifdef RESOLVSORT
+/*%
+ * Parse a sortlist entry and insert it into the specified slot in the
+ * sortlist.  Assumes no leading or trailing space.  Returns 1 if an entry
+ * was inserted, 0 otherwise.
+ */
+static int
+res_setsortlist(res_state statp, int i, const char *str, const char *end)
+{
+	struct __res_state_ext *ext = statp->_u._ext.ext;
+	char addr[48], mask[48], *dst, *numend;
+	_Static_assert(sizeof(addr) > INET6_ADDRSTRLEN,
+	    "address buffer too small for AF_INET6");
+	_Static_assert(sizeof(addr) > INET_ADDRSTRLEN,
+	    "address buffer too small for AF_INET");
+	_Static_assert(sizeof(mask) > INET6_ADDRSTRLEN,
+	    "mask buffer too small for AF_INET6");
+	_Static_assert(sizeof(mask) > INET_ADDRSTRLEN,
+	    "mask buffer too small for AF_INET");
+	struct in_addr a;
+	struct in6_addr a6;
+	uint8_t *u;
+	long num;
+
+	/* copy the address */
+	dst = addr;
+	while (str < end && !res_space(*str) && *str != '/') {
+		*dst++ = *str++;
+		if (dst == addr + sizeof(addr))
+			return (0);
+	}
+	*dst = '\0';
+
+	/* copy the mask, if there is one */
+	dst = mask;
+	if (str < end) {
+		if (*str++ != '/')
+			return (0);
+		if (end - str >= sizeof(mask))
+			return (0);
+		memcpy(mask, str, end - str);
+		dst += end - str;
+	}
+	*dst = '\0';
+
+	/* IPv4 case */
+	if (inet_pton(AF_INET, addr, &a) == 1) {
+		statp->sort_list[i].addr = a;
+		if (strchr(mask, '.') &&
+		    inet_pton(AF_INET, mask, &a) == 1) {
+			statp->sort_list[i].mask = a.s_addr;
+		} else if (*mask != '\0') {
+			if ((num = strtol(mask, &numend, 10)) < 0 ||
+			    num > 32 || *numend != '\0')
+				return (0);
+			statp->sort_list[i].mask =
+			    num == 0 ? 0 :
+			    htonl(0xffffffffU << (32 - num));
+		} else if (IN_CLASSA(ntohl(a.s_addr))) {
+			statp->sort_list[i].mask =
+			    htonl(IN_CLASSA_NET);
+		} else if (IN_CLASSB(ntohl(a.s_addr))) {
+			statp->sort_list[i].mask =
+			    htonl(IN_CLASSB_NET);
+		} else if (IN_CLASSC(ntohl(a.s_addr))) {
+			statp->sort_list[i].mask =
+			    htonl(IN_CLASSC_NET);
+		} else {
+			statp->sort_list[i].mask =
+			    htonl(IN_NETMASK_DEFAULT);
+		}
+		ext->sort_list[i].af = AF_INET;
+		ext->sort_list[i].addr.ina =
+		    statp->sort_list[i].addr;
+		ext->sort_list[i].mask.ina.s_addr =
+		    statp->sort_list[i].mask;
+#ifdef DEBUG
+		inet_ntop(AF_INET, &statp->sort_list[i].addr,
+		    addr, sizeof(addr));
+		a.s_addr = statp->sort_list[i].mask;
+		inet_ntop(AF_INET, &a, mask, sizeof(mask));
+		printf(";; sortlist %s/%s\n", addr, mask);
+#endif
+		return (1);
+	}
+
+	/* IPv6 case */
+	if (inet_pton(AF_INET6, addr, &a6) == 1) {
+		statp->sort_list[i].addr.s_addr = INADDR_NONE;
+		statp->sort_list[i].mask = INADDR_NONE;
+		ext->sort_list[i].af = AF_INET6;
+		ext->sort_list[i].addr.in6a = a6;
+		if (*mask != '\0') {
+			/* prefix length */
+			if ((num = strtol(mask, &numend, 10)) < 0 ||
+			    num > 128 || *numend != '\0')
+				return (0);
+		} else {
+			num = 64;
+		}
+#ifdef DEBUG
+		inet_ntop(AF_INET6, &a6, addr, sizeof(addr));
+		printf(";; sortlist %s/%ld\n", addr, num);
+#endif
+		a6 = (struct in6_addr)IN6ADDR_ANY_INIT;
+		for (u = a6.s6_addr8; num > 0; num -= 8)
+			*u++ = num > 8 ? 0xffU : 0xffU << (8 - num);
+		ext->sort_list[i].mask.in6a = a6;
+		return (1);
+	}
+
+	/* invalid */
+	return (0);
+}
+#endif /* RESOLVSORT */
+
 /*%
  * Split the string up into words and interpret them as resolver options.
  */
@@ -775,21 +816,6 @@ res_setoptions(res_state statp, const char *options, const char *source)
 	}
 }
 
-#ifdef RESOLVSORT
-/* XXX - should really support CIDR which means explicit masks always. */
-static u_int32_t
-net_mask(struct in_addr in)		/*!< XXX - should really use system's version of this  */
-{
-	u_int32_t i = ntohl(in.s_addr);
-
-	if (IN_CLASSA(i))
-		return (htonl(IN_CLASSA_NET));
-	else if (IN_CLASSB(i))
-		return (htonl(IN_CLASSB_NET));
-	return (htonl(IN_CLASSC_NET));
-}
-#endif
-
 void
 freebsd15_res_rndinit(res_state statp)
 {
diff --git a/share/man/man5/resolver.5 b/share/man/man5/resolver.5
index c9e7526ab865..749ffba21fbd 100644
--- a/share/man/man5/resolver.5
+++ b/share/man/man5/resolver.5
@@ -100,20 +100,30 @@ for one of the domains.
 .Pp
 The search list is currently limited to six domains
 with a total of 256 characters.
-.It Sy sortlist
-Sortlist allows addresses returned by the resolver to be sorted.
-A sortlist is specified by IP address netmask pairs.
-If the netmask is not specified,
-it defaults to the historical Class A/B/C netmask of the net;
-this usage is deprecated.
-The IP address
-and network pairs are separated by slashes.
-Up to 10 pairs may
-be specified.
-E.g.,
+.It Cm sortlist Ar addr1 Ns Oo Li / Ns Ar plen1 Oc Op addr2 Ns Oo Li / Ns Ar plen2 Oc ...
+Sort order for query results in the form of a list of
+.Ar addr Ns Li / Ns Ar mask
+or
+.Ar addr Ns Li / Ns Ar plen
+entries, for instance:
+.Bd -literal
+sortlist 198.51.100.0/24 203.0.113.0/255.255.255.0 2001:db8::/32
+.Ed
+.Pp
+Masks are only supported for IPv4.
+If the mask or prefix length is omitted, it defaults to historical
+class-based addressing for IPv4 and to 64 for IPv6.
+.Pp
+Query results that match one of the sort list entries are sorted
+accordingly and ahead of query results that don't.
+If multiple results match a single sort list entry, the order is
+unspecified.
 .Pp
-.Dl "sortlist 10.9.1.0/255.255.240.0 10.9.0.0/255.255.0.0"
-.It Cm options Ar option1 Op Ar option2 ...
+If multiple
+.Cm sortlist
+lines are encountered, entries are appended to the list in the order
+in which they were encountered, up to a maximum of ten.
+.It Cm options
 Options allows certain internal resolver variables to be modified.
 The following options are supported:
 .Bl -tag -width indent