From 032e26591f7ae4839fe9401a63dd94d6f955738b Mon Sep 17 00:00:00 2001
From: Arseniy Volynets <vol0ncar@yandex.ru>
Date: Mon, 3 Jul 2023 01:42:40 +0300
Subject: [PATCH] feat: add limit for max executed vdbe opcodes

- Add a configurable non-negative
session parameter "sql_vdbe_max_steps"
-- max number of opcodes that Vdbe
is allowed to execute for sql query.

- Default value can be specified in box.cfg.
If not set via box.cfg, default value
is 45000. Value 0 means that no
checks for number of executed Vdbe
opcodes will be made.

- Add the third argument to box.execute
function, that allows to specify options
for query execution. The only option
supported: sql_vdbe_max_steps. Usage
example:

```
box.execute([[select * from t]], {}, {{sql_vdbe_max_steps = 1000}})
```

part of picodata/picodata/sbroad!461

NO_DOC=picodata internal patch
NO_CHANGELOG=picodata internal patch
---
 src/box/box.cc                           |  35 +++++-
 src/box/box.h                            |   1 +
 src/box/errcode.h                        |   6 +-
 src/box/execute.c                        |  18 ++--
 src/box/execute.h                        |   6 +-
 src/box/iproto.cc                        |   6 +-
 src/box/lua/execute.c                    |  85 +++++++++++----
 src/box/lua/execute.h                    |   2 +-
 src/box/lua/load_cfg.lua                 |   2 +
 src/box/lua/session.c                    |  12 +++
 src/box/session.c                        |   3 +
 src/box/session.h                        |   2 +
 src/box/session_settings.c               |   1 +
 src/box/session_settings.h               |   1 +
 src/box/sql/build.c                      |  32 +++++-
 src/box/sql/main.c                       |   4 -
 src/box/sql/sqlInt.h                     |  17 +--
 src/box/sql/sqlLimit.h                   |   8 +-
 src/box/sql/vdbe.c                       |  21 +++-
 src/box/sql/vdbeInt.h                    |   6 +-
 src/box/sql/vdbeapi.c                    |   8 ++
 src/box/sql/vdbeaux.c                    |   1 +
 test/box-tap/cfg.test.lua                |   3 +-
 test/box/admin.result                    |   2 +
 test/box/cfg.result                      |   4 +
 test/box/error.result                    |   3 +
 test/box/session_settings.result         |   1 +
 test/sql-luatest/vdbe_max_steps_test.lua | 129 +++++++++++++++++++++++
 test/sql-tap/autoindex1.test.lua         |   9 ++
 test/sql-tap/count.test.lua              |   3 +
 test/sql-tap/delete3.test.lua            |   3 +
 test/sql-tap/index4.test.lua             |   3 +
 test/sql-tap/limit.test.lua              |   6 +-
 test/sql-tap/select2.test.lua            |   4 +
 test/sql-tap/selectG.test.lua            |   3 +
 test/sql-tap/sort.test.lua               |   5 +
 test/sql-tap/trigger8.test.lua           |   3 +
 test/sql-tap/with1.test.lua              |   1 +
 test/sql/prepared.result                 |   8 ++
 test/sql/prepared.test.lua               |   2 +
 40 files changed, 410 insertions(+), 59 deletions(-)
 create mode 100644 test/sql-luatest/vdbe_max_steps_test.lua

diff --git a/src/box/box.cc b/src/box/box.cc
index 5edc4cb5c9..59ce162b4d 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -96,6 +96,7 @@
 #include "mp_util.h"
 #include "small/static.h"
 #include "memory.h"
+#include "sqlLimit.h"
 
 static char status[64] = "unconfigured";
 
@@ -253,6 +254,9 @@ static char box_feedback_host[BOX_FEEDBACK_HOST_MAX];
 /** Whether sending crash info to feedback URL is enabled. */
 static bool box_feedback_crash_enabled;
 
+/** Default limit for the number of executed VDBE opcodes. */
+uint64_t default_vdbe_max_steps = 45000;
+
 static int
 box_run_on_recovery_state(enum box_recovery_state state)
 {
@@ -1638,6 +1642,20 @@ box_check_sql_cache_size(int size)
 	return 0;
 }
 
+/**
+ * Check sql_vdbe_max_steps cfg option value
+ */
+static int
+box_check_sql_vdbe_max_steps(int steps)
+{
+	if (steps < 0) {
+		diag_set(ClientError, ER_CFG, "sql_vdbe_max_steps",
+			 "must be non-negative");
+		return -1;
+	}
+	return 0;
+}
+
 static int
 box_check_allocator(void)
 {
@@ -1645,7 +1663,7 @@ box_check_allocator(void)
 	if (strcmp(allocator, "small") && strcmp(allocator, "system")) {
 		diag_set(ClientError, ER_CFG, "memtx_allocator",
 			 tt_sprintf("must be small or system, "
-				    "but was set to %s", allocator));
+					"but was set to %s", allocator));
 		return -1;
 	}
 	return 0;
@@ -1817,6 +1835,8 @@ box_check_config(void)
 		diag_raise();
 	if (box_check_sql_cache_size(cfg_geti("sql_cache_size")) != 0)
 		diag_raise();
+	if (box_check_sql_vdbe_max_steps(cfg_geti("sql_vdbe_max_steps")) != 0)
+		diag_raise();
 	if (box_check_txn_timeout() < 0)
 		diag_raise();
 	if (box_check_txn_isolation() == txn_isolation_level_MAX)
@@ -3001,6 +3021,17 @@ box_set_prepared_stmt_cache_size(void)
 	return 0;
 }
 
+int
+box_set_vdbe_max_steps(void)
+{
+	int new_limit = cfg_geti("sql_vdbe_max_steps");
+	if (box_check_sql_vdbe_max_steps(new_limit) != 0)
+		return -1;
+	current_session()->vdbe_max_steps = new_limit;
+	default_vdbe_max_steps = new_limit;
+	return 0;
+}
+
 /**
  * Report crash information to the feedback daemon
  * (ie send it to feedback daemon).
@@ -5054,6 +5085,8 @@ box_cfg_xc(void)
 
 	if (box_set_prepared_stmt_cache_size() != 0)
 		diag_raise();
+	if (box_set_vdbe_max_steps() != 0)
+		diag_raise();
 	box_set_net_msg_max();
 	box_set_readahead();
 	box_set_too_long_threshold();
diff --git a/src/box/box.h b/src/box/box.h
index fa90835724..af95eedc0c 100644
--- a/src/box/box.h
+++ b/src/box/box.h
@@ -353,6 +353,7 @@ void box_set_replication_skip_conflict(void);
 void box_set_replication_anon(void);
 void box_set_net_msg_max(void);
 int box_set_prepared_stmt_cache_size(void);
+int box_set_vdbe_max_steps(void);
 int box_set_feedback(void);
 int box_set_txn_timeout(void);
 int box_set_txn_isolation(void);
diff --git a/src/box/errcode.h b/src/box/errcode.h
index ceafd3c308..bde19049ea 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -319,9 +319,9 @@ struct errcode_record {
 	/*264 */_(ER_NIL_UUID,			"Nil UUID is reserved and can't be used in replication") \
 	/*265 */_(ER_WRONG_FUNCTION_OPTIONS,	"Wrong function options: %s") \
 	/*266 */_(ER_MISSING_SYSTEM_SPACES,	"Snapshot has no system spaces") \
-	/*267 */_(ER_UNUSED1,			"") \
-	/*268 */_(ER_UNUSED2,			"") \
-	/*269 */_(ER_UNUSED3,			"") \
+	/*267 */_(ER_EXCEEDED_VDBE_MAX_STEPS,	"Reached a limit on max executed vdbe opcodes. Limit: %u") \
+	/*268 */_(ER_ILLEGAL_OPTIONS,		"Illegal options: %s")   \
+	/*269 */_(ER_ILLEGAL_OPTIONS_FORMAT,    "Each option in third argument must be a table containing only one key value pair")   \
 	/*270 */_(ER_UNUSED4,			"") \
 	/*271 */_(ER_UNUSED5,			"") \
 	/*272 */_(ER_SCHEMA_UPGRADE_IN_PROGRESS, "Schema upgrade is already in progress") \
diff --git a/src/box/execute.c b/src/box/execute.c
index ce4aa87a78..72cfa12136 100644
--- a/src/box/execute.c
+++ b/src/box/execute.c
@@ -43,7 +43,6 @@
 #include "schema.h"
 #include "port.h"
 #include "tuple.h"
-#include "sql/vdbe.h"
 #include "box/lua/execute.h"
 #include "box/sql_stmt_cache.h"
 #include "session.h"
@@ -176,10 +175,12 @@ sql_unprepare(uint32_t stmt_id)
  * @retval -1 Error.
  */
 static inline int
-sql_execute(struct sql_stmt *stmt, struct port *port, struct region *region)
+sql_execute(struct sql_stmt *stmt, struct port *port, struct region *region,
+	    uint64_t vdbe_max_steps)
 {
 	int rc, column_count = sql_column_count(stmt);
 	rmean_collect(rmean_box, IPROTO_EXECUTE, 1);
+	sql_set_vdbe_max_steps(stmt, vdbe_max_steps);
 	if (column_count > 0) {
 		/* Either ROW or DONE or ERROR. */
 		while ((rc = sql_step(stmt)) == SQL_ROW) {
@@ -200,7 +201,8 @@ sql_execute(struct sql_stmt *stmt, struct port *port, struct region *region)
 int
 sql_execute_prepared(uint32_t stmt_id, const struct sql_bind *bind,
 		     uint32_t bind_count, struct port *port,
-		     struct region *region)
+		     struct region *region,
+		     uint64_t vdbe_max_steps)
 {
 
 	if (!session_check_stmt_id(current_session(), stmt_id)) {
@@ -219,7 +221,8 @@ sql_execute_prepared(uint32_t stmt_id, const struct sql_bind *bind,
 	if (sql_stmt_busy(stmt)) {
 		const char *sql_str = sql_stmt_query_str(stmt);
 		return sql_prepare_and_execute(sql_str, strlen(sql_str), bind,
-					       bind_count, port, region);
+					       bind_count, port, region,
+					       vdbe_max_steps);
 	}
 	/*
 	 * Clear all set from previous execution cycle values to be bound and
@@ -232,7 +235,7 @@ sql_execute_prepared(uint32_t stmt_id, const struct sql_bind *bind,
 	enum sql_serialization_format format = sql_column_count(stmt) > 0 ?
 					       DQL_EXECUTE : DML_EXECUTE;
 	port_sql_create(port, stmt, format, false);
-	if (sql_execute(stmt, port, region) != 0) {
+	if (sql_execute(stmt, port, region, vdbe_max_steps) != 0) {
 		port_destroy(port);
 		sql_stmt_reset(stmt);
 		return -1;
@@ -245,7 +248,8 @@ sql_execute_prepared(uint32_t stmt_id, const struct sql_bind *bind,
 int
 sql_prepare_and_execute(const char *sql, int len, const struct sql_bind *bind,
 			uint32_t bind_count, struct port *port,
-			struct region *region)
+			struct region *region,
+			uint64_t vdbe_max_steps)
 {
 	struct sql_stmt *stmt;
 	if (sql_stmt_compile(sql, len, NULL, &stmt, NULL) != 0)
@@ -255,7 +259,7 @@ sql_prepare_and_execute(const char *sql, int len, const struct sql_bind *bind,
 					   DQL_EXECUTE : DML_EXECUTE;
 	port_sql_create(port, stmt, format, true);
 	if (sql_bind(stmt, bind, bind_count) == 0 &&
-	    sql_execute(stmt, port, region) == 0)
+		sql_execute(stmt, port, region, vdbe_max_steps) == 0)
 		return 0;
 	port_destroy(port);
 	return -1;
diff --git a/src/box/execute.h b/src/box/execute.h
index c15e9d67ba..ebf061731c 100644
--- a/src/box/execute.h
+++ b/src/box/execute.h
@@ -72,7 +72,8 @@ sql_execute_prepared_ext(uint32_t query_id, const struct sql_bind *bind,
 int
 sql_execute_prepared(uint32_t query_id, const struct sql_bind *bind,
 		     uint32_t bind_count, struct port *port,
-		     struct region *region);
+		     struct region *region,
+		     uint64_t vdbe_max_steps);
 
 /**
  * Prepare and execute an SQL statement.
@@ -90,7 +91,8 @@ sql_execute_prepared(uint32_t query_id, const struct sql_bind *bind,
 int
 sql_prepare_and_execute(const char *sql, int len, const struct sql_bind *bind,
 			uint32_t bind_count, struct port *port,
-			struct region *region);
+			struct region *region,
+			uint64_t vdbe_max_steps);
 
 int
 sql_stmt_finalize(struct sql_stmt *stmt);
diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index 4217151eff..54ef40356d 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -2506,7 +2506,8 @@ tx_process_sql(struct cmsg *m)
 			sql = msg->sql.sql_text;
 			sql = mp_decode_str(&sql, &len);
 			if (sql_prepare_and_execute(sql, len, bind, bind_count,
-						    &port, &fiber()->gc) != 0)
+						    &port, &fiber()->gc,
+						    current_session()->vdbe_max_steps) != 0)
 				goto error;
 		} else {
 			assert(msg->sql.sql_text == NULL);
@@ -2514,7 +2515,8 @@ tx_process_sql(struct cmsg *m)
 			sql = msg->sql.stmt_id;
 			uint32_t stmt_id = mp_decode_uint(&sql);
 			if (sql_execute_prepared(stmt_id, bind, bind_count,
-						 &port, &fiber()->gc) != 0)
+						 &port, &fiber()->gc,
+						 current_session()->vdbe_max_steps) != 0)
 				goto error;
 		}
 	} else {
diff --git a/src/box/lua/execute.c b/src/box/lua/execute.c
index 768001b303..fa249f8890 100644
--- a/src/box/lua/execute.c
+++ b/src/box/lua/execute.c
@@ -11,6 +11,7 @@
 #include "mpstream/mpstream.h"
 #include "box/sql/vdbeInt.h"
 #include "box/sql/port.h"
+#include "box/session.h"
 
 /**
  * Serialize a description of the prepared statement.
@@ -103,18 +104,20 @@ lbox_execute_prepared(struct lua_State *L)
 {
 	int top = lua_gettop(L);
 
-	if ((top != 1 && top != 2) || ! lua_istable(L, 1))
-		return luaL_error(L, "Usage: statement:execute([, params])");
+	if ((top != 1 && top != 2 && top != 3) || !lua_istable(L, 1))
+		return luaL_error(L, "Usage: statement:execute([, "
+				     "params[, options]])");
 	lua_getfield(L, 1, "stmt_id");
 	if (!lua_isnumber(L, -1))
 		return luaL_error(L, "Query id is expected to be numeric");
 	lua_remove(L, 1);
-	if (top == 2) {
+	if (top >= 2) {
 		/*
 		 * Stack state (before remove operation):
 		 * 1 Prepared statement object (Lua table)
 		 * 2 Bindings (Lua table)
-		 * 3 Statement ID(fetched from PS table) - top of stack
+		 * 3 Options (Lua table)
+		 * 4 Statement ID(fetched from PS table) - top of stack
 		 *
 		 * We should make it suitable to pass arguments to
 		 * lbox_execute(), i.e. after manipulations stack
@@ -124,10 +127,14 @@ lbox_execute_prepared(struct lua_State *L)
 		 * Since there's no swap operation, we firstly remove
 		 * PS object, then copy table of values to be bound to
 		 * the top of stack (push), and finally remove original
-		 * bindings from stack.
+		 * bindings from stack. The goes if Options are present
 		 */
 		lua_pushvalue(L, 1);
 		lua_remove(L, 1);
+		if (top == 3) {
+			lua_pushvalue(L, 1);
+			lua_remove(L, 1);
+		}
 	}
 	return lbox_execute(L);
 }
@@ -278,26 +285,29 @@ sql_execute_prepared_ext(uint32_t stmt_id, const struct sql_bind *bind,
 			 uint32_t bind_count, struct port *port)
 {
 	return sql_execute_prepared(stmt_id, bind, bind_count, port,
-								&fiber()->gc);
+				    &fiber()->gc,
+				    current_session()->vdbe_max_steps);
 }
 
 /**
- * Decode a single bind column from Lua stack.
+ * Decode a single bind column or option from Lua stack.
  *
  * @param L Lua stack.
  * @param[out] bind Bind to decode to.
  * @param idx Position of table with bind columns on Lua stack.
  * @param i Ordinal bind number.
+ * @param is_option whether the option is being decoded.
  *
  * @retval  0 Success.
  * @retval -1 Memory or client error.
  */
 static inline int
-lua_sql_bind_decode(struct lua_State *L, struct sql_bind *bind, int idx, int i)
+lua_sql_bind_decode(struct lua_State *L, struct sql_bind *bind, int idx, int i, bool is_option)
 {
 	struct luaL_field field;
 	struct region *region = &fiber()->gc;
 	char *buf;
+	int old_stack_sz = lua_gettop(L);
 	lua_rawgeti(L, idx, i + 1);
 	bind->pos = i + 1;
 	if (lua_istable(L, -1)) {
@@ -330,6 +340,9 @@ lua_sql_bind_decode(struct lua_State *L, struct sql_bind *bind, int idx, int i)
 		memcpy(buf, bind->name, name_len + 1);
 		bind->name = buf;
 		bind->name_len = name_len;
+	} else if (is_option) {
+		diag_set(ClientError, ER_ILLEGAL_OPTIONS_FORMAT);
+		return -1;
 	} else {
 		bind->name = NULL;
 		bind->name_len = 0;
@@ -418,13 +431,13 @@ lua_sql_bind_decode(struct lua_State *L, struct sql_bind *bind, int idx, int i)
 	default:
 		unreachable();
 	}
-	lua_pop(L, lua_gettop(L) - idx);
+	lua_pop(L, lua_gettop(L) - old_stack_sz);
 	return 0;
 }
 
 int
 lua_sql_bind_list_decode(struct lua_State *L, struct sql_bind **out_bind,
-			 int idx)
+			 int idx, bool is_option)
 {
 	assert(out_bind != NULL);
 	uint32_t bind_count = lua_objlen(L, idx);
@@ -445,7 +458,7 @@ lua_sql_bind_list_decode(struct lua_State *L, struct sql_bind **out_bind,
 	struct sql_bind *bind = xregion_alloc_array(region, typeof(bind[0]),
 						    bind_count);
 	for (uint32_t i = 0; i < bind_count; ++i) {
-		if (lua_sql_bind_decode(L, &bind[i], idx, i) != 0) {
+		if (lua_sql_bind_decode(L, &bind[i], idx, i, is_option) != 0) {
 			region_truncate(region, used);
 			return -1;
 		}
@@ -457,27 +470,57 @@ lua_sql_bind_list_decode(struct lua_State *L, struct sql_bind **out_bind,
 static int
 lbox_execute(struct lua_State *L)
 {
-	struct sql_bind *bind = NULL;
+	struct sql_bind *bind = NULL, *options = NULL;
 	int bind_count = 0;
+	uint64_t vdbe_max_steps = current_session()->vdbe_max_steps;
 	size_t length;
 	struct port port;
 	int top = lua_gettop(L);
 
-	if ((top != 1 && top != 2) || ! lua_isstring(L, 1))
-		return luaL_error(L, "Usage: box.execute(sqlstring[, params]) "
-				  "or box.execute(stmt_id[, params])");
+	if ((top != 1 && top != 2 && top != 3) || !lua_isstring(L, 1))
+		return luaL_error(L, "Usage: box.execute(sqlstring"
+				     "[, params[, options]]) "
+				  "or box.execute(stmt_id[, params[, options]])");
 
 	if (lua_type(L, 1) != LUA_TSTRING && lua_tointeger(L, 1) < 0)
 		return luaL_error(L, "Statement id can't be negative");
+	if (top >= 2 && !lua_istable(L, 2))
+		return luaL_error(L, "Second argument must be a table");
+	if (top == 3 && !lua_istable(L, 3))
+		return luaL_error(L, "Third argument must be a table");
 
 	size_t region_svp = region_used(&fiber()->gc);
-	if (top == 2) {
-		if (! lua_istable(L, 2))
-			return luaL_error(L, "Second argument must be a table");
-		bind_count = lua_sql_bind_list_decode(L, &bind, 2);
+	if (top >= 2) {
+		bind_count = lua_sql_bind_list_decode(L, &bind, 2, false);
 		if (bind_count < 0)
 			return luaT_push_nil_and_error(L);
 	}
+	if (top == 3) {
+		int option_count = lua_sql_bind_list_decode(L,
+							    &options, 3, true);
+		if (option_count < 0)
+			goto error;
+		const char *option_name = "sql_vdbe_max_steps";
+		for (int i = 0; i < option_count; i++) {
+			/* Currently there exists only one option */
+			if (strcmp(options[i].name, option_name) == 0) {
+				if (options[i].type != MP_UINT) {
+					diag_set(ClientError,
+						 ER_ILLEGAL_OPTIONS,
+						 tt_sprintf("value of the "
+							    "%s option "
+							    "should be a non-negative integer.",
+							    option_name));
+					goto error;
+				}
+				vdbe_max_steps = options[i].u64;
+			} else {
+				diag_set(ClientError, ER_ILLEGAL_OPTIONS,
+					 options[i].name);
+				goto error;
+			}
+		}
+	}
 	/*
 	 * lua_isstring() returns true for numeric values as well,
 	 * so test explicit type instead.
@@ -485,13 +528,13 @@ lbox_execute(struct lua_State *L)
 	if (lua_type(L, 1) == LUA_TSTRING) {
 		const char *sql = lua_tolstring(L, 1, &length);
 		if (sql_prepare_and_execute(sql, length, bind, bind_count, &port,
-					    &fiber()->gc) != 0)
+					    &fiber()->gc, vdbe_max_steps) != 0)
 			goto error;
 	} else {
 		assert(lua_type(L, 1) == LUA_TNUMBER);
 		lua_Integer query_id = lua_tointeger(L, 1);
 		if (sql_execute_prepared(query_id, bind, bind_count, &port,
-					 &fiber()->gc) != 0)
+					 &fiber()->gc, vdbe_max_steps) != 0)
 			goto error;
 	}
 	port_dump_lua(&port, L, false);
diff --git a/src/box/lua/execute.h b/src/box/lua/execute.h
index bafd676156..ad8eaab49d 100644
--- a/src/box/lua/execute.h
+++ b/src/box/lua/execute.h
@@ -63,7 +63,7 @@ port_sql_dump_lua(struct port *port, struct lua_State *L, bool is_flat);
  */
 int
 lua_sql_bind_list_decode(struct lua_State *L, struct sql_bind **out_bind,
-			 int idx);
+			 int idx, bool is_option);
 
 void
 box_lua_sql_init(struct lua_State *L);
diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua
index 4465065c71..672031cf89 100644
--- a/src/box/lua/load_cfg.lua
+++ b/src/box/lua/load_cfg.lua
@@ -198,6 +198,7 @@ local default_cfg = {
     feedback_metrics_limit = ifdef_feedback(1024 * 1024),
     net_msg_max           = 768,
     sql_cache_size        = 5 * 1024 * 1024,
+    sql_vdbe_max_steps    = 45000,
     txn_timeout           = 365 * 100 * 86400,
     txn_isolation         = "best-effort",
 
@@ -390,6 +391,7 @@ local template_cfg = {
     feedback_metrics_limit = ifdef_feedback('number'),
     net_msg_max           = 'number',
     sql_cache_size        = 'number',
+    sql_vdbe_max_steps    = 'number',
     txn_timeout           = 'number',
 
     metrics = 'table',
diff --git a/src/box/lua/session.c b/src/box/lua/session.c
index 4db8dcd4a4..9c53999c3d 100644
--- a/src/box/lua/session.c
+++ b/src/box/lua/session.c
@@ -406,6 +406,9 @@ lbox_session_setting_get_by_id(struct lua_State *L, int sid)
 	if (field_type == FIELD_TYPE_BOOLEAN) {
 		bool value = mp_decode_bool(&mp_pair);
 		lua_pushboolean(L, value);
+	} else if (field_type == FIELD_TYPE_UNSIGNED) {
+		uint64_t value = mp_decode_uint(&mp_pair);
+		luaL_pushuint64(L, value);
 	} else {
 		assert(field_type == FIELD_TYPE_STRING);
 		const char *str = mp_decode_str(&mp_pair, &len);
@@ -465,6 +468,15 @@ lbox_session_setting_set(struct lua_State *L)
 			return luaT_error(L);
 		break;
 	}
+	case LUA_TNUMBER: {
+		const uint64_t value = lua_tonumber(L, -1);
+		size_t size = mp_sizeof_uint(value);
+		char *mp_value = (char *)static_alloc(size);
+		mp_encode_uint(mp_value, value);
+		if (setting->set(sid, mp_value) != 0)
+			return luaT_error(L);
+		break;
+	}
 	default:
 		diag_set(ClientError, ER_SESSION_SETTING_INVALID_VALUE,
 			 session_setting_strs[sid],
diff --git a/src/box/session.c b/src/box/session.c
index de4222efe2..60384c0b03 100644
--- a/src/box/session.c
+++ b/src/box/session.c
@@ -42,6 +42,8 @@
 #include "watcher.h"
 #include "on_shutdown.h"
 #include "sql.h"
+#include "sqlInt.h"
+#include "cfg.h"
 
 const char *session_type_strs[] = {
 	"background",
@@ -258,6 +260,7 @@ session_new(enum session_type type)
 	session_set_type(session, type);
 	session->sql_flags = sql_default_session_flags();
 	session->sql_default_engine = SQL_STORAGE_ENGINE_MEMTX;
+	session->vdbe_max_steps = default_vdbe_max_steps;
 	session->sql_stmts = NULL;
 	session->watchers = NULL;
 	rlist_create(&session->in_shutdown_list);
diff --git a/src/box/session.h b/src/box/session.h
index 0d9ffcde61..cef5f46422 100644
--- a/src/box/session.h
+++ b/src/box/session.h
@@ -114,6 +114,8 @@ struct session {
 	/** SQL Connection flag for current user session */
 	uint32_t sql_flags;
 	enum session_type type;
+	/** Max number of Vdbe commands for query. */
+	uint64_t vdbe_max_steps;
 	/** Session virtual methods. */
 	const struct session_vtab *vtab;
 	/** Session metadata. */
diff --git a/src/box/session_settings.c b/src/box/session_settings.c
index 9c38cd3f96..969a5c87ab 100644
--- a/src/box/session_settings.c
+++ b/src/box/session_settings.c
@@ -51,6 +51,7 @@ const char *session_setting_strs[SESSION_SETTING_COUNT] = {
 	"sql_select_debug",
 	"sql_seq_scan",
 	"sql_vdbe_debug",
+	"sql_vdbe_max_steps",
 };
 
 struct session_settings_index {
diff --git a/src/box/session_settings.h b/src/box/session_settings.h
index 1eb6fa522a..9bbc20f704 100644
--- a/src/box/session_settings.h
+++ b/src/box/session_settings.h
@@ -52,6 +52,7 @@ enum {
 	SESSION_SETTING_SQL_SELECT_DEBUG,
 	SESSION_SETTING_SQL_SEQ_SCAN,
 	SESSION_SETTING_SQL_VDBE_DEBUG,
+	SESSION_SETTING_SQL_VDBE_MAX_STEPS,
 	SESSION_SETTING_SQL_END,
 	/**
 	 * Follow the pattern for groups of settings:
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index f0a49f1ef8..86811595bc 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -3222,6 +3222,8 @@ static struct sql_option_metadata sql_session_opts[] = {
 	/** SESSION_SETTING_SQL_VDBE_DEBUG */
 	{FIELD_TYPE_BOOLEAN,
 	 SQL_SqlTrace | SQL_VdbeListing | SQL_VdbeTrace},
+	 /** SESSION_SETTING_SQL_VDBE_MAX_STEPS */
+	 {FIELD_TYPE_UNSIGNED, 0},
 };
 
 static void
@@ -3236,15 +3238,18 @@ sql_session_setting_get(int id, const char **mp_pair, const char **mp_pair_end)
 	const char *name = session_setting_strs[id];
 	size_t name_len = strlen(name);
 	size_t engine_len;
+	uint64_t vdbe_max_steps = session->vdbe_max_steps;
 	const char *engine;
 	size_t size = mp_sizeof_array(2) + mp_sizeof_str(name_len);
 	/*
-	 * Currently, SQL session settings are of a boolean or
+	 * Currently, SQL session settings are of a boolean, unsigned or
 	 * string type.
 	 */
 	bool is_bool = opt->field_type == FIELD_TYPE_BOOLEAN;
 	if (is_bool) {
 		size += mp_sizeof_bool(true);
+	} else if (opt->field_type == FIELD_TYPE_UNSIGNED) {
+		size += mp_sizeof_uint(vdbe_max_steps);
 	} else {
 		assert(id == SESSION_SETTING_SQL_DEFAULT_ENGINE);
 		engine = sql_storage_engine_strs[session->sql_default_engine];
@@ -3258,6 +3263,8 @@ sql_session_setting_get(int id, const char **mp_pair, const char **mp_pair_end)
 	pos_end = mp_encode_str(pos_end, name, name_len);
 	if (is_bool)
 		pos_end = mp_encode_bool(pos_end, (flags & mask) == mask);
+	else if (opt->field_type == FIELD_TYPE_UNSIGNED)
+		pos_end = mp_encode_uint(pos_end, vdbe_max_steps);
 	else
 		pos_end = mp_encode_str(pos_end, engine, engine_len);
 	*mp_pair = pos;
@@ -3309,6 +3316,24 @@ sql_set_string_option(int id, const char *value)
 	return 0;
 }
 
+/**
+ * Set given value for option that has
+ * type uint64_t.
+ *
+ * Currently it is only sql_vdbe_max_steps
+ * option.
+ */
+static int
+sql_set_unsigned_option(int id, uint64_t value)
+{
+	assert(sql_session_opts[id - SESSION_SETTING_SQL_BEGIN].field_type =
+				   FIELD_TYPE_UNSIGNED);
+	assert(id == SESSION_SETTING_SQL_VDBE_MAX_STEPS);
+	(void)id;
+	current_session()->vdbe_max_steps = value;
+	return 0;
+}
+
 static int
 sql_session_setting_set(int id, const char *mp_value)
 {
@@ -3329,6 +3354,11 @@ sql_session_setting_set(int id, const char *mp_value)
 		tmp = mp_decode_str(&mp_value, &len);
 		tmp = tt_cstr(tmp, len);
 		return sql_set_string_option(id, tmp);
+	case FIELD_TYPE_UNSIGNED:
+		if (mtype != MP_UINT)
+			break;
+		return sql_set_unsigned_option(id,
+			mp_decode_uint(&mp_value));
 	default:
 		unreachable();
 	}
diff --git a/src/box/sql/main.c b/src/box/sql/main.c
index 402576bcfc..b84fd226c5 100644
--- a/src/box/sql/main.c
+++ b/src/box/sql/main.c
@@ -258,7 +258,6 @@ static const int aHardLimit[] = {
 	SQL_MAX_COLUMN,
 	SQL_MAX_EXPR_DEPTH,
 	SQL_MAX_COMPOUND_SELECT,
-	SQL_MAX_VDBE_OP,
 	SQL_MAX_FUNCTION_ARG,
 	SQL_MAX_ATTACHED,
 	SQL_MAX_LIKE_PATTERN_LENGTH,
@@ -280,9 +279,6 @@ static const int aHardLimit[] = {
 #if SQL_MAX_COMPOUND_SELECT<2
 #error SQL_MAX_COMPOUND_SELECT must be at least 2
 #endif
-#if SQL_MAX_VDBE_OP<40
-#error SQL_MAX_VDBE_OP must be at least 40
-#endif
 #if SQL_MAX_FUNCTION_ARG<0 || SQL_MAX_FUNCTION_ARG>127
 #error SQL_MAX_FUNCTION_ARG must be between 0 and 127
 #endif
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index a5008fbc17..6d90453396 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -296,11 +296,10 @@ struct sql_vfs {
 #define SQL_LIMIT_COLUMN                    2
 #define SQL_LIMIT_EXPR_DEPTH                3
 #define SQL_LIMIT_COMPOUND_SELECT           4
-#define SQL_LIMIT_VDBE_OP                   5
-#define SQL_LIMIT_FUNCTION_ARG              6
-#define SQL_LIMIT_ATTACHED                  7
-#define SQL_LIMIT_LIKE_PATTERN_LENGTH       8
-#define SQL_LIMIT_TRIGGER_DEPTH             9
+#define SQL_LIMIT_FUNCTION_ARG              5
+#define SQL_LIMIT_ATTACHED                  6
+#define SQL_LIMIT_LIKE_PATTERN_LENGTH       7
+#define SQL_LIMIT_TRIGGER_DEPTH             8
 
 struct tt_uuid;
 struct sql_key_info;
@@ -350,6 +349,13 @@ sql_stmt_compile(const char *sql, int bytes_count, struct Vdbe *re_prepared,
 int
 sql_step(sql_stmt *);
 
+/**
+ * Set vdbe_max_steps limit before execution of
+ * Vdbe program.
+ */
+void
+sql_set_vdbe_max_steps(sql_stmt *, uint64_t);
+
 int
 sql_column_bytes16(sql_stmt *, int iCol);
 
@@ -479,7 +485,6 @@ sql_vfs_register(sql_vfs *, int makeDflt);
 #define SQL_STMTSTATUS_FULLSCAN_STEP     1
 #define SQL_STMTSTATUS_SORT              2
 #define SQL_STMTSTATUS_AUTOINDEX         3
-#define SQL_STMTSTATUS_VM_STEP           4
 
 /** Unbind all parameters of given prepared statement. */
 void
diff --git a/src/box/sql/sqlLimit.h b/src/box/sql/sqlLimit.h
index 53dbe15059..f56ec3d960 100644
--- a/src/box/sql/sqlLimit.h
+++ b/src/box/sql/sqlLimit.h
@@ -122,12 +122,10 @@ enum {
 #endif
 
 /*
- * The maximum number of opcodes in a VDBE program.
- * Not currently enforced.
+ * The default number of opcodes Vdbe is allowed
+ * to execute.
  */
-#ifndef SQL_MAX_VDBE_OP
-#define SQL_MAX_VDBE_OP 25000
-#endif
+extern uint64_t default_vdbe_max_steps;
 
 /*
  * The maximum number of arguments to an SQL function.
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index fb4f4bd839..927affbcf9 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -370,7 +370,6 @@ int sqlVdbeExec(Vdbe *p)
 	/* The database */
 	struct sql *db = sql_get();
 	int iCompare = 0;          /* Result of last comparison */
-	unsigned nVmStep = 0;      /* Number of virtual machine steps */
 	Mem *aMem = p->aMem;       /* Copy of p->aMem */
 	Mem *pIn1 = 0;             /* 1st input operand */
 	Mem *pIn2 = 0;             /* 2nd input operand */
@@ -416,7 +415,13 @@ int sqlVdbeExec(Vdbe *p)
 		assert(rc == 0);
 
 		assert(pOp>=aOp && pOp<&aOp[p->nOp]);
-		nVmStep++;
+		p->step_count++;
+		if (p->vdbe_max_steps > 0 &&
+		    p->vdbe_max_steps < p->step_count) {
+			diag_set(ClientError, ER_EXCEEDED_VDBE_MAX_STEPS,
+				 p->vdbe_max_steps);
+			goto abort_due_to_error;
+		}
 
 		/* Only allow tracing if SQL_DEBUG is defined.
 		 */
@@ -4379,6 +4384,17 @@ case OP_SetSession: {
 			goto abort_due_to_error;
 		break;
 	}
+	case FIELD_TYPE_UNSIGNED: {
+		if (!mem_is_uint(pIn1))
+			goto invalid_type;
+		uint64_t value = pIn1->u.u;
+		size_t size = mp_sizeof_uint(value);
+		char *mp_value = (char *)static_alloc(size);
+		mp_encode_uint(mp_value, value);
+		if (setting->set(sid, mp_value) != 0)
+			goto abort_due_to_error;
+		break;
+	}
 	default:
 	invalid_type:
 		diag_set(ClientError, ER_SESSION_SETTING_INVALID_VALUE,
@@ -4448,7 +4464,6 @@ default: {          /* This is really OP_Noop and OP_Explain */
 
 	/* This is the only way out of this procedure. */
 vdbe_return:
-	p->aCounter[SQL_STMTSTATUS_VM_STEP] += (int)nVmStep;
 	assert(rc == 0 || rc == -1 || rc == SQL_ROW || rc == SQL_DONE);
 	return rc;
 
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index ab98dae1b5..0604e2602a 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -294,7 +294,7 @@ struct Vdbe {
 	 * does not use external resources other than bind variables.
 	 */
 	bft is_sandboxed : 1;
-	u32 aCounter[5];	/* Counters used by sql_stmt_status() */
+	u32 aCounter[4];	/* Counters used by sql_stmt_status() */
 	char *zSql;		/* Text of the SQL statement that generated this */
 	void *pFree;		/* Free this when deleting the vdbe */
 	VdbeFrame *pFrame;	/* Parent frame */
@@ -303,6 +303,10 @@ struct Vdbe {
 	SubProgram *pProgram;	/* Linked list of all sub-programs used by VM */
 	/** Parser flags with which this object was built. */
 	uint32_t sql_flags;
+	/** Limit for maximum vdbe opcodes to execute. */
+	uint64_t vdbe_max_steps;
+	/** The number of opcodes that were executed for current program */
+	uint64_t step_count;
 	/* Anonymous savepoint for aborts only */
 	struct txn_savepoint *anonymous_savepoint;
 };
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index 113f5d5f7e..5bb5a3089f 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -170,6 +170,14 @@ sql_step(sql_stmt * pStmt)
 	return sqlStep(v);
 }
 
+void
+sql_set_vdbe_max_steps(sql_stmt *pStmt, uint64_t vdbe_max_steps)
+{
+	Vdbe *v = (Vdbe *) pStmt;	/* the prepared statement */
+	assert(v != NULL);
+	v->vdbe_max_steps = vdbe_max_steps;
+}
+
 /*
  * Return the number of columns in the result set for the statement pStmt.
  */
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index f6edc96f39..ccc98ea038 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -1358,6 +1358,7 @@ sqlVdbeRewind(Vdbe * p)
 	/* Set the magic to VDBE_MAGIC_RUN sooner rather than later. */
 	p->magic = VDBE_MAGIC_RUN;
 
+	p->step_count = 0;
 	p->pc = -1;
 	p->is_aborted = false;
 	p->ignoreRaised = 0;
diff --git a/test/box-tap/cfg.test.lua b/test/box-tap/cfg.test.lua
index c6f511ec90..2d3e3a5222 100755
--- a/test/box-tap/cfg.test.lua
+++ b/test/box-tap/cfg.test.lua
@@ -6,7 +6,7 @@ local socket = require('socket')
 local fio = require('fio')
 local uuid = require('uuid')
 local msgpack = require('msgpack')
-test:plan(107)
+test:plan(108)
 
 --------------------------------------------------------------------------------
 -- Invalid values
@@ -49,6 +49,7 @@ invalid('vinyl_run_size_ratio', 1)
 invalid('vinyl_bloom_fpr', 0)
 invalid('vinyl_bloom_fpr', 1.1)
 invalid('wal_queue_max_size', -1)
+invalid('sql_vdbe_max_steps', -1)
 
 local function invalid_combinations(name, val)
     local status, result = pcall(box.cfg, val)
diff --git a/test/box/admin.result b/test/box/admin.result
index 53effd27f0..e5e2e9e2da 100644
--- a/test/box/admin.result
+++ b/test/box/admin.result
@@ -112,6 +112,8 @@ cfg_filter(box.cfg)
     - 8
   - - sql_cache_size
     - 5242880
+  - - sql_vdbe_max_steps
+    - 45000
   - - strip_core
     - true
   - - too_long_threshold
diff --git a/test/box/cfg.result b/test/box/cfg.result
index 733461143e..7fc65dd1e7 100644
--- a/test/box/cfg.result
+++ b/test/box/cfg.result
@@ -100,6 +100,8 @@ cfg_filter(box.cfg)
  |     - 8
  |   - - sql_cache_size
  |     - 5242880
+ |   - - sql_vdbe_max_steps
+ |     - 45000
  |   - - strip_core
  |     - true
  |   - - too_long_threshold
@@ -238,6 +240,8 @@ cfg_filter(box.cfg)
  |     - 8
  |   - - sql_cache_size
  |     - 5242880
+ |   - - sql_vdbe_max_steps
+ |     - 45000
  |   - - strip_core
  |     - true
  |   - - too_long_threshold
diff --git a/test/box/error.result b/test/box/error.result
index 56bf2886a2..c0973dd006 100644
--- a/test/box/error.result
+++ b/test/box/error.result
@@ -484,6 +484,9 @@ t;
  |   264: box.error.NIL_UUID
  |   265: box.error.WRONG_FUNCTION_OPTIONS
  |   266: box.error.MISSING_SYSTEM_SPACES
+ |   267: box.error.EXCEEDED_VDBE_MAX_STEPS
+ |   268: box.error.ILLEGAL_OPTIONS
+ |   269: box.error.ILLEGAL_OPTIONS_FORMAT
  |   272: box.error.SCHEMA_UPGRADE_IN_PROGRESS
  |   274: box.error.UNCONFIGURED
  |   278: box.error.IN_ANOTHER_PROMOTE
diff --git a/test/box/session_settings.result b/test/box/session_settings.result
index 6d74be6a00..f79de5aba7 100644
--- a/test/box/session_settings.result
+++ b/test/box/session_settings.result
@@ -61,6 +61,7 @@ s:select()
  |   - ['sql_select_debug', false]
  |   - ['sql_seq_scan', true]
  |   - ['sql_vdbe_debug', false]
+ |   - ['sql_vdbe_max_steps', 45000]
  | ...
 
 t = box.schema.space.create('settings', {format = s:format()})
diff --git a/test/sql-luatest/vdbe_max_steps_test.lua b/test/sql-luatest/vdbe_max_steps_test.lua
new file mode 100644
index 0000000000..4cd7aeee1a
--- /dev/null
+++ b/test/sql-luatest/vdbe_max_steps_test.lua
@@ -0,0 +1,129 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_each(function()
+    g.server = server:new({alias = 'vdbe_max_steps'})
+    g.server:start()
+    g.server:exec(function()
+        box.execute([[SET SESSION "sql_vdbe_max_steps" = 0;]])
+        box.execute([[CREATE TABLE t (i INT PRIMARY KEY, a INT);]])
+        box.execute([[INSERT INTO t VALUES (1, 1), (2, 2), (3, 3);]])
+    end)
+end)
+
+g.after_each(function()
+    g.server:exec(function()
+        box.execute([[SET SESSION "sql_vdbe_max_steps" = 0;]])
+        box.execute([[DROP TABLE t;]])
+    end)
+    g.server:stop()
+end)
+
+g.test_above_limit = function()
+    g.server:exec(function()
+        local _, err = box.execute([[SET SESSION "sql_vdbe_max_steps" = 5;]])
+        t.assert_equals(err, nil)
+
+        _, err = box.execute([[SELECT * FROM t;]])
+        t.assert_equals(err.message,
+                "Reached a limit on max executed vdbe opcodes. Limit: 5")
+
+        _, err = box.execute([[SELECT * FROM t WHERE i + 1 > 2;]])
+        t.assert_equals(err.message,
+                "Reached a limit on max executed vdbe opcodes. Limit: 5")
+
+        _, err = box.execute([[SELECT * FROM t WHERE a > 2;]], {},
+                {{sql_vdbe_max_steps = 4}})
+        t.assert_equals(err.message,
+                "Reached a limit on max executed vdbe opcodes. Limit: 4")
+
+        _, err = box.execute([[SELECT a, sum(i) FROM t
+        WHERE a > ? GROUP BY a;]],
+                {2}, {{sql_vdbe_max_steps = 20}})
+        t.assert_equals(err.message,
+                "Reached a limit on max executed vdbe opcodes. Limit: 20")
+    end)
+end
+
+g.test_signature = function()
+    g.server:exec(function()
+        local _, err = box.execute([[select * from t;]],
+                {{sql_vdbe_max_steps = 1}})
+        t.assert_equals(err.message, "Parameter 'sql_vdbe_max_steps' " ..
+        "was not found in the statement")
+
+        _, err = box.execute([[select i + ? from t;]], {2}, {2, 3, 4})
+        t.assert_equals(err.message, "Each option in third argument must " ..
+        "be a table containing only one key value pair")
+
+        _, err = box.execute([[select i + ? from t;]], {2},
+                {{sql_vdbe_max_steps = -1}})
+        t.assert_equals(err.message, "Illegal options: value of the " ..
+        "sql_vdbe_max_steps option should be a non-negative integer.")
+
+        _, err = box.execute([[select i + ? from t;]], {2},
+                {{vdbe_max_steps = -1}})
+        t.assert_equals(err.message, "Illegal options: vdbe_max_steps")
+
+        _, err = box.execute([[select i + ? from t;]], {2}, {})
+        t.assert_equals(err, nil)
+
+        -- test prepared statement
+        local r
+        r, err = box.prepare([[SELECT * FROM t ORDER BY a;]])
+        t.assert(r ~= nil and err == nil)
+        _, err = r:execute({}, {{sql_vdbe_max_steps = 10}})
+        t.assert_equals(err.message,
+                "Reached a limit on max executed vdbe opcodes. Limit: 10")
+        end)
+end
+
+g.test_sql_trigger = function()
+    g.server:exec(function()
+        local _, err = box.execute([[SET SESSION "sql_vdbe_max_steps" = 12;]])
+        t.assert_equals(err, nil)
+        -- such insert without triggers must take 12 opcodes
+        _, err = box.execute([[insert into t values (4, 4)]])
+        t.assert_equals(err, nil)
+
+        -- create trigger
+        _, err = box.execute([[create trigger before insert on t
+        for each row begin select * from t; end]], {},
+                {{sql_vdbe_max_steps = 0}})
+        t.assert_equals(err, nil)
+
+        -- check that now insert will fail for 12 opcodes limit
+        _, err = box.execute([[SET SESSION "sql_vdbe_max_steps" = 12;]])
+        t.assert_equals(err, nil)
+        _, err = box.execute([[insert into t values (5, 5)]])
+        t.assert_equals(err.message,
+                "Reached a limit on max executed vdbe opcodes. Limit: 12")
+    end)
+end
+
+g.test_below_limit = function()
+    g.server:exec(function()
+        local _, err = box.execute([[SET SESSION "sql_vdbe_max_steps" = 20;]])
+        t.assert(err == nil)
+
+        local res
+        res, err = box.execute([[SELECT max(i) FROM t;]])
+        t.assert(res ~= nil and err == nil)
+
+        res, err = box.execute([[SELECT min(i) FROM t;]])
+        t.assert(res ~= nil and err == nil)
+
+        res, err = box.execute([[SELECT * FROM t WHERE i = 2;]])
+        t.assert(res ~= nil and err == nil)
+
+        res, err = box.execute([[SELECT * FROM t WHERE i > 2;]])
+        t.assert(res ~= nil and err == nil)
+
+        box.execute([[SET SESSION "sql_vdbe_max_steps" = 0;]])
+        res, err = box.execute([[SELECT * FROM t INNER JOIN
+        (select i as c, a as b from t) on true;]])
+        t.assert(res ~= nil and err == nil)
+    end)
+end
diff --git a/test/sql-tap/autoindex1.test.lua b/test/sql-tap/autoindex1.test.lua
index 99d44a2bc2..63a61a4391 100755
--- a/test/sql-tap/autoindex1.test.lua
+++ b/test/sql-tap/autoindex1.test.lua
@@ -36,6 +36,7 @@ test:do_eqp_test(
         {1,0,0,"SEARCH TABLE T2 USING EPHEMERAL INDEX (C=?) (~20 rows)"}
     })
 
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 local result = test:execsql([[SELECT b, (SELECT d FROM t2 WHERE c = a) FROM t1;]])
 
 test:do_eqp_test(
@@ -50,6 +51,8 @@ test:do_execsql_test(
     "autoindex-1.3", [[
         SELECT b, d FROM t1 JOIN t2 ON a = c ORDER BY b;
     ]], result)
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 
 test:do_eqp_test(
     "autoindex-1.4", [[
@@ -59,10 +62,13 @@ test:do_eqp_test(
         {0,1,1,"SEARCH TABLE T2 USING EPHEMERAL INDEX (C=?) (~20 rows)"}
     })
 
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 test:do_execsql_test(
     "autoindex-1.5", [[
         SELECT b, d FROM t1 CROSS JOIN t2 ON (c = a);
     ]], result)
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 
 test:execsql([[
     CREATE TABLE t3(i INT PRIMARY KEY, a INT, b INT);
@@ -76,6 +82,7 @@ test:execsql([[
 --
 for i = 1, 10240 do test:execsql("INSERT INTO t3 VALUES ("..i..", "..i..", "..(i + 1)..");") end
 
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 test:do_execsql_test(
     "autoindex-1.6", [[
         SELECT count(*)
@@ -92,6 +99,8 @@ test:do_execsql_test(
     ]], {
         10231
     })
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 
 test:do_eqp_test(
     "autoindex-1.7", [[
diff --git a/test/sql-tap/count.test.lua b/test/sql-tap/count.test.lua
index 9e75fb20b1..66fb15be60 100755
--- a/test/sql-tap/count.test.lua
+++ b/test/sql-tap/count.test.lua
@@ -90,6 +90,7 @@ for _, zIndex in ipairs(queries) do
             4096
         })
 
+    test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
     test:do_execsql_test(
         "count-1."..iTest..".5",
         [[
@@ -103,6 +104,8 @@ for _, zIndex in ipairs(queries) do
         ]], {
             65536
         })
+    -- restore exec limit to default value
+    test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 
 end
 local function uses_op_count(sql)
diff --git a/test/sql-tap/delete3.test.lua b/test/sql-tap/delete3.test.lua
index eb20ebd528..6c995b67a1 100755
--- a/test/sql-tap/delete3.test.lua
+++ b/test/sql-tap/delete3.test.lua
@@ -21,6 +21,7 @@ test:plan(2)
 -- ["source",[["testdir"],"\/tester.tcl"]]
 -- Create a table that contains a large number of rows.
 --
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 test:do_execsql_test(
     "delete3-1.1",
     [[
@@ -64,6 +65,8 @@ test:do_execsql_test(
         262144
         -- </delete3-1.2>
     })
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 
 --integrity_check delete3-1.3
 test:finish_test()
diff --git a/test/sql-tap/index4.test.lua b/test/sql-tap/index4.test.lua
index 262aee5b10..53bfaa8bfa 100755
--- a/test/sql-tap/index4.test.lua
+++ b/test/sql-tap/index4.test.lua
@@ -18,6 +18,7 @@ test:plan(7)
 --
 -- ["set","testdir",[["file","dirname",["argv0"]]]]
 -- ["source",[["testdir"],"\/tester.tcl"]]
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 test:do_execsql_test(
     1.1,
     [[
@@ -42,6 +43,8 @@ test:do_execsql_test(
           INSERT INTO t1 SELECT randomblob(102) FROM t1;     -- 65536
         COMMIT;
     ]])
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 
 test:do_execsql_test(
     1.2,
diff --git a/test/sql-tap/limit.test.lua b/test/sql-tap/limit.test.lua
index 7173df55f0..d5d1a753c3 100755
--- a/test/sql-tap/limit.test.lua
+++ b/test/sql-tap/limit.test.lua
@@ -248,6 +248,8 @@ test:do_execsql_test(
 
 
 
+-- disable sql exec limit
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 test:do_test(
     "limit-4.1",
     function()
@@ -276,6 +278,9 @@ test:do_test(
         -- </limit-4.1>
     })
 
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
+
 test:do_execsql_test(
     "limit-4.2",
     [[
@@ -358,7 +363,6 @@ test:do_execsql_test(
         1000, 1528204, 593161, 0, 3107, 505, 1005
         -- </limit-5.5>
     })
-
 -- There is some contraversy about whether LIMIT 0 should be the same as
 -- no limit at all or if LIMIT 0 should result in zero output rows.
 --
diff --git a/test/sql-tap/select2.test.lua b/test/sql-tap/select2.test.lua
index ab679b0cf1..d2036e38f4 100755
--- a/test/sql-tap/select2.test.lua
+++ b/test/sql-tap/select2.test.lua
@@ -95,6 +95,7 @@ test:do_execsql_test(
         -- </select2-2.1>
     })
 
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 test:do_execsql_test(
     "select2-2.2",
     [[
@@ -114,6 +115,9 @@ test:do_execsql_test(
         500
         -- </select2-3.1>
     })
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]],
+        box.cfg.sql_vdbe_max_steps))
 
 test:do_execsql_test(
     "select2-3.2a",
diff --git a/test/sql-tap/selectG.test.lua b/test/sql-tap/selectG.test.lua
index 63777d7b0d..3e24d080a4 100755
--- a/test/sql-tap/selectG.test.lua
+++ b/test/sql-tap/selectG.test.lua
@@ -36,6 +36,7 @@ else
     time_quota = engine == 'memtx' and 25 or (
                  engine == 'vinyl' and 50 or 0) -- seconds
 end
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 test:do_test(
     100,
     function()
@@ -57,6 +58,8 @@ test:do_test(
         100000, 5000050000, 50000, true
         -- </100>
     })
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 
 test:finish_test()
 
diff --git a/test/sql-tap/sort.test.lua b/test/sql-tap/sort.test.lua
index d3f32bfb65..2fd474d026 100755
--- a/test/sql-tap/sort.test.lua
+++ b/test/sql-tap/sort.test.lua
@@ -729,6 +729,9 @@ test:do_execsql_test(
         CREATE TABLE t10(id  INT primary key, a INT , b INT );
     ]])
 
+-- disable sql execution limit
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
+
 test:do_test(
     "sort-13.1",
     function()
@@ -791,6 +794,8 @@ box.internal.sql_create_function("cksum", cksum)
             UPDATE t11 SET b = cksum(a);
         ]])
 
+    -- restore exec limit to default value
+    test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
     -- Legacy from the original code. Must be replaced with analogue
     -- functions from box.
     local tn = nil
diff --git a/test/sql-tap/trigger8.test.lua b/test/sql-tap/trigger8.test.lua
index adb0c86ab2..c99f5764f2 100755
--- a/test/sql-tap/trigger8.test.lua
+++ b/test/sql-tap/trigger8.test.lua
@@ -47,6 +47,7 @@ test:do_test(
         end
         sql = sql .. "END;"
         test:execsql(sql)
+        test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
         return test:execsql [[
             INSERT INTO t1 VALUES(5);
             SELECT COUNT(*) FROM t2;
@@ -56,6 +57,8 @@ test:do_test(
         nStatement
         -- </trigger8-1.1>
     })
+-- restore exec limit to default value
+test:execsql(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 
 -- MUST_WORK_TEST
 test:finish_test()
diff --git a/test/sql-tap/with1.test.lua b/test/sql-tap/with1.test.lua
index 2aa9f0aca4..e30d671416 100755
--- a/test/sql-tap/with1.test.lua
+++ b/test/sql-tap/with1.test.lua
@@ -23,6 +23,7 @@ test:plan(64)
 --  return
 -- end
 
+test:execsql([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 test:do_execsql_test(1.0, [[
   CREATE TABLE t1(x INTEGER UNIQUE, y INTEGER, z INTEGER PRIMARY KEY);
   WITH x(a) AS ( SELECT * FROM t1) SELECT 10
diff --git a/test/sql/prepared.result b/test/sql/prepared.result
index d96cc72ce2..a356520712 100644
--- a/test/sql/prepared.result
+++ b/test/sql/prepared.result
@@ -479,9 +479,17 @@ s = prepare([[WITH RECURSIVE \
  | ---
  | ...
 
+execute([[SET SESSION "sql_vdbe_max_steps" = 0;]])
+ | ---
+ | - row_count: 1
+ | ...
 res = execute(s.stmt_id)
  | ---
  | ...
+execute(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
+ | ---
+ | - row_count: 1
+ | ...
 res.metadata
  | ---
  | - - name: COLUMN_13
diff --git a/test/sql/prepared.test.lua b/test/sql/prepared.test.lua
index eedcf0a370..1aea026d41 100644
--- a/test/sql/prepared.test.lua
+++ b/test/sql/prepared.test.lua
@@ -179,7 +179,9 @@ s = prepare([[WITH RECURSIVE \
                               FROM m2 GROUP BY cy) \
                   SELECT group_concat(CAST(TRIM(TRAILING FROM t) AS VARBINARY), x'0a') FROM a;]])
 
+execute([[SET SESSION "sql_vdbe_max_steps" = 0;]])
 res = execute(s.stmt_id)
+execute(string.format([[SET SESSION "sql_vdbe_max_steps" = %d;]], box.cfg.sql_vdbe_max_steps))
 res.metadata
 unprepare(s.stmt_id)
 
-- 
GitLab