Skip to content

Commit cee425f

Browse files
committed
PS-10113 [8.4] Server crashes if JS routine tries to allocate more than internal heap memory limit
https://perconadev.atlassian.net/browse/PS-10113 Attempt to allocate more than js_lang.max_mem_size * 4 bytes of memory on JS heap in one go caused server crash. The problem occurred because such allocation exceeded V8 memory limit, which was set by our JS language component to be 4x from the memory limit specified by used through js_lang.max_mem_size variable. The idea was that we should be able to detect exceeding max_mem_size limit well before reaching V8 memory limit. It worked OK in case of small allocations, but failed in case when the problem was caused by single huge allocation. V8 is not good at handling OOM situations gracefully and simply aborts process if allocation is about to exceed its memory limit and extra GC doesn't help. This patch tries to alleviate the problem by not setting V8 memory limit explicitly and relying on its generous default instead (typically > 1Gb). As result only really huge allocation attempts should be affected. The old behavior is still can be achieved by setting newly introduced js_lang.max_mem_size_hard_limit_factor system variable to non-0 value. In this case JS language component will set V8 memory limit to max_mem_size * max_mem_size_hard_limit_factor bytes, and one should be ready to tolerate process aborts if this limit is exceeded. To reduce chance of misuse changing of this new variable requires server restart.
1 parent 29998d6 commit cee425f

File tree

5 files changed

+209
-43
lines changed

5 files changed

+209
-43
lines changed

components/js_lang/js_lang_common.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,11 @@ static constexpr std::string_view CREATE_PRIVILEGE_NAME = "CREATE_JS_ROUTINE";
113113
// Defined as a macro so we can easier concatenate it with other literals.
114114
#define MAX_CONSOLE_LOG_SIZE_VAR_NAME "max_console_log_size"
115115

116-
// Name of system variable which limits the size of per isolate memory.
116+
// Names of system variables which limit the size of per isolate memory.
117117
//
118-
// Defined as a macro so we can easier concatenate it with other literals.
118+
// Defined as macros so we can easier concatenate them with other literals.
119119
#define MAX_MEM_SIZE_VAR_NAME "max_mem_size"
120+
#define MAX_MEM_SIZE_HARD_LIMIT_FACTOR_VAR_NAME "max_mem_size_hard_limit_factor"
120121

121122
// We use RapidJSON to produce console log and information about memory
122123
// usage in JSON format.

components/js_lang/js_lang_core.cc

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -301,27 +301,24 @@ Js_isolate *Js_isolate::create() {
301301
isolated processes, for which "let us crash on reaching the limit"
302302
behavior doesn't cause problems.
303303
304-
We do best-effort attempt to respect our memory limits without crashes
305-
by employing the following approach:
306-
- Set V8 limit MAX_MEM_SIZE_SAFETY_MARGIN_FACTOR times (4x at the moment)
307-
higher than our intended limit.
308-
- Check if our intended limit is exceeded periodically (at GC time).
304+
By default, we try to avoid such aborts and do not set V8 memory limit
305+
explicitly, relying on its generous default (typically > 1Gb).
306+
We only do best-effort attempt to respect max_mem_size memory limit by
307+
checking if it is exceeded periodically (at GC time).
309308
310-
TODO: Consider changing safety multiplier after getting more feedback
311-
from users (PLv8 uses x2, but some of our tests easily crashed with
312-
such multiplier, some other V8 embedders seem to use x8).
313-
314-
Note that if we won't set heap limit explicitly it will be set
315-
automatically by V8 to some large value anyway (1Gb).
309+
Users can set smaller V8 limit by setting max_mem_size_hard_limit_factor
310+
to non-0 value if they are willing to risk server aborts or if such aborts
311+
are preferrable to exceeding memory limits.
316312
317313
TODO: Consider having a hardened mode for JS execution in which it will
318314
be run in separate worker process (like it is done in Chrome).
319315
*/
320316

321-
constexpr size_t MAX_MEM_SIZE_SAFETY_MARGIN_FACTOR = 4;
322-
create_params.constraints.ConfigureDefaultsFromHeapSize(
323-
0, result->m_memory_manager.m_max_mem_size *
324-
MAX_MEM_SIZE_SAFETY_MARGIN_FACTOR);
317+
if (Js_isolate::Memory_manager::s_max_mem_size_hard_limit_factor > 0) {
318+
create_params.constraints.ConfigureDefaultsFromHeapSize(
319+
0, result->m_memory_manager.m_max_mem_size *
320+
Js_isolate::Memory_manager::s_max_mem_size_hard_limit_factor);
321+
}
325322

326323
v8::Isolate *isolate = v8::Isolate::New(create_params);
327324

@@ -573,18 +570,20 @@ constexpr unsigned int MAX_MEM_SIZE_DEFAULT = 8 * 1024 * 1024;
573570
constexpr unsigned int MAX_MEM_SIZE_MIN = 3 * 1024 * 1024;
574571

575572
unsigned int Js_isolate::Memory_manager::s_max_mem_size = MAX_MEM_SIZE_DEFAULT;
573+
unsigned int Js_isolate::Memory_manager::s_max_mem_size_hard_limit_factor = 0;
576574

577575
bool Js_isolate::register_vars() {
578576
/*
579-
Register variable for limiting per Isolate memory consumption.
577+
Register variables for limiting of per Isolate memory consumption.
580578
581-
Note that while minimum value for this setting might seem to be fairly
582-
big it was determined by expiriments. V8 tends to overrides/increases
583-
smaller values anyway.
579+
Note that while minimum value for max_mem_size setting might seem to be
580+
fairly big it was determined by expiriments. V8 tends to overrides/
581+
increases smaller values anyway.
584582
585583
TODO: Possibly change the default after gathering more feedback from users.
586584
*/
587-
INTEGRAL_CHECK_ARG(uint) max_mem_size_check;
585+
INTEGRAL_CHECK_ARG(uint) max_mem_size_check, max_mem_size_hl_factor_check;
586+
588587
max_mem_size_check.def_val = MAX_MEM_SIZE_DEFAULT;
589588
max_mem_size_check.min_val = MAX_MEM_SIZE_MIN;
590589
max_mem_size_check.max_val = 1024 * 1024 * 1024;
@@ -593,7 +592,7 @@ bool Js_isolate::register_vars() {
593592
if (mysql_service_component_sys_variable_register->register_variable(
594593
CURRENT_COMPONENT_NAME_STR, MAX_MEM_SIZE_VAR_NAME,
595594
PLUGIN_VAR_INT | PLUGIN_VAR_UNSIGNED,
596-
"Maximum JS memory size for each user and connection", nullptr,
595+
"Soft limit for JS memory size for each user and connection", nullptr,
597596
nullptr, (void *)&max_mem_size_check,
598597
(void *)&Memory_manager::s_max_mem_size)) {
599598
// Registration of system variable is non-trivial, and can fail for
@@ -605,12 +604,42 @@ bool Js_isolate::register_vars() {
605604
return true;
606605
}
607606

607+
/*
608+
We do not enable hard JS memory limit by default, as hitting it causes
609+
process abort and, in general case, it is fairly easy to do so without
610+
triggering the best-effort limit first. We also do not allow turning it on/
611+
changing this setting at runtime to reduce misuse.
612+
*/
613+
max_mem_size_hl_factor_check.def_val = 0;
614+
max_mem_size_hl_factor_check.min_val = 0;
615+
max_mem_size_hl_factor_check.max_val = 1024;
616+
max_mem_size_hl_factor_check.blk_sz = 0;
617+
618+
if (mysql_service_component_sys_variable_register->register_variable(
619+
CURRENT_COMPONENT_NAME_STR, MAX_MEM_SIZE_HARD_LIMIT_FACTOR_VAR_NAME,
620+
PLUGIN_VAR_INT | PLUGIN_VAR_UNSIGNED | PLUGIN_VAR_READONLY,
621+
"Multiplier defining hard limit for JS memory size for each"
622+
"user and connection (0 means that V8 default is used)",
623+
nullptr, nullptr, (void *)&max_mem_size_hl_factor_check,
624+
(void *)&Memory_manager::s_max_mem_size_hard_limit_factor)) {
625+
// See above.
626+
my_error(ER_LANGUAGE_COMPONENT, MYF(0),
627+
"Can't register " MAX_MEM_SIZE_HARD_LIMIT_FACTOR_VAR_NAME
628+
" system variable for " CURRENT_COMPONENT_NAME_STR " component.");
629+
// We can't do much if the below call fails.
630+
(void)mysql_service_component_sys_variable_unregister->unregister_variable(
631+
CURRENT_COMPONENT_NAME_STR, MAX_MEM_SIZE_VAR_NAME);
632+
return true;
633+
}
634+
608635
if (mysql_service_status_variable_registration->register_variable(
609636
get_status_vars_defs())) {
610637
// Registration of status variables can't fail unless OOM.
611638
// In this case error should have been reported already.
612639
//
613-
// We can't do much if we the below call fails.
640+
// We can't do much if the below calls fail.
641+
(void)mysql_service_component_sys_variable_unregister->unregister_variable(
642+
CURRENT_COMPONENT_NAME_STR, MAX_MEM_SIZE_HARD_LIMIT_FACTOR_VAR_NAME);
614643
(void)mysql_service_component_sys_variable_unregister->unregister_variable(
615644
CURRENT_COMPONENT_NAME_STR, MAX_MEM_SIZE_VAR_NAME);
616645
return true;

components/js_lang/js_lang_core.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,21 @@ class Js_isolate {
393393
bool m_is_max_mem_size_exceeded{false};
394394

395395
/**
396-
Global dynamic variable for maximum memory size limit to set for
396+
Global dynamic variable for maximum memory size soft limit to set for
397397
new isolates.
398398
*/
399399
static unsigned int s_max_mem_size;
400400

401+
/**
402+
Global read-only (i.e. start-up settable) variable for hard memory limit
403+
in V8 engine.
404+
0 - indicates that no V8 limit is set explicitly and the default V8 limit
405+
is used (typically > 1Gb),
406+
non-0 - indicates that V8 limit is set explicitly. The value used as a
407+
multiplier which we use to produce limit value from m_max_mem_size.
408+
*/
409+
static unsigned int s_max_mem_size_hard_limit_factor;
410+
401411
/**
402412
Memory usage counter values for this isolate which already have been
403413
agreggated into global memory usage counters.

mysql-test/suite/component_js_lang/r/js_lang_mem_limit.result

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,23 @@ GRANT CREATE_JS_ROUTINE ON *.* TO root@localhost;
77
SELECT @@js_lang.max_mem_size;
88
@@js_lang.max_mem_size
99
8388608
10-
#
11-
# 1) Create routine which will hog prescribed amount of memory on the
12-
# V8 heap in the process of its execution.
10+
# Create routine which will hog prescribed amount of memory on the
11+
# V8 heap in the process of its execution.
1312
#
1413
# JSON.parse(JSON.stringify()) trick is necessary to suppress V8 string
1514
# optimizations which will cause much smaller memory usage if applied.
15+
CREATE PROCEDURE p_heap_hog(size INT) LANGUAGE JS AS
16+
$$ let s = JSON.parse(JSON.stringify(("A").repeat(size/2 /* Two copies of string! */))) $$;
17+
#
18+
# 1) Test how we enforce memory limits for allocations on V8 heap.
1619
#
17-
# Note that since we can only do "best effort" memory limit detection
18-
# with V8, spike allocation of memory still might cause V8/process
19-
# aborts, or go unnoticed if memory is quickly released.
20+
# Note that since, by default, we can only do "best effort" memory limit
21+
# detection with V8, spike allocation of memory might go unnoticed if
22+
# memory is quickly released.
2023
#
2124
# So the below tests might have to be adjusted to slow down memory
2225
# allocation (to let detection kick in) or disabled if it starts to
2326
# misbehave on some machine.
24-
CREATE PROCEDURE p_heap_hog(size INT) LANGUAGE JS AS
25-
$$ let s = JSON.parse(JSON.stringify(("A").repeat(size/2 /* Two copies of string! */))) $$;
2627
#
2728
# Check that we detect exceeding the limit and report it.
2829
CALL p_heap_hog(10*1000*1000);
@@ -35,7 +36,13 @@ ERROR HY000: Memory limit exceeded.
3536
CALL p_heap_hog(5*1000*1000);
3637
CALL p_heap_hog(5*1000*1000);
3738
CALL p_heap_hog(5*1000*1000);
38-
DROP PROCEDURE p_heap_hog;
39+
#
40+
# Also check what happens if we try to hog way more than the limit.
41+
#
42+
# This was the problem in PS-10113 "Server crashes if JS routine tries
43+
# to allocate more than internal heap memory limit".
44+
CALL p_heap_hog(40*1000*1000);
45+
ERROR HY000: Memory limit exceeded.
3946
#
4047
# 2) Now create routine which will hog prescribed amount of memory on
4148
# ArrayBuffer allocator.
@@ -290,7 +297,51 @@ glob_ems peak_ems
290297
# Local clean-up.
291298
DROP PROCEDURE p1;
292299
DROP FUNCTION f1;
300+
#
301+
# 7) Test coverage for max_mem_size and max_mem_size_hard_limit_factor
302+
# system variables.
303+
#
304+
#
305+
# 7.1) Increasing max_mem_size variable should allow to allocate
306+
# more in new connections/isolates.
307+
#
308+
SET GLOBAL js_lang.max_mem_size = 16*1024*1024;
309+
connect con_add2, localhost, root,,;
310+
CALL p_heap_hog(10*1000*1000);
311+
disconnect con_add2;
312+
connection default;
313+
SET GLOBAL js_lang.max_mem_size = DEFAULT;
314+
#
315+
# 7.2) But max_mem_size_hard_limit_factor variable should not be
316+
# settable at runtime.
317+
#
318+
SET GLOBAL js_lang.max_mem_size_hard_limit_factor = 2;
319+
ERROR HY000: Variable 'js_lang.max_mem_size_hard_limit_factor' is a read only variable
320+
#
321+
# 7.3) OTOH it should be possible to change this variable
322+
# using PERSIST_ONLY and restart.
323+
#
324+
SET PERSIST_ONLY js_lang.max_mem_size_hard_limit_factor = 2;
325+
# restart
326+
SELECT @@js_lang.max_mem_size_hard_limit_factor;
327+
@@js_lang.max_mem_size_hard_limit_factor
328+
2
329+
#
330+
# 7.4) Exceeding hard limit set by variable should lead to
331+
# server abort.
332+
#
333+
CALL p_heap_hog(20*1000*1000);
334+
ERROR HY000: Lost connection to MySQL server during query
335+
# restart
336+
# Let us restore factor variable value now.
337+
# Another server restart is needed for this.
338+
RESET PERSIST js_lang.max_mem_size_hard_limit_factor;
339+
# restart
340+
SELECT @@js_lang.max_mem_size_hard_limit_factor;
341+
@@js_lang.max_mem_size_hard_limit_factor
342+
0
293343
# Global clean-up.
344+
DROP PROCEDURE p_heap_hog;
294345
REVOKE CREATE_JS_ROUTINE ON *.* FROM root@localhost;
295346
# Disconnect of default connection to free the only remaining
296347
# connection context and isolate, so we can uninstall component.

mysql-test/suite/component_js_lang/t/js_lang_mem_limit.test

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
#
44
--source include/have_js_lang_component.inc
55

6+
# Valgrind and CrashReporter are problematic for testing crashes.
7+
--source include/not_valgrind.inc
8+
--source include/not_crashrep.inc
9+
610
--echo # Global prepare of playground.
711
--enable_connect_log
812

@@ -15,22 +19,24 @@ GRANT CREATE_JS_ROUTINE ON *.* TO root@localhost;
1519
--echo # parameters of calls below need to be ajusted.
1620
SELECT @@js_lang.max_mem_size;
1721

18-
--echo #
19-
--echo # 1) Create routine which will hog prescribed amount of memory on the
20-
--echo # V8 heap in the process of its execution.
22+
--echo # Create routine which will hog prescribed amount of memory on the
23+
--echo # V8 heap in the process of its execution.
2124
--echo #
2225
--echo # JSON.parse(JSON.stringify()) trick is necessary to suppress V8 string
2326
--echo # optimizations which will cause much smaller memory usage if applied.
27+
CREATE PROCEDURE p_heap_hog(size INT) LANGUAGE JS AS
28+
$$ let s = JSON.parse(JSON.stringify(("A").repeat(size/2 /* Two copies of string! */))) $$;
29+
30+
--echo #
31+
--echo # 1) Test how we enforce memory limits for allocations on V8 heap.
2432
--echo #
25-
--echo # Note that since we can only do "best effort" memory limit detection
26-
--echo # with V8, spike allocation of memory still might cause V8/process
27-
--echo # aborts, or go unnoticed if memory is quickly released.
33+
--echo # Note that since, by default, we can only do "best effort" memory limit
34+
--echo # detection with V8, spike allocation of memory might go unnoticed if
35+
--echo # memory is quickly released.
2836
--echo #
2937
--echo # So the below tests might have to be adjusted to slow down memory
3038
--echo # allocation (to let detection kick in) or disabled if it starts to
3139
--echo # misbehave on some machine.
32-
CREATE PROCEDURE p_heap_hog(size INT) LANGUAGE JS AS
33-
$$ let s = JSON.parse(JSON.stringify(("A").repeat(size/2 /* Two copies of string! */))) $$;
3440

3541
--echo #
3642
--echo # Check that we detect exceeding the limit and report it.
@@ -46,7 +52,13 @@ CALL p_heap_hog(5*1000*1000);
4652
CALL p_heap_hog(5*1000*1000);
4753
CALL p_heap_hog(5*1000*1000);
4854

49-
DROP PROCEDURE p_heap_hog;
55+
--echo #
56+
--echo # Also check what happens if we try to hog way more than the limit.
57+
--echo #
58+
--echo # This was the problem in PS-10113 "Server crashes if JS routine tries
59+
--echo # to allocate more than internal heap memory limit".
60+
--error ER_LANGUAGE_COMPONENT
61+
CALL p_heap_hog(40*1000*1000);
5062

5163
--echo #
5264
--echo # 2) Now create routine which will hog prescribed amount of memory on
@@ -253,7 +265,70 @@ SELECT JSON_EXTRACT(@mem_usage, "$.global.externalMemorySize") < 1024 AS glob_e
253265
DROP PROCEDURE p1;
254266
DROP FUNCTION f1;
255267

268+
--echo #
269+
--echo # 7) Test coverage for max_mem_size and max_mem_size_hard_limit_factor
270+
--echo # system variables.
271+
--echo #
272+
273+
--echo #
274+
--echo # 7.1) Increasing max_mem_size variable should allow to allocate
275+
--echo # more in new connections/isolates.
276+
--echo #
277+
278+
SET GLOBAL js_lang.max_mem_size = 16*1024*1024;
279+
280+
--connect (con_add2, localhost, root,,)
281+
282+
CALL p_heap_hog(10*1000*1000);
283+
284+
--disconnect con_add2
285+
--source include/wait_until_disconnected.inc
286+
--connection default
287+
288+
SET GLOBAL js_lang.max_mem_size = DEFAULT;
289+
290+
--echo #
291+
--echo # 7.2) But max_mem_size_hard_limit_factor variable should not be
292+
--echo # settable at runtime.
293+
--echo #
294+
--error ER_INCORRECT_GLOBAL_LOCAL_VAR
295+
SET GLOBAL js_lang.max_mem_size_hard_limit_factor = 2;
296+
297+
--echo #
298+
--echo # 7.3) OTOH it should be possible to change this variable
299+
--echo # using PERSIST_ONLY and restart.
300+
--echo #
301+
302+
SET PERSIST_ONLY js_lang.max_mem_size_hard_limit_factor = 2;
303+
304+
--source include/restart_mysqld.inc
305+
306+
SELECT @@js_lang.max_mem_size_hard_limit_factor;
307+
308+
--echo #
309+
--echo # 7.4) Exceeding hard limit set by variable should lead to
310+
--echo # server abort.
311+
--echo #
312+
313+
--source include/expect_crash.inc
314+
315+
--error CR_SERVER_LOST
316+
CALL p_heap_hog(20*1000*1000);
317+
318+
--source include/start_mysqld.inc
319+
320+
--echo # Let us restore factor variable value now.
321+
--echo # Another server restart is needed for this.
322+
323+
RESET PERSIST js_lang.max_mem_size_hard_limit_factor;
324+
325+
--source include/restart_mysqld.inc
326+
327+
SELECT @@js_lang.max_mem_size_hard_limit_factor;
328+
256329
--echo # Global clean-up.
330+
DROP PROCEDURE p_heap_hog;
331+
257332
REVOKE CREATE_JS_ROUTINE ON *.* FROM root@localhost;
258333

259334
--echo # Disconnect of default connection to free the only remaining

0 commit comments

Comments
 (0)