The word parser in tsort(1) does not treat null bytes as word
delimiters. When input contains embedded null bytes, they become
part of node name strings passed to ohash.
This causes a heap buffer overread in ohash_lookup_interval()
at lib/libutil/ohash.c line 244:
strncmp(stored_key, start, end - start) == 0 &&
stored_key[end-start] == '\0'
When a stored key contains an embedded null (e.g., "a\0bc", 4
bytes), strncmp treats the null as end-of-string and returns 0
for any lookup key sharing the same prefix (e.g., "a\0xyz", 5
bytes). The check stored_key[end-start] then reads past the
stored key's allocation.
Fix: treat null bytes as word delimiters in read_pairs(), the
same way whitespace characters are handled. This ensures no
node name contains embedded nulls, which is consistent with
how ohash uses strncmp for key comparison.
Found by AFL++ fuzzing.
Index: usr.bin/tsort/tsort.c
===================================================================
RCS file: /cvs/src/usr.bin/tsort/tsort.c,v
retrieving revision 1.38
diff -u -p -r1.38 tsort.c
--- usr.bin/tsort/tsort.c 18 Jan 2024 15:34:29 -0000 1.38
+++ usr.bin/tsort/tsort.c
@@ -318,12 +318,13 @@ read_pairs(FILE *f, struct ohash *h, int
char *e;
while (str < sentinel &&
- isspace((unsigned char)*str))
+ (isspace((unsigned char)*str) || *str == '\0'))
str++;
if (str == sentinel)
break;
for (e = str;
- e < sentinel && !isspace((unsigned char)*e); e++)
+ e < sentinel && !isspace((unsigned char)*e) &&
+ *e != '\0'; e++)
continue;
if (toggle) {
a = node_lookup(h, str, e);