From 9e403e42176b519df362ffa0e2f070d6952b74bf Mon Sep 17 00:00:00 2001
From: Olga Arkhangelskaia <arkholga@tarantool.org>
Date: Wed, 5 Feb 2020 14:05:25 +0300
Subject: [PATCH] json: don't spoil instance with per-call options

When json.decode is used with 2 arguments, 2nd argument seeps out to the
json configuration of the instance. Moreover, due to current
serializer.cfg implementation it remains invisible while checking
settings using json.cfg table.

This fixes commit 6508ddb792fafb4edcacce63e42cc9bc3556bbe8 ('json: fix
stack-use-after-scope in json_decode()').

Closes #4761

(cherry picked from commit f54f4dc03afcc9b34261e6275881f0120faef67e)
---
 .../gh-4761-json-per-call-options.test.lua    | 37 +++++++++++++++++++
 third_party/lua-cjson/lua_cjson.c             | 24 +++++++++---
 2 files changed, 56 insertions(+), 5 deletions(-)
 create mode 100755 test/app-tap/gh-4761-json-per-call-options.test.lua

diff --git a/test/app-tap/gh-4761-json-per-call-options.test.lua b/test/app-tap/gh-4761-json-per-call-options.test.lua
new file mode 100755
index 0000000000..1fb24744ec
--- /dev/null
+++ b/test/app-tap/gh-4761-json-per-call-options.test.lua
@@ -0,0 +1,37 @@
+#!/usr/bin/env tarantool
+
+local json = require('json')
+local tap = require('tap')
+
+--
+-- gh-4761: json.decode silently changes instance settings when
+-- called with 2nd parameter.
+--
+-- Verify json.encode as well.
+--
+local res = tap.test('gh-4761-json-per-call-options', function(test)
+    test:plan(2)
+
+    -- Preparation code: call :decode() with a custom option.
+    local ok, err = pcall(json.decode, '{"foo": {"bar": 1}}',
+                          {decode_max_depth = 1})
+    assert(not ok, 'expect "too many nested data structures" error')
+
+    -- Verify that the instance option remains unchanged.
+    local exp_res = {foo = {bar = 1}}
+    local ok, res = pcall(json.decode, '{"foo": {"bar": 1}}')
+    test:is_deeply({ok, res}, {true, exp_res},
+                   'json instance settings remain unchanged after :decode()')
+
+    -- Same check for json.encode.
+    local nan = 1/0
+    local ok, err = pcall(json.encode, {a = nan},
+                          {encode_invalid_numbers = false})
+    assert(not ok, 'expected "number must not be NaN or Inf" error')
+    local exp_res = '{"a":inf}'
+    local ok, res = pcall(json.encode, {a = nan})
+    test:is_deeply({ok, res}, {true, exp_res},
+                   'json instance settings remain unchanged after :encode()')
+end)
+
+os.exit(res and 0 or 1)
diff --git a/third_party/lua-cjson/lua_cjson.c b/third_party/lua-cjson/lua_cjson.c
index 1ee2e1c109..9c1fb6106a 100644
--- a/third_party/lua-cjson/lua_cjson.c
+++ b/third_party/lua-cjson/lua_cjson.c
@@ -998,13 +998,27 @@ static int json_decode(lua_State *l)
     luaL_argcheck(l, lua_gettop(l) == 2 || lua_gettop(l) == 1, 1,
                   "expected 1 or 2 arguments");
 
+    struct luaL_serializer *cfg = luaL_checkserializer(l);
+
+    /*
+     * user_cfg is per-call local version of serializer instance
+     * options: it is used if a user passes custom options to
+     * :decode() method within a separate argument. In this case
+     * it is required to avoid modifying options of the instance.
+     * Life span of user_cfg is restricted by the scope of
+     * :decode() so it is enough to allocate it on the stack.
+     */
+    struct luaL_serializer user_cfg;
+    json.cfg = cfg;
     if (lua_gettop(l) == 2) {
-        struct luaL_serializer *user_cfg = luaL_checkserializer(l);
-        luaL_serializer_parse_options(l, user_cfg);
+        /*
+         * on_update triggers are left uninitialized for user_cfg.
+         * The decoding code don't (and shouldn't) run them.
+         */
+        luaL_serializer_copy_options(&user_cfg, cfg);
+        luaL_serializer_parse_options(l, &user_cfg);
         lua_pop(l, 1);
-        json.cfg = user_cfg;
-    } else {
-        json.cfg = luaL_checkserializer(l);
+        json.cfg = &user_cfg;
     }
 
     json.data = luaL_checklstring(l, 1, &json_len);
-- 
GitLab