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.

Это связано с накладными расходами на производительность, поскольку чем выше частота, с которой он профилируется, чем больше циклов ЦП требуется, тем меньше сбоев произойдет. И наоборот.

Заключение

Злоумышленнику просто нужно отправить крошечное лямбда-выражение, чтобы разрушить бессерверный экземпляр. Перед сбоем бессерверного экземпляра этот экземпляр на несколько секунд исчерпывает одно ядро ​​сервера, выделяя память как можно больше и быстрее. Это был бы очень эффективный способ истощения ресурсов сервера с очень небольшой полезной нагрузкой. И до сих пор не существует эффективных способов его предотвращения.