diff --git a/src/box/access.h b/src/box/access.h
index 94c5b1d4851f4bd1717f50c5439e64d62261892e..b0cbf366c4cafdc94261813ff62523d5fc363e50 100644
--- a/src/box/access.h
+++ b/src/box/access.h
@@ -41,6 +41,8 @@ enum {
 	PRIV_W = 2,
 	/* CALL */
 	PRIV_X = 4,
+	/** Everything. */
+	PRIV_ALL = PRIV_R + PRIV_W + PRIV_X
 };
 
 /* Privilege name for error messages */
diff --git a/src/box/alter.cc b/src/box/alter.cc
index e0468e056a55db4b38601929849f8640134db1f9..858d14806e18434a95e91f206055f9888f3e5516 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -62,6 +62,9 @@
 #define PRIV_OBJECT_ID   3
 #define PRIV_ACCESS      4
 
+/** _func columns */
+#define FUNC_SETUID      3
+
 /* {{{ Auxiliary functions and methods. */
 
 void
@@ -1324,6 +1327,8 @@ func_def_create_from_tuple(struct func_def *func, struct tuple *tuple)
 {
 	func->fid = tuple_field_u32(tuple, ID);
 	func->uid = tuple_field_u32(tuple, UID);
+	func->auth_token = user_cache_find(func->uid)->auth_token;
+	func->setuid = false;
 	const char *name = tuple_field_cstr(tuple, NAME);
 	uint32_t len = strlen(name);
 	if (len >= sizeof(func->name)) {
@@ -1333,6 +1338,8 @@ func_def_create_from_tuple(struct func_def *func, struct tuple *tuple)
 	snprintf(func->name, sizeof(func->name), "%s", name);
 	/** Nobody has access to the function but the owner. */
 	memset(func->access, 0, sizeof(func->access));
+	if (tuple_field_count(tuple) > FUNC_SETUID)
+		func->setuid = tuple_field_u32(tuple, FUNC_SETUID);
 }
 
 /** Remove a function from function cache */
diff --git a/src/box/key_def.h b/src/box/key_def.h
index 152e4a08b079cd4643b58c9ae83f175e837cbe21..63e7251357d554a31174d1ddac1f06980d7d7769 100644
--- a/src/box/key_def.h
+++ b/src/box/key_def.h
@@ -301,6 +301,16 @@ struct func_def {
 	uint32_t fid;
 	/** Owner of the function. */
 	uint32_t uid;
+	/**
+	 * True if the function requires change of user id before
+	 * invocaction.
+	 */
+	bool setuid;
+	/**
+	 * Authentication id of the owner of the function,
+	 * used for set-user-id functions.
+	 */
+	uint8_t auth_token;
 	/** Function name. */
 	char name[BOX_NAME_MAX + 1];
 	/**
diff --git a/src/box/lua/call.cc b/src/box/lua/call.cc
index 4a394417a3afd9734ca1b8db70a208784a520f18..8fc9b24539fdc0487bfd979532084ab8ec3d06e8 100644
--- a/src/box/lua/call.cc
+++ b/src/box/lua/call.cc
@@ -459,26 +459,74 @@ lbox_call_loadproc(struct lua_State *L)
 	return box_lua_find(L, name, name + name_len);
 }
 
-static inline void
-access_check_func(const char *name, uint32_t name_len,
-		  struct user *user, uint8_t access)
+/**
+ * Check access to a function and change the current
+ * user id if the function is a set-definer-user-id one.
+ * The original user is restored in the destructor.
+ */
+struct SetuidGuard
 {
-	access &= ~user->universal_access.effective;
-	 /*
-	  * No special check for ADMIN user is necessary
-	  * since ADMIN has universal access.
-	  */
-	if (access == 0)
+	/** True if the function was set-user-id one. */
+	bool setuid;
+	/** Original authentication token, only set if setuid = true. */
+	uint8_t orig_auth_token;
+	/** Original user id, only set if setuid = true. */
+	uint32_t orig_uid;
+
+	inline SetuidGuard(const char *name, uint32_t name_len,
+			   struct user *user, uint8_t access);
+	inline ~SetuidGuard();
+};
+
+SetuidGuard::SetuidGuard(const char *name, uint32_t name_len,
+			 struct user *user, uint8_t access)
+	:setuid(false)
+{
+	/*
+	 * If the user has universal access, don't bother with setuid.
+	 * No special check for ADMIN user is necessary
+	 * since ADMIN has universal access.
+	 */
+	if (user->universal_access.effective & PRIV_ALL)
 		return;
+	access &= ~user->universal_access.effective;
+	/*
+	 * We need to look up the function by name even if
+	 * the user has access to it, since it could require
+	 * a set of other user id.
+	 */
 	struct func_def *func = func_by_name(name, name_len);
+	if (func == NULL && access == 0) {
+		/**
+		 * Well, the function is not explicitly defined,
+		 * so it's obviously not a setuid one. Wasted
+		 * cycles on look up while the user had universal
+		 * access :(
+		 */
+		return;
+	}
 	if (func == NULL || (func->uid != user->uid &&
-		     access & ~func->access[user->auth_token].effective)) {
+	     access & ~func->access[user->auth_token].effective)) {
+		/* Access violation, report error. */
 		char name_buf[BOX_NAME_MAX + 1];
 		snprintf(name_buf, sizeof(name_buf), "%.*s", name_len, name);
 
 		tnt_raise(ClientError, ER_FUNCTION_ACCESS_DENIED,
 			  priv_name(access), user->name, name_buf);
 	}
+	if (func->setuid) {
+		/** Remember and change the current user id. */
+		setuid = func->setuid;
+		orig_auth_token = user->auth_token;
+		orig_uid = user->uid;
+		session_set_user(session(), func->auth_token, func->uid);
+	}
+}
+
+SetuidGuard::~SetuidGuard()
+{
+	if (setuid)
+		session_set_user(session(), orig_auth_token, orig_uid);
 }
 
 /**
@@ -497,12 +545,13 @@ box_lua_call(struct request *request, struct port *port)
 	/* Try to find a function by name */
 	int oc = box_lua_find(L, name, name + name_len);
 	/**
-	 * Check access to the function. Sic: the order
+	 * Check access to the function and optionally change
+	 * execution time user id (set user id). Sic: the order
 	 * is important, as is described in
 	 * https://github.com/tarantool/tarantool/issues/300
 	 * - if a function does not exist, say it first.
 	 */
-	access_check_func(name, name_len, user, PRIV_X);
+	SetuidGuard setuid(name, name_len, user, PRIV_X);
 	/* Push the rest of args (a tuple). */
 	const char *args = request->tuple;
 	uint32_t arg_count = mp_decode_array(&args);
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 6160d6019b390a68873fe72cd7dd28be88b7a99f..bb1e9a46f2f4a0d167c9a0b39baad5a224a7494b 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -911,13 +911,16 @@ local function object_name(object_type, object_id)
 end
 
 box.schema.func = {}
-box.schema.func.create = function(name)
+box.schema.func.create = function(name, opts)
     local _func = box.space[box.schema.FUNC_ID]
     local func = _func.index.name:get{name}
     if func then
             box.error(box.error.FUNCTION_EXISTS, name)
     end
-    _func:auto_increment{session.uid(), name}
+    check_param_table(opts, { setuid = 'boolean' })
+    opts = update_param_table(opts, { setuid = false })
+    opts.setuid = opts.setuid and 1 or 0
+    _func:auto_increment{session.uid(), name, opts.setuid}
 end
 
 box.schema.func.drop = function(name)
@@ -926,8 +929,8 @@ box.schema.func.drop = function(name)
     local fid = object_resolve('function', name)
     local privs = _priv:select{}
     for k, tuple in pairs(privs) do
-        if tuple[2] == 'function' and tuple[3] == function_id then
-            box.schema.user.revoke(tuple[1], tuple[4], tuple[2], tuple[3])
+        if tuple[3] == 'function' and tuple[4] == fid then
+            box.schema.user.revoke(tuple[2], tuple[5], tuple[3], tuple[4])
         end
     end
     _func:delete{fid}
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 347d166c0a3c88166b6755fcc480c7f054662121..5358c7d37b2e5a6272a0b86352ce452d6b5d10c7 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -167,7 +167,6 @@ box.schema.user.grant('user', 'read', 'universe')
 box.space._priv:select{id}
 box.schema.user.drop('user')
 box.space._priv:select{id}
-session = nil
 -- -----------------------------------------------------------
 -- Be a bit more rigorous in what is accepted in space _user
 -- -----------------------------------------------------------
@@ -175,3 +174,4 @@ box.space._user:insert{10, 1, 'name'}
 box.space._user:insert{10, 1, 'name', 'strange-object-type'}
 box.space._user:insert{10, 1, 'name', 'user', 'password'}
 box.space._user:insert{10, 1, 'name', 'role', 'password'}
+session = nil
diff --git a/test/box/access_bin.result b/test/box/access_bin.result
index 1e08ae289fd80d9d12e4f1b2de48722ecfb92b7c..23aa5b2d6f54fc0b81d192f24bec291cd079f5c2 100644
--- a/test/box/access_bin.result
+++ b/test/box/access_bin.result
@@ -28,3 +28,70 @@ c:close()
 box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 ---
 ...
+-- gh-488 suid functions
+--
+setuid_space = box.schema.space.create('setuid_space')
+---
+...
+setuid_space:create_index('primary')
+---
+...
+setuid_func = function() return box.space.setuid_space:auto_increment{} end
+---
+...
+box.schema.func.create('setuid_func')
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'setuid_func')
+---
+...
+c = net.box:new(0, box.cfg.listen)
+---
+...
+c:call("setuid_func")
+---
+- error: Read access denied for user 'guest' to space 'setuid_space'
+...
+session.su('guest')
+---
+...
+setuid_func()
+---
+- error: Read access denied for user 'guest' to space 'setuid_space'
+...
+session.su('admin')
+---
+...
+box.schema.func.drop('setuid_func')
+---
+...
+box.schema.func.create('setuid_func', { setuid = true })
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'setuid_func')
+---
+...
+c:call("setuid_func")
+---
+- - [1]
+...
+session.su('guest')
+---
+...
+setuid_func()
+---
+- error: Read access denied for user 'guest' to space 'setuid_space'
+...
+session.su('admin')
+---
+...
+c:close()
+---
+...
+box.schema.func.drop('setuid_func')
+---
+...
+setuid_space:drop()
+---
+...
+--
diff --git a/test/box/access_bin.test.lua b/test/box/access_bin.test.lua
index 692f60e332f787afcdaca4445aa05d82e9c98d03..f9841d7ea50220baa1ddb83ccf11d29607c37b0c 100644
--- a/test/box/access_bin.test.lua
+++ b/test/box/access_bin.test.lua
@@ -10,3 +10,27 @@ c:call("dostring", "session.su('admin')")
 c:call("dostring", "return session.user()")
 c:close()
 box.schema.user.revoke('guest', 'read,write,execute', 'universe')
+
+-- gh-488 suid functions
+--
+setuid_space = box.schema.space.create('setuid_space')
+setuid_space:create_index('primary')
+setuid_func = function() return box.space.setuid_space:auto_increment{} end
+box.schema.func.create('setuid_func')
+box.schema.user.grant('guest', 'execute', 'function', 'setuid_func')
+c = net.box:new(0, box.cfg.listen)
+c:call("setuid_func")
+session.su('guest')
+setuid_func()
+session.su('admin')
+box.schema.func.drop('setuid_func')
+box.schema.func.create('setuid_func', { setuid = true })
+box.schema.user.grant('guest', 'execute', 'function', 'setuid_func')
+c:call("setuid_func")
+session.su('guest')
+setuid_func()
+session.su('admin')
+c:close()
+box.schema.func.drop('setuid_func')
+setuid_space:drop()
+--