Skip to content

Commit bc32323

Browse files
committed
Fixes
1 parent 497bd56 commit bc32323

7 files changed

Lines changed: 48 additions & 42 deletions

File tree

src/catchup/CatchupWork.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "work/WorkWithCallback.h"
2828
#include <Tracy.hpp>
2929
#include <fmt/format.h>
30+
#include <limits>
3031

3132
namespace stellar
3233
{
@@ -407,6 +408,21 @@ CatchupWork::runCatchupStep()
407408
// HAS is fetched and validated at this point
408409
releaseAssert(mHAS->currentLedger > LedgerManager::GENESIS_LEDGER_SEQ);
409410

411+
// Reject HAS whose currentLedger is so large that checkpoint arithmetic
412+
// would overflow. checkpointContainingLedger saturates to UINT32_MAX on
413+
// overflow; treat that as failure here so downstream code never sees a
414+
// wrapped/bogus ledger.
415+
if (HistoryManager::checkpointContainingLedger(mHAS->currentLedger,
416+
mApp.getConfig()) ==
417+
std::numeric_limits<uint32_t>::max())
418+
{
419+
CLOG_ERROR(History,
420+
"HAS currentLedger {} is too large for checkpoint "
421+
"arithmetic",
422+
mHAS->currentLedger);
423+
return State::WORK_FAILURE;
424+
}
425+
410426
auto resolvedConfiguration =
411427
mCatchupConfiguration.resolve(mHAS->currentLedger);
412428
auto catchupRange =

src/history/HistoryArchive.cpp

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
#include "util/GlobalChecks.h"
2121
#include "util/Logging.h"
2222
#include "util/ProtocolVersion.h"
23+
#include "util/types.h"
2324
#include <Tracy.hpp>
2425
#include <fmt/format.h>
2526

26-
#include <cctype>
2727
#include <cereal/archives/json.hpp>
2828
#include <cereal/cereal.hpp>
2929
#include <cereal/types/vector.hpp>
@@ -159,7 +159,7 @@ isValidHexHash(std::string const& s)
159159
}
160160
for (unsigned char c : s)
161161
{
162-
if (!std::isxdigit(c))
162+
if (!isAsciiHexDigit(c))
163163
{
164164
return false;
165165
}
@@ -195,13 +195,6 @@ validateHASAfterDeserialization(HistoryArchiveState const& has)
195195
has.hotArchiveBuckets.size()));
196196
}
197197

198-
// Prevent integer overflow in downstream CheckpointRange calculations
199-
if (has.currentLedger > HistoryArchiveState::MAX_CURRENT_LEDGER)
200-
{
201-
throw std::runtime_error(fmt::format(
202-
FMT_STRING("currentLedger {} is too large"), has.currentLedger));
203-
}
204-
205198
// Validate all bucket hash strings are well-formed 64-character hex
206199
auto validateHashesInBuckets = [](auto const& buckets,
207200
std::string const& name) {

src/history/HistoryArchive.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,6 @@ struct HistoryArchiveState
8888
// 50KB; 10MB is extremely generous.
8989
static constexpr size_t MAX_HAS_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
9090

91-
// Upper bound on currentLedger to prevent uint32_t overflow in
92-
// downstream arithmetic. Callers round currentLedger up to the next
93-
// checkpoint boundary and then step one checkpoint further, so we add up to
94-
// 128 to this number. We'll never hit this in practice, so use 256 for a
95-
// little wiggle room so we don't overflow.
96-
static constexpr uint32_t MAX_CURRENT_LEDGER =
97-
std::numeric_limits<uint32_t>::max() - 256;
98-
9991
enum Version : unsigned
10092
{
10193
HISTORY_ARCHIVE_STATE_VERSION_BEFORE_HOT_ARCHIVE = 1,

src/history/HistoryManager.h

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "overlay/StellarXDR.h"
1010
#include "util/GlobalChecks.h"
1111
#include <functional>
12+
#include <limits>
1213
#include <memory>
1314

1415
/**
@@ -222,14 +223,24 @@ class HistoryManager
222223
// Return checkpoint that contains given ledger. Checkpoint is identified
223224
// by last ledger in range. This does not consult the network nor take
224225
// account of manual checkpoints.
226+
//
227+
// Saturates to UINT32_MAX when rounding up to the next checkpoint would
228+
// overflow uint32_t. Callers that take untrusted ledger numbers (e.g.
229+
// HAS-derived) must treat a UINT32_MAX result as failure.
225230
static uint32_t
226231
checkpointContainingLedger(uint32_t ledger, Config const& cfg)
227232
{
228233
uint32_t freq = getCheckpointFrequency(cfg);
229234
// Round-up to next multiple of freq, then subtract 1 since checkpoints
230235
// are numbered for (and cover ledgers up to) the last ledger in them,
231-
// which is one-before the next multiple of freq.
232-
return (((ledger / freq) + 1) * freq) - 1;
236+
// which is one-before the next multiple of freq. Done in 64-bit to
237+
// avoid wraparound near UINT32_MAX.
238+
uint64_t next = (static_cast<uint64_t>(ledger) / freq + 1) * freq;
239+
if (next > std::numeric_limits<uint32_t>::max())
240+
{
241+
return std::numeric_limits<uint32_t>::max();
242+
}
243+
return static_cast<uint32_t>(next) - 1;
233244
}
234245

235246
// Return true iff closing `ledger` should cause publishing a checkpoint.

src/history/test/HistoryArchiveFormatTests.cpp

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -165,29 +165,6 @@ TEST_CASE("HAS rejects invalid version", "[history][archive-format]")
165165
}
166166
}
167167

168-
TEST_CASE("HAS rejects extreme currentLedger", "[history][archive-format]")
169-
{
170-
// UINT32_MAX
171-
{
172-
auto json = makeHASJson(std::numeric_limits<uint32_t>::max());
173-
HistoryArchiveState has;
174-
REQUIRE_THROWS_AS(has.fromString(json), std::runtime_error);
175-
}
176-
177-
// Just above MAX_CURRENT_LEDGER
178-
{
179-
auto json = makeHASJson(HistoryArchiveState::MAX_CURRENT_LEDGER + 1);
180-
HistoryArchiveState has;
181-
REQUIRE_THROWS_AS(has.fromString(json), std::runtime_error);
182-
}
183-
184-
// Exactly at MAX_CURRENT_LEDGER (should be accepted)
185-
{
186-
auto json = makeHASJson(HistoryArchiveState::MAX_CURRENT_LEDGER);
187-
HistoryArchiveState has;
188-
REQUIRE_NOTHROW(has.fromString(json));
189-
}
190-
}
191168
TEST_CASE("HAS rejects wrong-sized bucket vectors", "[history][archive-format]")
192169
{
193170
// too many currentBuckets

src/history/test/HistoryTests.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ TEST_CASE("checkpoint containing ledger", "[history]")
105105
0x13f);
106106
CHECK(HistoryManager::checkpointContainingLedger(258, app->getConfig()) ==
107107
0x13f);
108+
109+
// Saturates at UINT32_MAX rather than silently wrapping.
110+
uint32_t const MAX = std::numeric_limits<uint32_t>::max();
111+
CHECK(HistoryManager::checkpointContainingLedger(MAX, app->getConfig()) ==
112+
MAX);
113+
CHECK(HistoryManager::checkpointContainingLedger(MAX - 63,
114+
app->getConfig()) == MAX);
115+
CHECK(HistoryManager::checkpointContainingLedger(
116+
MAX - 64, app->getConfig()) == MAX - 64);
108117
}
109118

110119
TEST_CASE("HistoryManager compress", "[history]")

src/util/types.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ isAsciiAlphaNumeric(char c)
193193
return false;
194194
}
195195

196+
inline bool
197+
isAsciiHexDigit(char c)
198+
{
199+
unsigned char uc = static_cast<unsigned char>(c);
200+
return ('0' <= uc && uc <= '9') || ('a' <= uc && uc <= 'f') ||
201+
('A' <= uc && uc <= 'F');
202+
}
203+
196204
inline bool
197205
isAsciiNonControl(char c)
198206
{

0 commit comments

Comments
 (0)