Merge pull request #190 from RomainROCH/fix/buffer-offset-overflow

fix(relinker): reject overflowing buffer offsets
This commit is contained in:
boykopovar 2026-09-30 22:00:51 +03:00 committed by GitHub
commit a54cffadc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 110 additions and 9 deletions

View File

@ -106,6 +106,14 @@ endif()
if(BUILD_TESTING)
add_executable(buffer_bounds_tests
${CMAKE_CURRENT_SOURCE_DIR}/io/tests/BufferBoundsTests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp
)
target_include_directories(buffer_bounds_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include)
add_test(NAME buffer_bounds COMMAND buffer_bounds_tests)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(file_writer_tests
${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileWriterTests.cpp

View File

@ -11,19 +11,19 @@ void WriteU8(std::vector<std::uint8_t>& buf, std::size_t offset, std::uint8_t v)
}
void WriteU16(std::vector<std::uint8_t>& buf, std::size_t offset, std::uint16_t v) {
if (offset + 2 > buf.size())
if (offset > buf.size() || buf.size() - offset < 2)
throw std::out_of_range("WriteU16 out of bounds");
std::memcpy(buf.data() + offset, &v, 2);
}
void WriteU32(std::vector<std::uint8_t>& buf, std::size_t offset, std::uint32_t v) {
if (offset + 4 > buf.size())
if (offset > buf.size() || buf.size() - offset < 4)
throw std::out_of_range("WriteU32 out of bounds");
std::memcpy(buf.data() + offset, &v, 4);
}
void WriteU64(std::vector<std::uint8_t>& buf, std::size_t offset, std::uint64_t v) {
if (offset + 8 > buf.size())
if (offset > buf.size() || buf.size() - offset < 8)
throw std::out_of_range("WriteU64 out of bounds");
std::memcpy(buf.data() + offset, &v, 8);
}
@ -72,7 +72,7 @@ std::uint64_t AlignUp64(std::uint64_t value, std::uint64_t alignment) {
}
std::uint16_t ReadU16(const std::vector<std::uint8_t>& buf, std::size_t offset) {
if (offset + 2 > buf.size())
if (offset > buf.size() || buf.size() - offset < 2)
throw std::out_of_range("ReadU16 out of bounds");
std::uint16_t v;
std::memcpy(&v, buf.data() + offset, 2);
@ -80,7 +80,7 @@ std::uint16_t ReadU16(const std::vector<std::uint8_t>& buf, std::size_t offset)
}
std::uint32_t ReadU32(const std::vector<std::uint8_t>& buf, std::size_t offset) {
if (offset + 4 > buf.size())
if (offset > buf.size() || buf.size() - offset < 4)
throw std::out_of_range("ReadU32 out of bounds");
std::uint32_t v;
std::memcpy(&v, buf.data() + offset, 4);
@ -88,7 +88,7 @@ std::uint32_t ReadU32(const std::vector<std::uint8_t>& buf, std::size_t offset)
}
std::uint64_t ReadU64(const std::vector<std::uint8_t>& buf, std::size_t offset) {
if (offset + 8 > buf.size())
if (offset > buf.size() || buf.size() - offset < 8)
throw std::out_of_range("ReadU64 out of bounds");
std::uint64_t v;
std::memcpy(&v, buf.data() + offset, 8);

View File

@ -5,7 +5,7 @@
namespace Io {
std::uint16_t ByteReader::ReadU16(const std::vector<std::uint8_t>& buf, std::size_t offset) const {
if (offset + 2 > buf.size())
if (offset > buf.size() || buf.size() - offset < 2)
throw std::out_of_range("ByteReader::ReadU16 out of bounds");
std::uint16_t v;
std::memcpy(&v, buf.data() + offset, 2);
@ -13,7 +13,7 @@ std::uint16_t ByteReader::ReadU16(const std::vector<std::uint8_t>& buf, std::siz
}
std::uint32_t ByteReader::ReadU32(const std::vector<std::uint8_t>& buf, std::size_t offset) const {
if (offset + 4 > buf.size())
if (offset > buf.size() || buf.size() - offset < 4)
throw std::out_of_range("ByteReader::ReadU32 out of bounds");
std::uint32_t v;
std::memcpy(&v, buf.data() + offset, 4);
@ -21,7 +21,7 @@ std::uint32_t ByteReader::ReadU32(const std::vector<std::uint8_t>& buf, std::siz
}
std::uint64_t ByteReader::ReadU64(const std::vector<std::uint8_t>& buf, std::size_t offset) const {
if (offset + 8 > buf.size())
if (offset > buf.size() || buf.size() - offset < 8)
throw std::out_of_range("ByteReader::ReadU64 out of bounds");
std::uint64_t v;
std::memcpy(&v, buf.data() + offset, 8);

View File

@ -0,0 +1,93 @@
#include <io/BufferUtils.hpp>
#include <io/ByteReader.hpp>
#include <cstring>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
namespace {
using Bytes = std::vector<std::uint8_t>;
void require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
template<typename TOperation>
void requireOutOfRange(const TOperation& operation, const std::string& name, std::size_t offset) {
try {
operation();
} catch (const std::out_of_range& error) {
require(std::string(error.what()) == name + " out of bounds", name + " changed its out-of-range diagnostic");
return;
}
throw std::runtime_error(name + " accepted invalid offset " + std::to_string(offset));
}
template<typename TValue, typename TOperation>
void invalidRanges(Bytes& bytes, const TOperation& operation, const std::string& name) {
const auto original = bytes;
constexpr auto maxOffset = std::numeric_limits<std::size_t>::max();
for (const std::size_t offset : {bytes.size() - sizeof(TValue) + 1, bytes.size(), bytes.size() + 1,
maxOffset - sizeof(TValue), maxOffset - sizeof(TValue) + 1, maxOffset}) {
requireOutOfRange([&] { operation(bytes, offset); }, name, offset);
require(bytes == original, name + " modified the buffer on failure");
}
for (std::size_t size = 0; size < sizeof(TValue); ++size) {
Bytes shortBuffer(size, 0xCC);
requireOutOfRange([&] { operation(shortBuffer, 0); }, name, 0);
require(shortBuffer == Bytes(size, 0xCC), name + " modified a short buffer on failure");
requireOutOfRange([&] { operation(shortBuffer, maxOffset); }, name, maxOffset);
require(shortBuffer == Bytes(size, 0xCC), name + " modified a short buffer on failure");
}
}
template<typename TValue, typename TRead>
void readBounds(const TRead& read, const std::string& name) {
const auto value = static_cast<TValue>(0x8877665544332211ull);
Bytes bytes(sizeof(value) + 2, 0xCC);
std::memcpy(bytes.data() + 1, &value, sizeof(value));
require(read(bytes, 1) == value, name + " failed an unaligned read");
bytes.pop_back();
require(read(bytes, 1) == value, name + " rejected a read ending at the buffer boundary");
invalidRanges<TValue>(bytes, read, name);
}
template<typename TValue, typename TWrite>
void writeBounds(const TWrite& write, const std::string& name) {
const auto value = static_cast<TValue>(0x8877665544332211ull);
Bytes bytes(sizeof(value) + 2, 0xCC);
auto expected = bytes;
std::memcpy(expected.data() + 1, &value, sizeof(value));
write(bytes, 1, value);
require(bytes == expected, name + " failed an unaligned write");
bytes.pop_back();
expected.pop_back();
bytes.assign(expected.size(), 0xCC);
write(bytes, 1, value);
require(bytes == expected, name + " rejected a write ending at the buffer boundary");
invalidRanges<TValue>(bytes, [&](Bytes& buffer, std::size_t offset) { write(buffer, offset, value); }, name);
}
}
int main() {
try {
const Io::ByteReader reader;
readBounds<std::uint16_t>([&](const Bytes& bytes, std::size_t offset) { return reader.ReadU16(bytes, offset); }, "ByteReader::ReadU16");
readBounds<std::uint32_t>([&](const Bytes& bytes, std::size_t offset) { return reader.ReadU32(bytes, offset); }, "ByteReader::ReadU32");
readBounds<std::uint64_t>([&](const Bytes& bytes, std::size_t offset) { return reader.ReadU64(bytes, offset); }, "ByteReader::ReadU64");
readBounds<std::uint16_t>(Io::ReadU16, "ReadU16");
readBounds<std::uint32_t>(Io::ReadU32, "ReadU32");
readBounds<std::uint64_t>(Io::ReadU64, "ReadU64");
writeBounds<std::uint8_t>(Io::WriteU8, "WriteU8");
writeBounds<std::uint16_t>(Io::WriteU16, "WriteU16");
writeBounds<std::uint32_t>(Io::WriteU32, "WriteU32");
writeBounds<std::uint64_t>(Io::WriteU64, "WriteU64");
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
return 0;
}