2017-04-17-0-apple-clang-elides-bounds-check.md (6437B)
1 Problem 2 ======= 3 4 Some bounds checks are elided by Apple's compiler and possibly others, leading 5 to a possible attack especially in 32-bit builds. 6 7 Although triggered by a compiler optimization, this is a bug in Cap'n Proto, 8 not the compiler. 9 10 Discovered by 11 ============= 12 13 Kenton Varda <kenton@cloudflare.com> <kenton@sandstorm.io> 14 15 Announced 16 ========= 17 18 2017-04-17 19 20 CVE 21 === 22 23 CVE-2017-7892 24 25 Impact 26 ====== 27 28 - Remotely segfault a 32-bit application by sending it a malicious message. 29 - Exfiltration of memory from such applications **might** be possible, but our 30 current analysis indicates that other checks would cause any such attempt to 31 fail (see below). 32 - At present I've only reproduced the problem on Mac OS using Apple's 33 compiler. Other compilers and platforms do not seem to apply the problematic 34 optimization. 35 36 Fixed in 37 ======== 38 39 - git commit [52bc956459a5e83d7c31be95763ff6399e064ae4][0] 40 - release 0.5.3.1: 41 - Unix: https://capnproto.org/capnproto-c++-0.5.3.1.tar.gz 42 - Windows: https://capnproto.org/capnproto-c++-win32-0.5.3.1.zip 43 - release 0.6 (future) 44 45 [0]: https://github.com/sandstorm-io/capnproto/commit/52bc956459a5e83d7c31be95763ff6399e064ae4 46 47 Details 48 ======= 49 50 *The following text contains speculation about the exploitability of this 51 bug. This is provided for informational purposes, but as such speculation is 52 often shown to be wrong, you should not rely on the accuracy of this 53 section for the safety of your service. Please update your library.* 54 55 During regular testing in preparation for a release, it was discovered that 56 when Cap'n Proto is built using the current version of Apple's Clang compiler 57 in 32-bit mode with optimizations enabled, it is vulnerable to attack via 58 specially-crafted malicious input, causing the application to read from an 59 invalid memory location and crash. 60 61 Specifically, a malicious message could contain a [far pointer][1] pointing 62 outside of the message. Cap'n Proto messages are broken into segments of 63 continuous memory. A far pointer points from one segment into another, 64 indicating both the segment number and an offset within that segment. If a 65 malicious far pointer contained an offset large enough to overflow the pointer 66 arithmetic (wrapping around to the beginning of memory), then it would escape 67 bounds checking, in part because the compiler would elide half of the bounds 68 check as an optimization. 69 70 That is, the code looked like (simplification): 71 72 word* target = segmentStart + farPointer.offset; 73 if (target < segmentStart || target >= segmentEnd) { 74 throwBoundsError(); 75 } 76 doSomething(*target); 77 78 To most observers, this code would appear to be correct. However, as it turns 79 out, pointer arithmetic that overflows is undefined behavior under the C 80 standard. As a result, the compiler is allowed to assume that the addition on 81 the first line never overflows. Since `farPointer.offset` is an unsigned 82 number, the compiler is able to conclude that `target < segmentStart` always 83 evaluates false. Thus, the compiler removes this part of the check. 84 Unfortunately, in the case of overflow, this is exactly the part of the check 85 that we need. 86 87 Based on both fuzz testing and logical analysis, I believe that the far pointer 88 bounds check is the only check affected by this optimization. If true, then it 89 is not possible to exfiltrate memory through this vulnerability: the target of 90 a far pointer is always expected to be another pointer, which in turn points to 91 the final object. That second pointer will go through its own bounds checking, 92 which will fail (unless it somehow happens to point back into the message 93 bounds, in which case no harm is done). 94 95 I believe this bug does not affect any common 64-bit platform because the 96 maximum offset expressible by a far pointer is 2^32 bytes. In order to trigger 97 the bug in a 64-bit address space, the message location would have to begin 98 with 0xFFFFFFFF (allocated in the uppermost 4GB of address space) and the 99 target would have to begin with 0x00000000 (allocated in the lowermost 4GB of 100 address space). Typically, on 64-bit architectures, the upper half of the 101 address space is reserved for the OS kernel, thus a message could not possibly 102 be located there. 103 104 I was not able to reproduce this bug on other platforms, perhaps because the 105 compiler optimization is not applied by other compilers. On Linux, I tested GCC 106 4.9, 5.4, and 6.3, as well as Clang 3.6, 3.8, and 4.0. None were affected. 107 Nevertheless, the compiler behavior is technically allowed, thus it should be 108 assumed that it can happen on other platforms as well. 109 110 The specific compiler version which was observed to be affected is: 111 112 $ clang++ --version 113 Apple LLVM version 8.1.0 (clang-802.0.41) 114 Target: x86_64-apple-darwin16.5.0 115 Thread model: posix 116 InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin 117 118 (Note: Despite being Clang-based, Apple's compiler version numbers have no 119 apparent relationship to Clang version numbers.) 120 121 [1]: https://capnproto.org/encoding.html#inter-segment-pointers 122 123 Preventative measures 124 ===================== 125 126 The problem was caught by running Cap'n Proto's standard fuzz tests in this 127 configuration. These tests are part of the Cap'n Proto test suite which runs 128 when you invoke `make check`, which Cap'n Proto's installation instructions 129 suggest to all users. 130 131 However, these fuzz tests were introduced after the 0.5.x release branch, 132 hence are not currently present in release versions of Cap'n Proto, only in 133 git. A 0.6 release will come soon, fixing this. 134 135 The bugfix commit forces the compiler to perform all checks by casting the 136 relevant pointers to `uintptr_t`. According to the standard, unsigned integers 137 implement modulo arithmetic, rather than leaving overflow undefined, thus the 138 compiler cannot make the assumptions that lead to eliding the check. This 139 change has been shown to fix the problem in practice. However, this quick fix 140 does not technically avoid undefined behavior, as the code still computes 141 pointers that point to invalid locations before they are checked. A 142 technically-correct solution has been implemented in the next commit, 143 [2ca8e41140ebc618b8fb314b393b0a507568cf21][2]. However, as this required more 144 extensive refactoring, it is not appropriate for cherry-picking, and will 145 only land in versions 0.6 and up. 146 147 [2]: https://github.com/sandstorm-io/capnproto/commit/2ca8e41140ebc618b8fb314b393b0a507568cf21