The function v8::internal::StringToBigInt is used by V8 when converting a string to a BigInt (e.g. via BigInt(“1337”)). It first parses the string into individual digit_t’s (in the FromStringAccumulator) then goes on to call BigInt::Allocate: template MaybeHandle BigInt::Allocate(IsolateT* isolate, bigint::FromStringAccumulator* accumulator, bool negative, AllocationType allocation) { uint32_t digits = accumulator->ResultLength(); DCHECK_LE(digits, kMaxLength); Handle result = MutableBigInt::New(isolate, digits, allocation).ToHandleChecked(); // [1] bigint::Status status = isolate->bigint_processor()->FromString(result->rw_digits(), accumulator); // [2] // ... if (digits > 0) result->set_sign(negative); return MutableBigInt::MakeImmutable(result); } Note that here we already allocate the output BigInt (and therefore digit buffer) inside the sandbox (at [1]) and pass it on to FromString (at [2]). FromString is then responsible for actually converting the string to a BigInt. For a non-power-of-two radix, this involves performing multiple multiplications (naively an algorithm that multiplies the current accumulator by the radix, then adds the current digit until all digits have been processed). For large strings, we end up in ProcessorImpl::FromStringLarge, which is fairly complex but an excerpt is shown below: void ProcessorImpl::FromStringLarge(RWDigits Z, FromStringAccumulator* accumulator) { uint32_t num_parts = static_cast(accumulator->heap_parts_.size()); // This is a release-mode check to guard against concurrent in-sandbox corruption. CHECK(Z.len() >= num_parts); RWDigits parts(accumulator->heap_parts_.data(), num_parts); // [Outside the sandbox] Storage multipliers_storage(num_parts); RWDigits multipliers(multipliers_storage.get(), num_parts); // [Outside the sandbox] RWDigits temp(Z, 0, num_parts); // [Inside the sandbox] // ... while (num_parts > 1) { // ... for (; i + 1 < num_parts; i += 2) { // ... // The arguments will be a permutation of the RWDigits buffers from above. Multiply(p_out, p_in, m_in2); // ... // ... // ... } Then, for very large inputs, Multiply will use MultiplyFFT, which is a heavily optimized version of integer multiplication for very large inputs, based on fast-fourier-transforms (implemented in mul-fft.cc). The problem here is that the buffer Z (allocated by BigInt::Allocate inside the sandbox) is not just used as an output buffer but also as temporary storage for intermediate results (e.g. as temp), while other temporary buffers are allocated outside the sandbox (for example the FromStringAccumulator::heap_parts_ buffer). This is problematic, as it means that the code is operating (both reading and writing) on untrusted in-sandbox data and trusted out-of-sandbox data at the same time, which is often dangerous. Specifically, it seems that one invariant of the MultiplyFFT algorithm is that intermediate values have a certain amount of zero padding which guarantees that the results stay below a certain size. If these intermediate values are stored inside the sandbox but the output is stored outside, it becomes possible to violate this assumption and subsequently cause out-of-bounds writes outside the sandbox. Reproduction The issue is somewhat tricky (but possible) to reproduce purely from JavaScript as it requires using a background Worker that guesses the location of the output buffer Z, then mutates data inside of it while MultiplyFFT runs on another thread. However, it is much easier to reproduce the issue with a custom patch that simulates in-sandbox corruption during MultiplyFFT: diff --git a/src/bigint/mul-fft.cc b/src/bigint/mul-fft.cc index f1d8bff8496..41ee9d09397 100644 --- a/src/bigint/mul-fft.cc +++ b/src/bigint/mul-fft.cc @@ -7,10 +7,18 @@ // Christoph Lüders: Fast Multiplication of Large Integers, // http://arxiv.org/abs/1503.04955 +#include "src/sandbox/sandbox.h" +// Necessary hack to make it compile. +#undef CHECK +#undef DCHECK +#undef USE + #include "src/bigint/bigint-internal.h" #include "src/bigint/digit-arithmetic.h" #include "src/bigint/util.h" +#include + namespace v8 { namespace bigint { @@ -479,6 +487,17 @@ class FFTContainer { inline void CopyAndZeroExtend(digit_t* dst, const digit_t* src, int digits_to_copy, size_t total_bytes) { + // Simulate concurrent corruption inside the sandbox. + uintptr_t src_addr = reinterpret_cast(src); + if (internal::InsideSandbox(src_addr)) { + for (int i = 0; i < digits_to_copy; i++) { + if ((std::rand() % 100) == 0) { + digit_t* writable_src = const_cast(src); + writable_src[i] = static_cast(-1); + } + } + } + size_t bytes_to_copy = digits_to_copy * sizeof(digit_t); memcpy(dst, static_cast(src), bytes_to_copy); memset(dst + digits_to_copy, 0, total_bytes - bytes_to_copy); Then the issue can be observed with a simple testcase such as: BigInt("9".repeat(600000)); When run in an ASAN-enabled build: > cat out/sbxtst/args.gn is_debug = false dcheck_always_on = false is_asan = true target_cpu = "x64" > ./out/x64.sbxtst/d8 --sandbox-testing -e 'BigInt("9".repeat(600000));' Sandbox testing mode is enabled. Only sandbox violations will be reported, all other crashes will be ignored. Sandbox bounds: [0x7a3e00000000,0x7b3e00000000) ================================================================= ==832091==ERROR: AddressSanitizer: container-overflow on address 0x7b7f43f882d8 at pc 0x562b27daca6c bp 0x7fff070974b0 sp 0x7fff070974a8 WRITE of size 8 at 0x7b7f43f882d8 thread T0 #0 0x562b27daca6b in operator= src/bigint/bigint.h:162:37 #1 0x562b27daca6b in NormalizeAndRecombine src/bigint/mul-fft.cc:652:13 #2 0x562b27daca6b in v8::bigint::ProcessorImpl::MultiplyFFT(v8::bigint::RWDigits, v8::bigint::Digits, v8::bigint::Digits) src/bigint/mul-fft.cc:806:5 #3 0x562b27d7e62a in v8::bigint::ProcessorImpl::Multiply(v8::bigint::RWDigits, v8::bigint::Digits, v8::bigint::Digits) src/bigint/bigint-internal.cc:49:10 #4 0x562b27d95f50 in v8::bigint::ProcessorImpl::FromStringLarge(v8::bigint::RWDigits, v8::bigint::FromStringAccumulator*) src/bigint/fromstring.cc:164:7 #5 0x562b27d999fd in v8::bigint::Processor::FromString(v8::bigint::RWDigits, v8::bigint::FromStringAccumulator*) src/bigint/fromstring.cc:332:9 #6 0x562b26194195 in v8::internal::MaybeHandle v8::internal::BigInt::Allocate(v8::internal::Isolate*, v8::bigint::FromStringAccumulator*, bool, v8::internal::AllocationType) src/objects/bigint.cc:1200:36 #7 0x562b2617277d in v8::internal::StringToBigInt(v8::internal::Isolate*, v8::internal::DirectHandle) src/numbers/conversions.cc:1109:17 #8 0x562b26193d3b in T::MaybeType v8::internal::BigInt::FromObject(v8::internal::Isolate*, T) src/objects/bigint.cc:988:10 #9 0x562b2573f45a in v8::internal::Builtin_Impl_BigIntConstructor(v8::internal::BuiltinArguments, v8::internal::Isolate*) src/builtins/builtins-bigint.cc:37:39 #10 0x562b2a08a375 in Builtins_CEntry_Return1_ArgvOnStack_BuiltinExit setup-isolate-deserialize.cc #11 0x562b29fdc769 in Builtins_InterpreterEntryTrampoline setup-isolate-deserialize.cc #12 0x562b29fd951b in Builtins_JSEntryTrampoline setup-isolate-deserialize.cc #13 0x562b29fd926a in Builtins_JSEntry setup-isolate-deserialize.cc #14 0x562b25a17a86 in Call src/execution/simulator.h:216:12 #15 0x562b25a17a86 in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) src/execution/execution.cc:442:22 #16 0x562b25a18ef6 in v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::DirectHandle, v8::internal::DirectHandle, v8::internal::DirectHandle) src/execution/execution.cc:542:10 #17 0x562b25632fbb in v8::Script::Run(v8::Local, v8::Local) src/api/api.cc:1984:7 #18 0x562b253730d7 in v8::Shell::ExecuteString(v8::Isolate*, v8::Local, v8::Local, v8::Shell::ReportExceptions, v8::Global*) src/d8/d8.cc:1036:44 #19 0x562b253ab3f9 in v8::SourceGroup::Execute(v8::Isolate*) src/d8/d8.cc:5582:10 #20 0x562b253b76fd in v8::Shell::RunMainIsolate(v8::Isolate*, bool) src/d8/d8.cc:6590:37 #21 0x562b253b6b35 in v8::Shell::RunMain(v8::Isolate*, bool) src/d8/d8.cc:6498:18 #22 0x562b253ba1d7 in v8::Shell::Main(int, char**) src/d8/d8.cc:7391:18 #23 0x7f7f463eeca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 0x7b7f43f882d8 is located 252632 bytes inside of 262144-byte region [0x7b7f43f4a800,0x7b7f43f8a800) allocated by thread T0 here: #0 0x562b25343a1d in operator new(unsigned long) #1 0x562b259c85a8 in __libcpp_allocate gen/third_party/libc++/src/include/__new/allocate.h:43:28 #2 0x562b259c85a8 in allocate gen/third_party/libc++/src/include/__memory/allocator.h:92:14 #3 0x562b259c85a8 in __allocate_at_least, std::__Cr::allocator_traits > > gen/third_party/libc++/src/include/__memory/allocate_at_least.h:46:94 #4 0x562b259c85a8 in __split_buffer gen/third_party/libc++/src/include/__split_buffer:720:25 #5 0x562b259c85a8 in unsigned long* std::__Cr::vector>::__emplace_back_slow_path(unsigned long const&) gen/third_party/libc++/src/include/__vector/vector.h:1105:46 #6 0x562b26179c5e in operator() gen/third_party/libc++/src/include/__vector/vector.h:1148:21 #7 0x562b26179c5e in __if_likely_else<(lambda at gen/third_party/libc++/src/include/__vector/vector.h:1144:7), (lambda at gen/third_party/libc++/src/include/__vector/vector.h:1148:7)> gen/third_party/libc++/src/include/__vector/vector.h:1128:7 #8 0x562b26179c5e in emplace_back gen/third_party/libc++/src/include/__vector/vector.h:1142:3 #9 0x562b26179c5e in push_back gen/third_party/libc++/src/include/__vector/vector.h:461:93 #10 0x562b26179c5e in AddPart src/bigint/bigint.h:626:15 #11 0x562b26179c5e in AddPart src/bigint/bigint.h:607:10 #12 0x562b26179c5e in Parse src/bigint/bigint.h:572:10 #13 0x562b26179c5e in void v8::internal::StringToBigIntHelper::ParseInternal(unsigned char const*) src/numbers/conversions.cc:1081:28 #14 0x562b2616e5e2 in v8::internal::StringToIntHelper::ParseInt() src/numbers/conversions.cc #15 0x562b2616d0ad in v8::internal::StringToBigIntHelper::GetResult() src/numbers/conversions.cc:1025:5 #16 0x562b2617277d in v8::internal::StringToBigInt(v8::internal::Isolate*, v8::internal::DirectHandle) src/numbers/conversions.cc:1109:17 #17 0x562b26193d3b in T::MaybeType v8::internal::BigInt::FromObject(v8::internal::Isolate*, T) src/objects/bigint.cc:988:10 #18 0x562b2573f45a in v8::internal::Builtin_Impl_BigIntConstructor(v8::internal::BuiltinArguments, v8::internal::Isolate*) src/builtins/builtins-bigint.cc:37:39 #19 0x562b2a08a375 in Builtins_CEntry_Return1_ArgvOnStack_BuiltinExit setup-isolate-deserialize.cc #20 0x562b29fdc769 in Builtins_InterpreterEntryTrampoline setup-isolate-deserialize.cc #21 0x562b29fd951b in Builtins_JSEntryTrampoline setup-isolate-deserialize.cc #22 0x562b29fd926a in Builtins_JSEntry setup-isolate-deserialize.cc #23 0x562b25a17a86 in Call src/execution/simulator.h:216:12 #24 0x562b25a17a86 in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) src/execution/execution.cc:442:22 #25 0x562b25a18ef6 in v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::DirectHandle, v8::internal::DirectHandle, v8::internal::DirectHandle) src/execution/execution.cc:542:10 #26 0x562b25632fbb in v8::Script::Run(v8::Local, v8::Local) src/api/api.cc:1984:7 #27 0x562b253730d7 in v8::Shell::ExecuteString(v8::Isolate*, v8::Local, v8::Local, v8::Shell::ReportExceptions, v8::Global*) src/d8/d8.cc:1036:44 #28 0x562b253ab3f9 in v8::SourceGroup::Execute(v8::Isolate*) src/d8/d8.cc:5582:10 #29 0x562b253b76fd in v8::Shell::RunMainIsolate(v8::Isolate*, bool) src/d8/d8.cc:6590:37 #30 0x562b253b6b35 in v8::Shell::RunMain(v8::Isolate*, bool) src/d8/d8.cc:6498:18 #31 0x562b253ba1d7 in v8::Shell::Main(int, char**) src/d8/d8.cc:7391:18 #32 0x7f7f463eeca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 HINT: if you don't care about these errors you may set ASAN_OPTIONS=detect_container_overflow=0. Or if supported by the container library, pass -D__SANITIZER_DISABLE_CONTAINER_OVERFLOW__ to the compiler to disable instrumentation. If you suspect a false positive see also: https://github.com/google/sanitizers/wiki/AddressSanitizerContainerOverflow. SUMMARY: AddressSanitizer: container-overflow src/bigint/bigint.h:162:37 in operator= Shadow bytes around the buggy address: 0x7b7f43f88000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7b7f43f88080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7b7f43f88100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7b7f43f88180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7b7f43f88200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x7b7f43f88280: 00 00 00 00 00 00 00 00 00 00 00[fc]fc fc fc fc 0x7b7f43f88300: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 0x7b7f43f88380: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 0x7b7f43f88400: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 0x7b7f43f88480: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 0x7b7f43f88500: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==832091==ABORTING ## V8 sandbox violation detected! Fix Recommendation As an initial spot fix it may be enough to simply add a bounds-check when writing the carry value in NormalizeAndRecombine (i.e. turn the existing DCHECK(zi < Z.len()) into a CHECK). However, the BigInt parsing logic has historically been somewhat fragile when exposed to in-sandbox corruption (see the other sandbox-related CHECKs in src/bigint). As such, for a more thorough fix I would recommend refactoring the code to avoid operating on both in-sandbox and out-of-sandbox memory at the same time, ideally by moving all dynamic memory allocations performed by this code into the sandbox. Afterwards, similar bugs would only lead to further in-sandbox corruption and the BigInt logic would therefore effectively behave like sandboxed code (not writing to out-of-sandbox memory). Credit Information Samuel Groß of Google Project Zero Disclosure Deadline This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2026-04-07. For more details, see the Project Zero vulnerability disclosure policy: https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html Credit: saelo