|
| 1 | +>From 228da39e38c1cae13cbe637e771412c1984dba5d Mon Sep 17 00:00:00 2001 |
| 2 | +From: Rich Felker <dalias@aerifal.cx> |
| 3 | +Date: Thu, 9 Apr 2026 22:51:30 -0400 |
| 4 | +Subject: [PATCH 1/3] qsort: fix leonardo heap corruption from bug in |
| 5 | + doubleword ctz primitive |
| 6 | + |
| 7 | +the pntz function, implementing a "count trailing zeros" variant for a |
| 8 | +bit vector consisting of two size_t words, erroneously returned zero |
| 9 | +rather than the number of bits in the low word when the first bit set |
| 10 | +was the low bit of the high word. |
| 11 | + |
| 12 | +as a result, a loop in the trinkle function which should have a |
| 13 | +guaranteed small bound on the number of iterations, could run |
| 14 | +unboundedly, thereby overflowing a stack-based working-space array |
| 15 | +which was sized for the bound. |
| 16 | + |
| 17 | +CVE-2026-40200 has been assigned for this issue. |
| 18 | +--- |
| 19 | + src/stdlib/qsort.c | 8 ++++---- |
| 20 | + 1 file changed, 4 insertions(+), 4 deletions(-) |
| 21 | + |
| 22 | +diff --git a/src/stdlib/qsort.c b/src/stdlib/qsort.c |
| 23 | +index ab79dc6f..13219ab3 100644 |
| 24 | +--- a/src/stdlib/qsort.c |
| 25 | ++++ b/src/stdlib/qsort.c |
| 26 | +@@ -34,11 +34,11 @@ |
| 27 | + |
| 28 | + typedef int (*cmpfun)(const void *, const void *, void *); |
| 29 | + |
| 30 | ++/* returns index of first bit set, excluding the low bit assumed to always |
| 31 | ++ * be set, starting from low bit of p[0] up through high bit of p[1] */ |
| 32 | + static inline int pntz(size_t p[2]) { |
| 33 | +- int r = ntz(p[0] - 1); |
| 34 | +- if(r != 0 || (r = 8*sizeof(size_t) + ntz(p[1])) != 8*sizeof(size_t)) { |
| 35 | +- return r; |
| 36 | +- } |
| 37 | ++ if (p[0] != 1) return ntz(p[0] - 1); |
| 38 | ++ if (p[1]) return 8*sizeof(size_t) + ntz(p[1]); |
| 39 | + return 0; |
| 40 | + } |
| 41 | + |
| 42 | +-- |
| 43 | +2.21.0 |
| 44 | + |
| 45 | + |
| 46 | +>From b3291b9a9f77f1f993d2b4f8c68a26cf09221ae7 Mon Sep 17 00:00:00 2001 |
| 47 | +From: Rich Felker <dalias@aerifal.cx> |
| 48 | +Date: Thu, 9 Apr 2026 23:40:53 -0400 |
| 49 | +Subject: [PATCH 2/3] qsort: hard-preclude oob array writes independent of any |
| 50 | + invariants |
| 51 | + |
| 52 | +while the root cause of CVE-2026-40200 was a faulty ctz primitive, the |
| 53 | +fallout of the bug would have been limited to erroneous sorting or |
| 54 | +infinite loop if not for the stores to a stack-based array that |
| 55 | +depended on trusting invariants in order not to go out of bounds. |
| 56 | + |
| 57 | +increase the size of the array to a power of two so that we can mask |
| 58 | +indices into it to force them into range. in the absence of any |
| 59 | +further bug, the masking is a no-op, but it does not have any |
| 60 | +measurable performance cost, and it makes spatial memory safety |
| 61 | +trivial to prove (and for readers not familiar with the algorithms to |
| 62 | +trust). |
| 63 | +--- |
| 64 | + src/stdlib/qsort.c | 20 +++++++++++++------- |
| 65 | + 1 file changed, 13 insertions(+), 7 deletions(-) |
| 66 | + |
| 67 | +diff --git a/src/stdlib/qsort.c b/src/stdlib/qsort.c |
| 68 | +index 13219ab3..e4bce9f7 100644 |
| 69 | +--- a/src/stdlib/qsort.c |
| 70 | ++++ b/src/stdlib/qsort.c |
| 71 | +@@ -89,10 +89,16 @@ static inline void shr(size_t p[2], int n) |
| 72 | + p[1] >>= n; |
| 73 | + } |
| 74 | + |
| 75 | ++/* power-of-two length for working array so that we can mask indices and |
| 76 | ++ * not depend on any invariant of the algorithm for spatial memory safety. |
| 77 | ++ * the original size was just 14*sizeof(size_t)+1 */ |
| 78 | ++#define AR_LEN (16 * sizeof(size_t)) |
| 79 | ++#define AR_MASK (AR_LEN - 1) |
| 80 | ++ |
| 81 | + static void sift(unsigned char *head, size_t width, cmpfun cmp, void *arg, int pshift, size_t lp[]) |
| 82 | + { |
| 83 | + unsigned char *rt, *lf; |
| 84 | +- unsigned char *ar[14 * sizeof(size_t) + 1]; |
| 85 | ++ unsigned char *ar[AR_LEN]; |
| 86 | + int i = 1; |
| 87 | + |
| 88 | + ar[0] = head; |
| 89 | +@@ -104,16 +110,16 @@ static void sift(unsigned char *head, size_t width, cmpfun cmp, void *arg, int p |
| 90 | + break; |
| 91 | + } |
| 92 | + if(cmp(lf, rt, arg) >= 0) { |
| 93 | +- ar[i++] = lf; |
| 94 | ++ ar[i++ & AR_MASK] = lf; |
| 95 | + head = lf; |
| 96 | + pshift -= 1; |
| 97 | + } else { |
| 98 | +- ar[i++] = rt; |
| 99 | ++ ar[i++ & AR_MASK] = rt; |
| 100 | + head = rt; |
| 101 | + pshift -= 2; |
| 102 | + } |
| 103 | + } |
| 104 | +- cycle(width, ar, i); |
| 105 | ++ cycle(width, ar, i & AR_MASK); |
| 106 | + } |
| 107 | + |
| 108 | + static void trinkle(unsigned char *head, size_t width, cmpfun cmp, void *arg, size_t pp[2], int pshift, int trusty, size_t lp[]) |
| 109 | +@@ -121,7 +127,7 @@ static void trinkle(unsigned char *head, size_t width, cmpfun cmp, void *arg, si |
| 110 | + unsigned char *stepson, |
| 111 | + *rt, *lf; |
| 112 | + size_t p[2]; |
| 113 | +- unsigned char *ar[14 * sizeof(size_t) + 1]; |
| 114 | ++ unsigned char *ar[AR_LEN]; |
| 115 | + int i = 1; |
| 116 | + int trail; |
| 117 | + |
| 118 | +@@ -142,7 +148,7 @@ static void trinkle(unsigned char *head, size_t width, cmpfun cmp, void *arg, si |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | +- ar[i++] = stepson; |
| 123 | ++ ar[i++ & AR_MASK] = stepson; |
| 124 | + head = stepson; |
| 125 | + trail = pntz(p); |
| 126 | + shr(p, trail); |
| 127 | +@@ -150,7 +156,7 @@ static void trinkle(unsigned char *head, size_t width, cmpfun cmp, void *arg, si |
| 128 | + trusty = 0; |
| 129 | + } |
| 130 | + if(!trusty) { |
| 131 | +- cycle(width, ar, i); |
| 132 | ++ cycle(width, ar, i & AR_MASK); |
| 133 | + sift(head, width, cmp, arg, pshift, lp); |
| 134 | + } |
| 135 | + } |
| 136 | +-- |
| 137 | +2.21.0 |
| 138 | + |
| 139 | + |
| 140 | +>From 5122f9f3c99fee366167c5de98b31546312921ab Mon Sep 17 00:00:00 2001 |
| 141 | +From: Luca Kellermann <mailto.luca.kellermann@gmail.com> |
| 142 | +Date: Fri, 10 Apr 2026 03:03:22 +0200 |
| 143 | +Subject: [PATCH 3/3] qsort: fix shift UB in shl and shr |
| 144 | + |
| 145 | +if shl() or shr() are called with n==8*sizeof(size_t), n is adjusted |
| 146 | +to 0. the shift by (sizeof(size_t) * 8 - n) that then follows will |
| 147 | +consequently shift by the width of size_t, which is UB and in practice |
| 148 | +produces an incorrect result. |
| 149 | + |
| 150 | +return early in this case. the bitvector p was already shifted by the |
| 151 | +required amount. |
| 152 | +--- |
| 153 | + src/stdlib/qsort.c | 2 ++ |
| 154 | + 1 file changed, 2 insertions(+) |
| 155 | + |
| 156 | +diff --git a/src/stdlib/qsort.c b/src/stdlib/qsort.c |
| 157 | +index e4bce9f7..28607450 100644 |
| 158 | +--- a/src/stdlib/qsort.c |
| 159 | ++++ b/src/stdlib/qsort.c |
| 160 | +@@ -71,6 +71,7 @@ static inline void shl(size_t p[2], int n) |
| 161 | + n -= 8 * sizeof(size_t); |
| 162 | + p[1] = p[0]; |
| 163 | + p[0] = 0; |
| 164 | ++ if (!n) return; |
| 165 | + } |
| 166 | + p[1] <<= n; |
| 167 | + p[1] |= p[0] >> (sizeof(size_t) * 8 - n); |
| 168 | +@@ -83,6 +84,7 @@ static inline void shr(size_t p[2], int n) |
| 169 | + n -= 8 * sizeof(size_t); |
| 170 | + p[0] = p[1]; |
| 171 | + p[1] = 0; |
| 172 | ++ if (!n) return; |
| 173 | + } |
| 174 | + p[0] >>= n; |
| 175 | + p[0] |= p[1] << (sizeof(size_t) * 8 - n); |
| 176 | +-- |
| 177 | +2.21.0 |
| 178 | + |
| 179 | + |
0 commit comments