From e9e9db8b7d29203ca654fe1aba3feb8b6d1869f1 Mon Sep 17 00:00:00 2001
From: Konstantin Osipov <kostja@tarantool.org>
Date: Wed, 21 May 2014 21:22:07 +0400
Subject: [PATCH] A fix for gh-206: check for mandatory keys in request body.

A server could crash if some of the request fields were missing.
Check in the request parser for presence of all mandatory fields.
Add a test case from the original fir fox gh-206 by @roman
---
 src/box/lua/box_net.lua |  7 +++++
 src/box/request.cc      |  6 +++++
 src/errcode.h           |  1 +
 src/iproto_constants.cc | 55 ++++++++++++++++++++++++++++++++++++++
 src/iproto_constants.h  | 10 +++++++
 test/box/iproto.result  | 53 ++++++++++++++++++++++++++++++++++++
 test/box/iproto.test.py | 59 +++++++++++++++++++++++++++++++++++++++++
 test/box/misc.result    |  1 +
 test/box/net.box.result |  4 +--
 9 files changed, 194 insertions(+), 2 deletions(-)

diff --git a/src/box/lua/box_net.lua b/src/box/lua/box_net.lua
index 695a8d5aaa..b353031e47 100644
--- a/src/box/lua/box_net.lua
+++ b/src/box/lua/box_net.lua
@@ -436,6 +436,10 @@ box.net.box.new = function(host, port, reconnect_timeout)
                 return
             end
             local blen = self.s:recv(5)
+            if string.len(blen) < 5 then
+                self:fatal("The server has closed connection")
+                return
+            end
             blen = msgpack.decode(blen)
 
             local body = ''
@@ -469,6 +473,9 @@ box.net.box.new = function(host, port, reconnect_timeout)
 
                 while not self.closed do
                     local resp = self:read_response()
+                    if resp == nil or string.len(resp) == 0 then
+                        break
+                    end
                     local header, offset = msgpack.decode(resp);
                     local code = header[box.net.box.TYPE]
                     local sync = header[box.net.box.SYNC]
diff --git a/src/box/request.cc b/src/box/request.cc
index 80f9a008ef..898ce2d3b9 100644
--- a/src/box/request.cc
+++ b/src/box/request.cc
@@ -250,6 +250,7 @@ request_decode(struct request *request, const char *data, uint32_t len)
 {
 	assert(request->execute != NULL);
 	const char *end = data + len;
+	uint64_t key_map = iproto_body_key_map[request->type];
 
 	if (mp_typeof(*data) != MP_MAP || mp_check_map(data, end) > 0) {
 error:
@@ -263,6 +264,7 @@ request_decode(struct request *request, const char *data, uint32_t len)
 			continue;
 		}
 		unsigned char key = mp_decode_uint(&data);
+		key_map &= ~iproto_key_bit(key);
 		const char *value = data;
 		if (mp_check(&data, end))
 			goto error;
@@ -301,6 +303,10 @@ request_decode(struct request *request, const char *data, uint32_t len)
 	if (data != end)
 		tnt_raise(ClientError, ER_INVALID_MSGPACK, "packet end");
 #endif
+	if (key_map) {
+		tnt_raise(ClientError, ER_MISSING_REQUEST_FIELD,
+			  iproto_key_strs[__builtin_ffsll((long long) key_map) - 1]);
+	  }
 }
 
 int
diff --git a/src/errcode.h b/src/errcode.h
index 27bc4ec196..9c2cbb36d3 100644
--- a/src/errcode.h
+++ b/src/errcode.h
@@ -118,6 +118,7 @@ enum { TNT_ERRMSG_MAX = 512 };
 	/* 66 */_(ER_NODE_ID_IS_RO,		2, "Can't reset node id") \
 	/* 67 */_(ER_NODE_ID_IS_RESERVED,	2, "Can't initialize node id with a reserved value %u") \
 	/* 68 */_(ER_INVALID_ORDER,		2, "Invalid LSN order for node %u: prev lsn = %llu, new lsn = %llu") \
+	/* 69 */_(ER_MISSING_REQUEST_FIELD,	2, "Missing mandatory field '%s' in request") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/iproto_constants.cc b/src/iproto_constants.cc
index 3f5eb1c516..a64e4b9253 100644
--- a/src/iproto_constants.cc
+++ b/src/iproto_constants.cc
@@ -101,6 +101,61 @@ const char *iproto_request_type_strs[] =
 	"AUTH"
 };
 
+#define bit(c) (1ULL<<IPROTO_##c)
+const uint64_t iproto_body_key_map[IPROTO_DML_REQUEST_MAX + 1] = {
+	0,                                                     /* unused */
+	bit(SPACE_ID) | bit(LIMIT) | bit(KEY),                 /* SELECT */
+	bit(SPACE_ID) | bit(TUPLE),                            /* INSERT */
+	bit(SPACE_ID) | bit(TUPLE),                            /* REPLACE */
+	bit(SPACE_ID) | bit(KEY) | bit(TUPLE),                 /* UPDATE */
+	bit(SPACE_ID) | bit(KEY),                              /* DELETE */
+	bit(FUNCTION_NAME) | bit(TUPLE),                       /* CALL */
+	bit(USER_NAME) | bit(TUPLE)                            /* AUTH */
+};
+#undef bit
+
+const char *iproto_key_strs[IPROTO_KEY_MAX] = {
+	"type",             /* 0x00 */
+	"sync",             /* 0x01 */
+	"node_id",          /* 0x02 */
+	"lsn",              /* 0x03 */
+	"timestamp",        /* 0x04 */
+	"",                 /* 0x05 */
+	"",                 /* 0x06 */
+	"",                 /* 0x07 */
+	"",                 /* 0x08 */
+	"",                 /* 0x09 */
+	"",                 /* 0x0a */
+	"",                 /* 0x0b */
+	"",                 /* 0x0c */
+	"",                 /* 0x0d */
+	"",                 /* 0x0e */
+	"",                 /* 0x0f */
+	"space_id",         /* 0x10 */
+	"index_id",         /* 0x11 */
+	"limit",            /* 0x12 */
+	"offset",           /* 0x13 */
+	"iterator",         /* 0x14 */
+	"",                 /* 0x15 */
+	"",                 /* 0x16 */
+	"",                 /* 0x17 */
+	"",                 /* 0x18 */
+	"",                 /* 0x19 */
+	"",                 /* 0x1a */
+	"",                 /* 0x1b */
+	"",                 /* 0x1c */
+	"",                 /* 0x1d */
+	"",                 /* 0x1e */
+	"",                 /* 0x1f */
+	"key",              /* 0x20 */
+	"tuple",            /* 0x21 */
+	"function name",    /* 0x22 */
+	"user name",        /* 0x23 */
+	"node uuid"         /* 0x24 */
+	"cluster uuid"      /* 0x25 */
+	"lsn map"           /* 0x26 */
+};
+
 void
 iproto_header_decode(struct iproto_header *header, const char **pos,
 		     const char *end)
diff --git a/src/iproto_constants.h b/src/iproto_constants.h
index ffbd0e11c3..9730f63b26 100644
--- a/src/iproto_constants.h
+++ b/src/iproto_constants.h
@@ -97,6 +97,12 @@ iproto_body_has_key(const char *pos, const char *end)
 
 #undef bit
 
+static inline uint64_t
+iproto_key_bit(unsigned char key)
+{
+	return 1ULL << key;
+}
+
 extern const unsigned char iproto_key_type[IPROTO_KEY_MAX];
 
 enum iproto_request_type {
@@ -115,6 +121,10 @@ enum iproto_request_type {
 };
 
 extern const char *iproto_request_type_strs[];
+/** Key names. */
+extern const char *iproto_key_strs[];
+/** A map of mandatory members of an iproto DML request. */
+extern const uint64_t iproto_body_key_map[];
 
 static inline const char *
 iproto_request_name(uint32_t type)
diff --git a/test/box/iproto.result b/test/box/iproto.result
index a5fcc050ed..f08ef53e3f 100644
--- a/test/box/iproto.result
+++ b/test/box/iproto.result
@@ -13,3 +13,56 @@ ping
 ---
 - ok
 ...
+
+#  Test gh-206 "Segfault if sending IPROTO package without `KEY` field"
+
+IPROTO_SELECT
+query {'IPROTO_CODE': 1} {'IPROTO_SPACE_ID': 280}
+ping
+---
+- ok
+...
+
+
+IPROTO_DELETE
+query {'IPROTO_CODE': 5} {'IPROTO_SPACE_ID': 280}
+ping
+---
+- ok
+...
+
+
+IPROTO_UPDATE
+query {'IPROTO_CODE': 4} {'IPROTO_SPACE_ID': 280}
+ping
+---
+- ok
+...
+query {'IPROTO_CODE': 4} {'IPROTO_SPACE_ID': 280, 'IPROTO_KEY': (1,)}
+ping
+---
+- ok
+...
+
+
+IPROTO_REPLACE
+query {'IPROTO_CODE': 3} {'IPROTO_SPACE_ID': 280}
+ping
+---
+- ok
+...
+
+
+IPROTO_CALL
+query {'IPROTO_CODE': 6} {}
+ping
+---
+- ok
+...
+query {'IPROTO_CODE': 6} {'IPROTO_KEY': ('procname',)}
+ping
+---
+- ok
+...
+
+
diff --git a/test/box/iproto.test.py b/test/box/iproto.test.py
index 895a8c90ad..096b923227 100644
--- a/test/box/iproto.test.py
+++ b/test/box/iproto.test.py
@@ -2,6 +2,9 @@ import os
 import sys
 import struct
 import socket
+import msgpack
+from tarantool.const import *
+from tarantool import Connection
 
 print """
 #
@@ -24,3 +27,59 @@ sql("ping")
 
 # closing connection
 s.close()
+
+key_names = {}
+for (k,v) in globals().items():
+    if type(k) == str and k.startswith('IPROTO_') and type(v) == int:
+        key_names[v] = k
+
+def repr_dict(todump):
+    d = {}
+    for (k, v) in todump.items():
+        k_name = key_names.get(k, k)
+        d[k_name] = v
+    return repr(d)
+
+def test(header, body):
+    # Connect and authenticate
+    c = Connection('localhost', server.sql.port)
+    c.connect()
+    print 'query', repr_dict(header), repr_dict(body)
+    header = msgpack.dumps(header)
+    body = msgpack.dumps(body)
+    query = msgpack.dumps(len(header) + len(body)) + header + body
+    # Send raw request using connectred socket
+    s = c._socket
+    try:
+        s.send(query)
+    except OSError as e:
+        print '   => ', 'Failed to send request'
+    c.close()
+    sql("ping")
+
+print """
+#  Test gh-206 "Segfault if sending IPROTO package without `KEY` field"
+"""
+
+print "IPROTO_SELECT"
+test({ IPROTO_CODE : REQUEST_TYPE_SELECT }, { IPROTO_SPACE_ID: 280 })
+print "\n"
+
+print "IPROTO_DELETE"
+test({ IPROTO_CODE : REQUEST_TYPE_DELETE }, { IPROTO_SPACE_ID: 280 })
+print "\n"
+
+print "IPROTO_UPDATE"
+test({ IPROTO_CODE : REQUEST_TYPE_UPDATE }, { IPROTO_SPACE_ID: 280 })
+test({ IPROTO_CODE : REQUEST_TYPE_UPDATE },
+     { IPROTO_SPACE_ID: 280, IPROTO_KEY: (1, )})
+print "\n"
+
+print "IPROTO_REPLACE"
+test({ IPROTO_CODE : REQUEST_TYPE_REPLACE }, { IPROTO_SPACE_ID: 280 })
+print "\n"
+
+print "IPROTO_CALL"
+test({ IPROTO_CODE : REQUEST_TYPE_CALL }, {})
+test({ IPROTO_CODE : REQUEST_TYPE_CALL }, { IPROTO_KEY: ('procname', )})
+print "\n"
diff --git a/test/box/misc.result b/test/box/misc.result
index 121f3eaa68..94a63e618a 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -239,6 +239,7 @@ t;
   - 'box.error.ER_SPACE_EXISTS : 10'
   - 'box.error.ER_UNKNOWN_NODE : 62'
   - 'box.error.ER_MODIFY_INDEX : 14'
+  - 'box.error.ER_MISSING_REQUEST_FIELD : 69'
   - 'box.error.ER_SECONDARY : 7'
   - 'box.error.ER_NODE_ID_IS_RO : 66'
   - 'box.error.ER_INVALID_UUID : 64'
diff --git a/test/box/net.box.result b/test/box/net.box.result
index bc82f3272e..5056c5b584 100644
--- a/test/box/net.box.result
+++ b/test/box/net.box.result
@@ -430,11 +430,11 @@ remote:close()
 ...
 remote:close()
 ---
-- error: '[string "-- box_net.lua (internal file)..."]:530: box.net.box: already closed'
+- error: '[string "-- box_net.lua (internal file)..."]:537: box.net.box: already closed'
 ...
 remote:ping()
 ---
-- error: '[string "-- box_net.lua (internal file)..."]:535: box.net.box: connection
+- error: '[string "-- box_net.lua (internal file)..."]:542: box.net.box: connection
     was closed'
 ...
 space:drop()
-- 
GitLab