hostname-setup: read the whole wordlist into memory when picking words

hostname_pick_word() used to seek around in the wordlist file and read
individual lines with read_line()/read_stripped_line(). Instead, slurp the
whole file into memory use offset arithmetic. The wordlists are small, so
reading them entirely is faster and there is really no reason to do the
complicated thing. The new code is 15% faster (the slow parts are the
hashing and chaseat, they actually dominate over the reading of the data).
The "raced with truncation" case is gone, since the buffer is a consistent
snapshot of the file.

The random offsets derived from the machine ID are unchanged, so the
picked words stay the same.
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-09-01 15:57:58 +02:00
parent 00d512bdce
commit c737b297a3
+73 -54
View File
@@ -331,41 +331,68 @@ static bool normalize_and_validate_word(char *word) {
return hostname_is_valid(word, /* flags= */ 0);
}
static int pick_word_linear_scan(FILE *f, off_t offset, char **ret) {
int r;
assert(f);
static int pick_word_at(const char *words, size_t size, size_t offset, char **ret) {
assert(words);
assert(offset <= size);
assert(ret);
if (fseeko(f, offset, SEEK_SET) < 0)
return -errno;
/* Extracts the line starting at 'offset', strips and validates it. Returns 1 and the word on
* success, 0 if the line is a comment, empty, or not a valid hostname word. */
bool wrapped = false;
r = read_line(f, LONG_LINE_MAX, NULL); /* discard the partial line we landed in */
if (r < 0)
return r;
if (r == 0) {
wrapped = true;
rewind(f);
const char *p = words + offset;
size_t len = MIN(size - offset, LONG_LINE_MAX + 1);
const char *e = memchr(p, '\n', len);
if (e)
len = e - p;
if (len > LONG_LINE_MAX)
return -EUCLEAN;
/* Strip leading and trailing whitespace */
while (len > 0 && strchr(WHITESPACE, p[0])) {
p++;
len--;
}
while (len > 0 && strchr(WHITESPACE, p[len - 1]))
len--;
_cleanup_free_ char *word = strndup(p, len);
if (!word)
return -ENOMEM;
bool good = normalize_and_validate_word(word);
*ret = good ? TAKE_PTR(word) : NULL;
return good;
}
static int pick_word_linear_scan(const char *words, size_t size, size_t offset, char **ret) {
int r;
assert(words);
assert(size > 0);
assert(offset <= size);
assert(ret);
/* Skip the partial line we landed in */
bool wrapped = false;
const char *e = memchr(words + offset, '\n', size - offset);
offset = e ? (size_t) (e - words) + 1 : size;
for (;;) {
_cleanup_free_ char *line = NULL;
r = read_stripped_line(f, LONG_LINE_MAX, &line);
if (r < 0)
return r;
if (r == 0) { /* hit EOF: we started at a random offset, wrap around to the beginning */
if (offset >= size) { /* hit EOF: we started at a random offset, wrap around to the beginning */
if (wrapped) /* already wrapped once, the file contains no usable word at all */
return -ENOENT;
wrapped = true;
rewind(f);
continue;
}
if (normalize_and_validate_word(line)) {
*ret = TAKE_PTR(line);
return 0;
offset = 0;
}
r = pick_word_at(words, size, offset, ret);
if (r != 0)
return r < 0 ? r : 0;
/* Advance to the next line */
e = memchr(words + offset, '\n', size - offset);
offset = e ? (size_t) (e - words) + 1 : size;
}
}
@@ -391,55 +418,47 @@ int hostname_pick_word(sd_id128_t mid, size_t pos, char **ret) {
r = stat_verify_regular(&st);
if (r < 0)
return r;
if (st.st_size == 0)
_cleanup_free_ char *words = NULL;
size_t size;
r = read_full_stream(f, &words, &size);
if (r < 0)
return r;
if (size == 0)
return -ENOENT;
/* Pick a word without reading the whole list into memory:
* 1. pick a random offset in the file [0 … st.st_size-1]
* 2. if offset is zero, read a full line from the beginning of the file, use that.
* 3. otherwise, seek to offset minus 1 and read one character.
* 4. if that character is newline, then read a full line after it, and use that as result
* 5. otherwise, goto 1
/* Pick a word:
* 1. pick a random offset in the buffer [0 … size-1]
* 2. if the offset is zero or the preceding byte is a newline, it starts a line: use that line
* 3. otherwise, goto 1
*
* As a safety net terminate after a fixed number iterations (for pathological wordlists)
* This stream is independent of the '?' nibble stream so pure-'?' * templates keep producing
* This stream is independent of the '?' nibble stream so pure-'?' templates keep producing
* byte-identical output. Stable as long as the wordlist is stable. */
off_t offset = 0;
size_t offset = 0;
const unsigned int MAX_ITERATIONS = 64;
for (unsigned i = 0; i < MAX_ITERATIONS; i++) {
_cleanup_free_ char *line = NULL;
struct siphash state;
siphash24_init(&state, word_key.bytes);
siphash24_compress_typesafe(mid, &state);
siphash24_compress_typesafe(pos, &state);
siphash24_compress_typesafe(i, &state); /* counter mode */
offset = (off_t) (siphash24_finalize(&state) % (uint64_t) st.st_size);
offset = (size_t) (siphash24_finalize(&state) % (uint64_t) size);
if (offset > 0) {
if (fseeko(f, offset - 1, SEEK_SET) < 0)
return -errno;
if (fgetc(f) != '\n')
continue; /* not a line start */
} else if (fseeko(f, 0, SEEK_SET) < 0) /* offset 0 always begins the first line */
return -errno;
if (offset > 0 && words[offset - 1] != '\n')
continue; /* not a line start */
r = pick_word_at(words, size, offset, ret);
if (r != 0)
return r < 0 ? r : 0;
r = read_stripped_line(f, LONG_LINE_MAX, &line);
if (r < 0)
return r;
if (r == 0) /* raced with truncation */
continue;
if (normalize_and_validate_word(line)) {
*ret = TAKE_PTR(line);
return 0;
}
/* Comment/empty/invalid line: resample rather than advancing, to keep the pick uniform. */
}
/* We exhausted the uniform attempts, this should never happen but if it does fallback to picking the
* next word after our last attempt. */
log_warning("hostname_pick_word did not find a usable word after %u in wordlist %zu", MAX_ITERATIONS, pos);
return pick_word_linear_scan(f, offset, ret);
return pick_word_linear_scan(words, size, offset, ret);
}
int hostname_substitute_wildcards(const char *name, char **ret) {