From 82d852c69f406f157ed601d66229d9d917e21f83 Mon Sep 17 00:00:00 2001 From: Mariusz Borsa Date: Tue, 7 Feb 2023 14:19:54 -0800 Subject: [PATCH] [Sanitizers] Fix read buffer overrun in scanning loader commands The fix only affects Darwin, but to write the test I had to modify the MemoryMappingLayout class which is used by all OSes, to allow for mocking of image header (this change should be NFC). Hence no [Darwin] in the subject so I can get more eyes on it. While looking for a memory gap to put the shadow area into, the sanitizer code scans through the loaded images, and for each image it scans through its loader command to determine the occupied memory ranges. While doing so, if the 'segment load' (kLCSegment) loader comand is encountered, the command scanning function returns success (true), but does not decrement the command list iterator counter. The result is that the function is called again and again, with the iterator counter now being too high. The command scanner keeps updating the loader command pointer, by using the command size field. If the loop counter is too high, the command pointer lands into unintended area ( beyond +sizeof(mac_header64)+header->sizeofcmds ), and result depends on the random content found there. The random content interpreted as loader command might contain a large integer value in the cmdsize field - this value is added to the current loader command pointer, which might now point to an inaccessible memory address. It can occasionally result in a crash if it happens to run beyond the mapped memory segment. Note that when the area after the loader command list contains zeros or small integers only, the loop will end normally and the problem will go unnoticed. So it happened until now since having a some big value after the header area, falling into command size field is a pretty rare situation. The fix makes sure that the iterator counter gets updated when the segment load (kLCSegment) loader command is found too, and in the same code location so the updates will always go together. Undo the changes in the sanitizer_procmaps_mac.cpp to see the test failing. rdar://101161047 rdar://102819707 Differential Revision: https://reviews.llvm.org/D142164 --- .../lib/sanitizer_common/sanitizer_procmaps.h | 20 ++- .../sanitizer_common/sanitizer_procmaps_mac.cpp | 16 ++- .../lib/sanitizer_common/tests/CMakeLists.txt | 1 + .../tests/sanitizer_procmaps_mac_test.cpp | 137 +++++++++++++++++++++ 4 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 compiler-rt/lib/sanitizer_common/tests/sanitizer_procmaps_mac_test.cpp diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_procmaps.h b/compiler-rt/lib/sanitizer_common/sanitizer_procmaps.h index 19bad15..18940c5 100644 --- a/compiler-rt/lib/sanitizer_common/sanitizer_procmaps.h +++ b/compiler-rt/lib/sanitizer_common/sanitizer_procmaps.h @@ -65,6 +65,8 @@ class MemoryMappedSegment { MemoryMappedSegmentData *data_; }; +struct ImageHeader; + class MemoryMappingLayoutBase { public: virtual bool Next(MemoryMappedSegment *segment) { UNIMPLEMENTED(); } @@ -75,10 +77,18 @@ class MemoryMappingLayoutBase { ~MemoryMappingLayoutBase() {} }; -class MemoryMappingLayout final : public MemoryMappingLayoutBase { +class MemoryMappingLayout : public MemoryMappingLayoutBase { public: explicit MemoryMappingLayout(bool cache_enabled); + +// This destructor cannot be virtual, as it would cause an operator new() linking +// failures in hwasan test cases. However non-virtual destructors emit warnings +// in macOS build, hence disabling those +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-virtual-dtor" ~MemoryMappingLayout(); +#pragma clang diagnostic pop + virtual bool Next(MemoryMappedSegment *segment) override; virtual bool Error() const override; virtual void Reset() override; @@ -90,10 +100,14 @@ class MemoryMappingLayout final : public MemoryMappingLayoutBase { // Adds all mapped objects into a vector. void DumpListOfModules(InternalMmapVectorNoCtor *modules); + protected: +#if SANITIZER_APPLE + virtual const ImageHeader *CurrentImageHeader(); +#endif + MemoryMappingLayoutData data_; + private: void LoadFromCache(); - - MemoryMappingLayoutData data_; }; // Returns code range for the specified module. diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_procmaps_mac.cpp b/compiler-rt/lib/sanitizer_common/sanitizer_procmaps_mac.cpp index 4b0e678..b44e016a 100644 --- a/compiler-rt/lib/sanitizer_common/sanitizer_procmaps_mac.cpp +++ b/compiler-rt/lib/sanitizer_common/sanitizer_procmaps_mac.cpp @@ -250,7 +250,9 @@ static bool NextSegmentLoad(MemoryMappedSegment *segment, MemoryMappedSegmentData *seg_data, MemoryMappingLayoutData *layout_data) { const char *lc = layout_data->current_load_cmd_addr; + layout_data->current_load_cmd_addr += ((const load_command *)lc)->cmdsize; + layout_data->current_load_cmd_count--; if (((const load_command *)lc)->cmd == kLCSegment) { const SegmentCommand* sc = (const SegmentCommand *)lc; uptr base_virt_addr, addr_mask; @@ -358,11 +360,16 @@ static bool IsModuleInstrumented(const load_command *first_lc) { return false; } +const ImageHeader *MemoryMappingLayout::CurrentImageHeader() { + const mach_header *hdr = (data_.current_image == kDyldImageIdx) + ? get_dyld_hdr() + : _dyld_get_image_header(data_.current_image); + return (const ImageHeader *)hdr; +} + bool MemoryMappingLayout::Next(MemoryMappedSegment *segment) { for (; data_.current_image >= kDyldImageIdx; data_.current_image--) { - const mach_header *hdr = (data_.current_image == kDyldImageIdx) - ? get_dyld_hdr() - : _dyld_get_image_header(data_.current_image); + const mach_header *hdr = (const mach_header *)CurrentImageHeader(); if (!hdr) continue; if (data_.current_load_cmd_count < 0) { // Set up for this image; @@ -392,7 +399,7 @@ bool MemoryMappingLayout::Next(MemoryMappedSegment *segment) { (const load_command *)data_.current_load_cmd_addr); } - for (; data_.current_load_cmd_count >= 0; data_.current_load_cmd_count--) { + while (data_.current_load_cmd_count > 0) { switch (data_.current_magic) { // data_.current_magic may be only one of MH_MAGIC, MH_MAGIC_64. #ifdef MH_MAGIC_64 @@ -413,6 +420,7 @@ bool MemoryMappingLayout::Next(MemoryMappedSegment *segment) { } // If we get here, no more load_cmd's in this image talk about // segments. Go on to the next image. + data_.current_load_cmd_count = -1; // This will trigger loading next image } return false; } diff --git a/compiler-rt/lib/sanitizer_common/tests/CMakeLists.txt b/compiler-rt/lib/sanitizer_common/tests/CMakeLists.txt index 0c729ec..2ff6d46 100644 --- a/compiler-rt/lib/sanitizer_common/tests/CMakeLists.txt +++ b/compiler-rt/lib/sanitizer_common/tests/CMakeLists.txt @@ -35,6 +35,7 @@ set(SANITIZER_UNITTESTS sanitizer_posix_test.cpp sanitizer_printf_test.cpp sanitizer_procmaps_test.cpp + sanitizer_procmaps_mac_test.cpp sanitizer_ring_buffer_test.cpp sanitizer_quarantine_test.cpp sanitizer_stack_store_test.cpp diff --git a/compiler-rt/lib/sanitizer_common/tests/sanitizer_procmaps_mac_test.cpp b/compiler-rt/lib/sanitizer_common/tests/sanitizer_procmaps_mac_test.cpp new file mode 100644 index 0000000..f622bba --- /dev/null +++ b/compiler-rt/lib/sanitizer_common/tests/sanitizer_procmaps_mac_test.cpp @@ -0,0 +1,137 @@ +//===-- sanitizer_procmaps_mac_test.cpp ---------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file is a part of ThreadSanitizer/AddressSanitizer runtime. +// +//===----------------------------------------------------------------------===// + +# include "sanitizer_common/sanitizer_platform.h" + +# if SANITIZER_APPLE + +# include +# include +# include +# include + +# include +# include +# include + +# include "gtest/gtest.h" + +# include "sanitizer_common/sanitizer_procmaps.h" + +namespace __sanitizer { + +class MemoryMappingLayoutMock final : public MemoryMappingLayout { +private: + static constexpr uuid_command mock_uuid_command = { + .cmd = LC_UUID, + .cmdsize = sizeof(uuid_command), + .uuid = {} + }; + + static constexpr char dylib_name[] = "libclang_rt.\0\0\0"; // 8 bytes aligned, padded with zeros per loader.h + static constexpr dylib_command mock_dylib_command = { + .cmd = LC_LOAD_DYLIB, + .cmdsize = sizeof(dylib_command) + sizeof(dylib_name), + .dylib = { + .name = { + .offset = sizeof(dylib_command) + } + } + }; + + static constexpr uuid_command mock_trap_command = { + .cmd = LC_UUID, + .cmdsize = 0x10000, + .uuid = {} + }; + + const char *start_load_cmd_addr; + size_t sizeofcmds; + std::vector mock_header; + +public: + MemoryMappingLayoutMock(): MemoryMappingLayout(false) { + EXPECT_EQ(mock_uuid_command.cmdsize % 8, 0u); + EXPECT_EQ(mock_dylib_command.cmdsize % 8, 0u); + + Reset(); + +#ifdef MH_MAGIC_64 + const struct mach_header_64 *header = (mach_header_64 *)_dyld_get_image_header(0); // Any header will do + const size_t header_size = sizeof(mach_header_64); +#else + const struct mach_header *header = _dyld_get_image_header(0); + const size_t header_size = sizeof(mach_header); +#endif + const size_t mock_header_size_with_extras = header_size + header->sizeofcmds + + mock_uuid_command.cmdsize + mock_dylib_command.cmdsize + sizeof(uuid_command); + + mock_header.reserve(mock_header_size_with_extras); + // Copy the original header + copy((unsigned char *)header, + (unsigned char *)header + header_size + header->sizeofcmds, + back_inserter(mock_header)); + // The following commands are not supposed to be processed + // by the (correct) ::Next method at all, since they're not + // accounted for in header->ncmds . + copy((unsigned char *)&mock_uuid_command, + ((unsigned char *)&mock_uuid_command) + mock_uuid_command.cmdsize, + back_inserter(mock_header)); + copy((unsigned char *)&mock_dylib_command, + ((unsigned char *)&mock_dylib_command) + sizeof(dylib_command), // as mock_dylib_command.cmdsize contains the following string + back_inserter(mock_header)); + copy((unsigned char *)dylib_name, + ((unsigned char *)dylib_name) + sizeof(dylib_name), + back_inserter(mock_header)); + + // Append a command w. huge size to have the test detect the read overrun + copy((unsigned char *)&mock_trap_command, + ((unsigned char *)&mock_trap_command) + sizeof(uuid_command), + back_inserter(mock_header)); + + start_load_cmd_addr = (const char *)(mock_header.data() + header_size); + sizeofcmds = header->sizeofcmds; + + const char *last_byte_load_cmd_addr = (start_load_cmd_addr+sizeofcmds-1); + data_.current_image = -1; // So the loop in ::Next runs just once + } + + size_t SizeOfLoadCommands() { + return sizeofcmds; + } + + size_t CurrentLoadCommandOffset() { + size_t offset = data_.current_load_cmd_addr - start_load_cmd_addr; + return offset; + } + +protected: + virtual ImageHeader *CurrentImageHeader() override { + return (ImageHeader *)mock_header.data(); + } +}; + +TEST(MemoryMappingLayout, Next) { + __sanitizer::MemoryMappingLayoutMock memory_mapping; + __sanitizer::MemoryMappedSegment segment; + size_t size = memory_mapping.SizeOfLoadCommands(); + while (memory_mapping.Next(&segment)) { + size_t offset = memory_mapping.CurrentLoadCommandOffset(); + EXPECT_LE(offset, size); + } + size_t final_offset = memory_mapping.CurrentLoadCommandOffset(); + EXPECT_EQ(final_offset, size); // All commands processed, no more, no less +} + +} // namespace __sanitizer + +# endif // SANITIZER_APPLE -- 2.7.4