Author: gbechis
Date: Sun Aug 16 09:49:10 2026
New Revision: 1937150
Log:
randomize ip addresses returned by `getaddrinfo`
bz #7046
Added:
spamassassin/trunk/t/spamc_randomize_hosts.t
Modified:
spamassassin/trunk/MANIFEST
spamassassin/trunk/spamc/libspamc.c
Modified: spamassassin/trunk/MANIFEST
==============================================================================
--- spamassassin/trunk/MANIFEST Sun Aug 16 09:09:17 2026 (r1937149)
+++ spamassassin/trunk/MANIFEST Sun Aug 16 09:49:10 2026 (r1937150)
@@ -756,6 +756,7 @@ t/spamc_headers.t
t/spamc_l.t
t/spamc_optC.t
t/spamc_optL.t
+t/spamc_randomize_hosts.t
t/spamc_x_E_R.t
t/spamc_x_e.t
t/spamc_y.t
Modified: spamassassin/trunk/spamc/libspamc.c
==============================================================================
--- spamassassin/trunk/spamc/libspamc.c Sun Aug 16 09:09:17 2026 (r1937149)
+++ spamassassin/trunk/spamc/libspamc.c Sun Aug 16 09:49:10 2026 (r1937150)
@@ -2049,13 +2049,89 @@ void transport_init(struct transport *tp
}
/*
+* _ensure_rand_seeded()
+*
+* always seed rand()
+*/
+static void _ensure_rand_seeded(void)
+{
+ static int rand_seeded = 0;
+
+ if (!rand_seeded) {
+ rand_seeded = 1;
+ srand(((unsigned int) getpid()) ^ ((unsigned int) time(NULL)) ^
+ ((unsigned int) (size_t) &rand_seeded));
+ }
+}
+
+/*
+* _random_below()
+*
+* Return a uniformly-distributed random number in [0, bound).
+*/
+static unsigned int _random_below(unsigned int bound)
+{
+ unsigned int limit;
+ unsigned int r;
+
+ if (bound == 0)
+ return 0;
+
+ /* largest multiple of "bound" that is <= RAND_MAX+1 */
+ limit = ((unsigned int) RAND_MAX + 1u) - (((unsigned int) RAND_MAX + 1u) % bound);
+
+ do {
+ r = (unsigned int) rand();
+ } while (limit != 0 && r >= limit);
+
+ return r % bound;
+}
+
+#ifdef SPAMC_HAS_ADDRINFO
+/*
+* _randomize_addrinfo_chain()
+*
+* A single hostname can resolve to several A/AAAA records,
+* shuffle the ip addresses list too.
+*/
+static void _randomize_addrinfo_chain(struct addrinfo **head)
+{
+ struct addrinfo *nodes[TRANSPORT_MAX_HOSTS];
+ struct addrinfo *cur;
+ int count = 0;
+ int i;
+
+ if (head == NULL || *head == NULL)
+ return;
+
+ for (cur = *head; cur != NULL && count < TRANSPORT_MAX_HOSTS; cur = cur->ai_next)
+ nodes[count++] = cur;
+
+ if (count <= 1)
+ return;
+
+ for (i = count - 1; i > 0; i--) {
+ unsigned int j = _random_below((unsigned int) i + 1);
+ struct addrinfo *tmp = nodes[i];
+ nodes[i] = nodes[j];
+ nodes[j] = tmp;
+ }
+
+ for (i = 0; i < count - 1; i++) {
+ nodes[i]->ai_next = nodes[i + 1];
+ }
+ nodes[count - 1]->ai_next = NULL;
+
+ *head = nodes[0];
+}
+#endif
+
+/*
* randomize_hosts()
*
-* Given the transport object that contains one or more IP addresses
-* in this "hosts" list, rotate it by a random number of shifts to
-* randomize them - this is a kind of load balancing. It's possible
-* that the random number will be 0, which says not to touch. We don't
-* do anything unless
+* Given the transport object that contains one or more hosts in this
+* "hosts" list, shuffle it into a uniformly random order - this is a
+* kind of load balancing.
*/
static void _randomize_hosts(struct transport *tp)
@@ -2066,22 +2142,25 @@ static void _randomize_hosts(struct tran
struct in_addr tmp;
#endif
int i;
- int rnum;
assert(tp != 0);
- if (tp->nhosts <= 1)
- return;
-
- rnum = rand() % tp->nhosts;
+ _ensure_rand_seeded();
- while (rnum-- > 0) {
- tmp = tp->hosts[0];
+#ifdef SPAMC_HAS_ADDRINFO
+ for (i = 0; i < tp->nhosts; i++) {
+ _randomize_addrinfo_chain(&tp->hosts[i]);
+ }
+#endif
- for (i = 1; i < tp->nhosts; i++)
- tp->hosts[i - 1] = tp->hosts[i];
+ if (tp->nhosts <= 1)
+ return;
- tp->hosts[i - 1] = tmp;
+ for (i = tp->nhosts - 1; i > 0; i--) {
+ unsigned int j = _random_below((unsigned int) i + 1);
+ tmp = tp->hosts[i];
+ tp->hosts[i] = tp->hosts[j];
+ tp->hosts[j] = tmp;
}
}
@@ -2326,13 +2405,14 @@ nexthost:
/* QUASI-LOAD-BALANCING
*
- * If the user wants to do quasi load balancing, "rotate"
- * the list by a random amount based on the current time.
- * This may later be truncated to a single item. This is
- * meaningful only if we have more than one host.
+ * If the user wants to do quasi load balancing, shuffle the
+ * host list into a random order. This may later be truncated
+ * to a single item. Note this is still meaningful with just one
+ * configured hostname: on addrinfo-capable builds it may still
+ * resolve to several IPs, whose order also gets shuffled.
*/
- if ((flags & SPAMC_RANDOMIZE_HOSTS) && tp->nhosts > 1) {
+ if (flags & SPAMC_RANDOMIZE_HOSTS) {
_randomize_hosts(tp);
}
Added: spamassassin/trunk/t/spamc_randomize_hosts.t
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ spamassassin/trunk/t/spamc_randomize_hosts.t Sun Aug 16 09:49:10 2026 (r1937150)
@@ -0,0 +1,157 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("spamc_randomize_hosts");
+use Test::More;
+
+use File::Temp qw(tempdir);
+use Config;
+use Cwd qw(abs_path);
+
+# ---------------------------------------------------------------------------
+# Regression test for the spamc -H (SPAMC_RANDOMIZE_HOSTS) host/IP shuffle
+# in spamc/libspamc.c.
+# ---------------------------------------------------------------------------
+
+unless (-f "../spamc/config.h") {
+ plan skip_all => "../spamc/config.h not found; run spamc's configure first";
+ exit;
+}
+
+my $cc = untaint_var($Config{cc} || 'cc');
+
+plan tests => 4;
+
+my $workdir = tempdir("spamc_randomize_hosts.XXXXXX", DIR => "log");
+my $src = "$workdir/randomize_hosts_test.c";
+my $bin = "$workdir/randomize_hosts_test";
+
+my $libspamc_c = untaint_var(abs_path("../spamc/libspamc.c"));
+
+open(my $fh, '>', $src) or die "cannot write $src: $!";
+print $fh qq{#include "$libspamc_c"\n};
+print $fh <<'EOC';
+#include <stdio.h>
+#include <string.h>
+
+static struct addrinfo *make_chain(int n, int tag_base) {
+ struct addrinfo *head = NULL, *tail = NULL;
+ int i;
+ for (i = 0; i < n; i++) {
+ struct addrinfo *a = calloc(1, sizeof(*a));
+ a->ai_addrlen = tag_base + i; /* identity tag, unused otherwise */
+ a->ai_next = NULL;
+ if (!head) head = tail = a;
+ else { tail->ai_next = a; tail = a; }
+ }
+ return head;
+}
+
+static int chain_len(struct addrinfo *a) {
+ int n = 0;
+ while (a) { n++; a = a->ai_next; }
+ return n;
+}
+
+static void chain_tags(struct addrinfo *a, int *out) {
+ int i = 0;
+ while (a) { out[i++] = (int) a->ai_addrlen; a = a->ai_next; }
+}
+
+int main(void) {
+ struct transport tp;
+ int i, trial;
+ int perm_codes[6];
+ int nperm_codes = 0;
+ int chain_order_seen = 0;
+ int orig_tags[3], cur_tags[3];
+ int nhosts_ok = 1, chainlen_ok = 1;
+
+ memset(&tp, 0, sizeof(tp));
+ tp.nhosts = 3;
+ for (i = 0; i < 3; i++)
+ tp.hosts[i] = make_chain(3, i * 10);
+
+ {
+ struct addrinfo *fresh = make_chain(3, 0);
+ chain_tags(fresh, orig_tags);
+ }
+
+ for (trial = 0; trial < 20000; trial++) {
+ int idx[3], code, k, slot;
+
+ _randomize_hosts(&tp);
+
+ if (tp.nhosts != 3)
+ nhosts_ok = 0;
+
+ for (i = 0; i < 3; i++) {
+ if (chain_len(tp.hosts[i]) != 3) {
+ chainlen_ok = 0;
+ }
+ }
+
+ for (i = 0; i < 3; i++) {
+ idx[i] = ((int) tp.hosts[i]->ai_addrlen) / 10;
+ }
+
+ code = idx[0]*9 + idx[1]*3 + idx[2];
+ slot = -1;
+ for (k = 0; k < nperm_codes; k++) {
+ if (perm_codes[k] == code) {
+ slot = k;
+ break;
+ }
+ }
+ if (slot == -1 && nperm_codes < 6) {
+ perm_codes[nperm_codes++] = code;
+ }
+
+ for (i = 0; i < 3; i++) {
+ if (idx[i] == 0) {
+ chain_tags(tp.hosts[i], cur_tags);
+ if (memcmp(cur_tags, orig_tags, sizeof(orig_tags)) != 0) {
+ chain_order_seen = 1;
+ }
+ break;
+ }
+ }
+ }
+
+ printf("nhosts_unchanged=%d\n", nhosts_ok);
+ printf("chainlen_unchanged=%d\n", chainlen_ok);
+ printf("distinct_permutations=%d\n", nperm_codes);
+ printf("chain_order_varied=%d\n", chain_order_seen);
+
+ return 0;
+}
+EOC
+close $fh;
+
+my $build_status = untaint_system($cc, '-I', '../spamc', '-DHAVE_CONFIG_H',
+ '-o', $bin, $src, '../spamc/utils.c', '-lz');
+
+ok(($build_status == 0), "compile randomize_hosts test harness")
+ or diag("build of $src failed (status=$build_status)");
+
+SKIP: {
+ skip "harness did not build", 3 if $build_status != 0;
+
+ my $out = untaint_cmd($bin);
+ my %v;
+ while ($out =~ /^(\w+)=(\d+)$/mg) {
+ $v{$1} = $2;
+ }
+
+ ok(($v{nhosts_unchanged} && $v{chainlen_unchanged}),
+ "shuffle does not lose or duplicate hosts/IPs")
+ or diag("harness output:\n$out");
+
+ is($v{distinct_permutations}, 6,
+ "all 3! top-level host orderings are reachable (not just a rotation)")
+ or diag("harness output:\n$out");
+
+ ok($v{chain_order_varied},
+ "IPs within a single hostname's addrinfo chain are also randomized")
+ or diag("harness output:\n$out");
+}
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.