Skip to content

Commit eb2a02b

Browse files
committed
Handle cases where passing an overly long size could cause UB due to integer overflow
1 parent 499d4da commit eb2a02b

File tree

5 files changed

+44
-10
lines changed

5 files changed

+44
-10
lines changed

.github/workflows/clang-tidy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- image: "debian:testing"
1616
clang: 19
1717
- image: "debian:experimental"
18-
clang: 20
18+
clang: 22
1919
container:
2020
image: ${{ matrix.image }}
2121
env:
@@ -33,6 +33,7 @@ jobs:
3333
apt-get install -yq \
3434
clang-${{ matrix.clang }} \
3535
clang-tidy-${{ matrix.clang }} \
36+
libstdc++-12-dev \
3637
cmake \
3738
libprotobuf-dev \
3839
make \

include/protozero/pbf_reader.hpp

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ documentation.
3030
#include <cstddef>
3131
#include <cstdint>
3232
#include <cstring>
33+
#include <limits>
3334
#include <string>
3435
#include <utility>
3536

@@ -139,6 +140,20 @@ class pbf_reader {
139140
T{m_data, m_data}};
140141
}
141142

143+
// Adds size to ptr safely, and returning ptr if overflow would occur.
144+
const char* safe_ptr_add(const char* ptr, size_t length) {
145+
#if defined __has_builtin
146+
#if __has_builtin(__builtin_add_overflow)
147+
uintptr_t result;
148+
return __builtin_add_overflow(reinterpret_cast<uintptr_t>(ptr), length, &result) ? ptr : reinterpret_cast<const char*>(result);
149+
#endif
150+
#endif
151+
if (length > std::numeric_limits<uintptr_t>::max() - reinterpret_cast<uintptr_t>(ptr)) {
152+
return ptr;
153+
}
154+
return ptr + length;
155+
}
156+
142157
public:
143158

144159
/**
@@ -153,7 +168,7 @@ class pbf_reader {
153168
*/
154169
explicit pbf_reader(const data_view& view) noexcept
155170
: m_data{view.data()},
156-
m_end{view.data() + view.size()} {
171+
m_end{safe_ptr_add(view.data(), view.size())} {
157172
}
158173

159174
/**
@@ -168,7 +183,7 @@ class pbf_reader {
168183
*/
169184
pbf_reader(const char* data, std::size_t size) noexcept
170185
: m_data{data},
171-
m_end{data + size} {
186+
m_end{safe_ptr_add(data, size)} {
172187
}
173188

174189
#ifndef PROTOZERO_STRICT_API
@@ -185,7 +200,7 @@ class pbf_reader {
185200
*/
186201
explicit pbf_reader(const std::pair<const char*, std::size_t>& data) noexcept
187202
: m_data{data.first},
188-
m_end{data.first + data.second} {
203+
m_end{safe_ptr_add(data.first, data.second)} {
189204
}
190205
#endif
191206

@@ -201,7 +216,7 @@ class pbf_reader {
201216
*/
202217
explicit pbf_reader(const std::string& data) noexcept
203218
: m_data{data.data()},
204-
m_end{data.data() + data.size()} {
219+
m_end{safe_ptr_add(data.data(), data.size())} {
205220
}
206221

207222
/**

include/protozero/varint.hpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@ namespace detail {
3232

3333
// from https://github.com/facebook/folly/blob/master/folly/Varint.h
3434
inline uint64_t decode_varint_impl(const char** data, const char* end) {
35-
const auto* begin = reinterpret_cast<const int8_t*>(*data);
36-
const auto* iend = reinterpret_cast<const int8_t*>(end);
37-
const int8_t* p = begin;
35+
const int8_t* p = reinterpret_cast<const int8_t*>(*data);
36+
const int8_t* const iend = reinterpret_cast<const int8_t*>(end);
3837
uint64_t val = 0;
3938

40-
if (iend - begin >= max_varint_length) { // fast path
39+
if (iend - p >= max_varint_length) { // fast path
4140
do {
41+
// GCC 12 has trouble understanding that we're not actually taking this branch for short buffers.
42+
#pragma GCC diagnostic push
43+
#pragma GCC diagnostic ignored "-Warray-bounds"
44+
#endif
4245
int64_t b = *p++;
4346
val = ((static_cast<uint64_t>(b) & 0x7fU) ); if (b >= 0) { break; }
4447
b = *p++; val |= ((static_cast<uint64_t>(b) & 0x7fU) << 7U); if (b >= 0) { break; }
@@ -50,6 +53,7 @@ namespace detail {
5053
b = *p++; val |= ((static_cast<uint64_t>(b) & 0x7fU) << 49U); if (b >= 0) { break; }
5154
b = *p++; val |= ((static_cast<uint64_t>(b) & 0x7fU) << 56U); if (b >= 0) { break; }
5255
b = *p++; val |= ((static_cast<uint64_t>(b) & 0x01U) << 63U); if (b >= 0) { break; }
56+
#pragma GCC diagnostic pop
5357
throw varint_too_long_exception{};
5458
} while (false);
5559
} else {

test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ if(PROTOBUF_FOUND)
102102
target_link_libraries(writer_tests PRIVATE protobuf::libprotobuf-lite)
103103

104104
if(NOT MSVC)
105-
set_target_properties(writer_tests PROPERTIES COMPILE_FLAGS "-pthread")
105+
set_target_properties(writer_tests PROPERTIES COMPILE_FLAGS "-pthread -Wno-nullability-extension")
106106
if(NOT APPLE)
107107
set_target_properties(writer_tests PROPERTIES LINK_FLAGS "-pthread")
108108
endif()

test/unit/test_basic.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ TEST_CASE("empty buffer in pbf_reader is okay") {
4848
REQUIRE_FALSE(item.next());
4949
}
5050

51+
TEST_CASE("buffer that overflows address space") {
52+
const char buffer[1] = {0};
53+
// Create a pbf_reader with a length that overflows address space.
54+
// This should be handled gracefully.
55+
// We use a length of size_t(-1) which is the largest possible length.
56+
// This should not cause any reads or writes outside of the provided buffer.
57+
// The pbf_reader should behave as if the buffer is empty.
58+
protozero::pbf_reader item{buffer, size_t(-1)};
59+
60+
REQUIRE(item.length() == 0);
61+
REQUIRE_FALSE(item); // test operator bool()
62+
REQUIRE_FALSE(item.next());
63+
}
64+
5165
TEST_CASE("check every possible value for single byte in buffer") {
5266
char buffer[1];
5367
for (int i = 0; i <= 255; ++i) {

0 commit comments

Comments
 (0)