Skip to content

Commit e015ea5

Browse files
committed
precompiles: Validate and preprocess expmod input
Decompose the expmod input into separate bytes_views for base, exp, mod. Handle arguments padding. Handle mod being zero. Simplify and vastly reduce the expmod stub by using the preprocessed arguments and handling trivial cases before doing the result lookup.
1 parent 1d574cf commit e015ea5

File tree

4 files changed

+228
-394
lines changed

4 files changed

+228
-394
lines changed

Diff for: test/state/precompiles.cpp

+44-2
Original file line numberDiff line numberDiff line change
@@ -317,19 +317,61 @@ ExecutionResult expmod_execute(
317317
const uint8_t* input, size_t input_size, uint8_t* output, size_t output_size) noexcept
318318
{
319319
static constexpr auto LEN_SIZE = sizeof(intx::uint256);
320+
static constexpr auto HEADER_SIZE = 3 * LEN_SIZE;
321+
static constexpr auto LEN32_OFF = LEN_SIZE - sizeof(uint32_t);
320322

321323
// The output size equal to the modulus size.
324+
const auto mod_len = output_size;
325+
322326
// Handle short incomplete input up front. The answer is 0 of the length of the modulus.
323-
if (output_size == 0 || input_size <= 3 * LEN_SIZE) [[unlikely]]
327+
if (input_size <= HEADER_SIZE) [[unlikely]]
328+
{
329+
std::fill_n(output, output_size, 0);
330+
return {EVMC_SUCCESS, output_size};
331+
}
332+
333+
const auto base_len = intx::be::unsafe::load<uint32_t>(&input[LEN32_OFF]);
334+
const auto exp_len = intx::be::unsafe::load<uint32_t>(&input[LEN_SIZE + LEN32_OFF]);
335+
assert(intx::be::unsafe::load<uint32_t>(&input[2 * LEN_SIZE + LEN32_OFF]) == mod_len);
336+
337+
const bytes_view payload{input + HEADER_SIZE, input_size - HEADER_SIZE};
338+
const size_t mod_off = base_len + exp_len; // Cannot overflow if gas cost computed before.
339+
const auto mod_explicit = payload.substr(std::min(mod_off, payload.size()), mod_len);
340+
341+
// Handle the mod being zero early.
342+
// This serves two purposes:
343+
// - bigint libraries don't like zero modulus because division by 0 is not well-defined,
344+
// - having non-zero modulus guarantees that base and exp aren't out-of-bounds.
345+
if (mod_explicit.find_first_not_of(uint8_t{0}) == bytes_view::npos) [[unlikely]]
324346
{
347+
// The modulus is zero, so the result is zero.
325348
std::fill_n(output, output_size, 0);
326349
return {EVMC_SUCCESS, output_size};
327350
}
328351

352+
const auto mod_requires_padding = mod_explicit.size() != mod_len;
353+
if (mod_requires_padding) [[unlikely]]
354+
{
355+
// The modulus is the last argument and some of its bytes may be missing and be implicitly
356+
// zero. In this case, copy the explict modulus bytes to the output buffer and pad the rest
357+
// with zeroes. The output buffer is guaranteed to have exactly the modulus size.
358+
const auto [_, output_p] = std::ranges::copy(mod_explicit, output);
359+
std::fill(output_p, output + output_size, 0);
360+
}
361+
362+
const auto base = payload.substr(0, base_len);
363+
const auto exp = payload.substr(base_len, exp_len);
364+
const auto mod = mod_requires_padding ? bytes_view{output, mod_len} : mod_explicit;
365+
329366
#ifdef EVMONE_PRECOMPILES_SILKPRE
367+
(void)base;
368+
(void)exp;
369+
(void)mod;
370+
// For Silkpre use the raw input for compatibility.
330371
return silkpre_expmod_execute(input, input_size, output, output_size);
331372
#else
332-
return expmod_stub(input, input_size, output, output_size);
373+
expmod_stub(base, exp, mod, output);
374+
return {EVMC_SUCCESS, mod.size()};
333375
#endif
334376
}
335377

0 commit comments

Comments
 (0)