From 6c5d3f060459c943bbba8394e7f1f91f79efb5a6 Mon Sep 17 00:00:00 2001
From: Alexander Turenko <alexander.turenko@tarantool.org>
Date: Mon, 2 Sep 2019 13:24:30 +0300
Subject: [PATCH] lua: pwd: split data fetch from deserialization

This commit does not change a user visible behaviour. It refactors pwd
module to explicitly divide code that fetches data from passwd / group
databases from code that performs deserialization of the data to Lua
tables.

The idea of splitting of those actions appears when it was observed that
a call of getpw() / getgr() leads to problems on some systems when it is
invoked during passwd / group database traveral.

Now it is more obvious that we don't call getpw() during passwd
traversal and getgr() during group traveral.

Follows up #4428 and #4447.
---
 src/lua/pwd.lua | 111 +++++++++++++++++++++++++++++++++---------------
 1 file changed, 77 insertions(+), 34 deletions(-)

diff --git a/src/lua/pwd.lua b/src/lua/pwd.lua
index ab0229a724..d59594b46e 100644
--- a/src/lua/pwd.lua
+++ b/src/lua/pwd.lua
@@ -1,6 +1,8 @@
 local ffi   = require('ffi')
 local errno = require('errno')
 
+-- {{{ Definitions
+
 -- GID_T, UID_T and TIME_T are, essentially, `integer types`.
 -- http://pubs.opengroup.org/onlinepubs/009695399/basedefs/sys/types.h.html
 ffi.cdef[[
@@ -80,6 +82,29 @@ ffi.cdef[[
     void           setgrent();
 ]]
 
+-- }}}
+
+-- {{{ Error handling
+
+local pwgr_errstr = "get%s failed [errno %d]: %s"
+
+-- Use it in the following way: set errno to zero, call a passwd /
+-- group function, then call this function to check whether there
+-- was an error.
+local function pwgrcheck(func_name, pwgr)
+    if pwgr ~= nil then
+        return
+    end
+    if errno() == 0 then
+        return
+    end
+    error(pwgr_errstr:format(func_name, errno(), errno.strerror()))
+end
+
+-- }}}
+
+-- {{{ Call passwd / group database
+
 local function _getpw(uid)
     local pw = nil
     errno(0)
@@ -90,6 +115,7 @@ local function _getpw(uid)
     else
         error("Bad type of uid (expected 'string'/'number')")
     end
+    pwgrcheck('_getpw', pw)
     return pw
 end
 
@@ -103,22 +129,15 @@ local function _getgr(gid)
     else
         error("Bad type of gid (expected 'string'/'number')")
     end
+    pwgrcheck('_getgr', gr)
     return gr
 end
 
-local pwgr_errstr = "get%s failed [errno %d]: %s"
+-- }}}
 
-local function getgr(gid)
-    if gid == nil then
-        gid = tonumber(ffi.C.getgid())
-    end
-    local gr = type(gid) == 'cdata' and gid or _getgr(gid)
-    if gr == nil then
-        if errno() ~= 0 then
-            error(pwgr_errstr:format('gr', errno(), errno.strerror()))
-        end
-        return nil
-    end
+-- {{{ Serialize passwd / group structures to tables
+
+local function grtotable(gr)
     local gr_mem, group_members = gr.gr_mem, {}
     local i = 0
     while true do
@@ -129,48 +148,69 @@ local function getgr(gid)
         table.insert(group_members, ffi.string(member))
         i = i + 1
     end
-    local group = {
+    return {
         id      = tonumber(gr.gr_gid),
         name    = ffi.string(gr.gr_name),
         members = group_members,
     }
-    return group
 end
 
-local function getpw(uid)
-    if uid == nil then
-        uid = tonumber(ffi.C.getuid())
-    end
-    local pw = type(uid) == 'cdata' and uid or _getpw(uid)
-    if pw == nil then
-        if errno() ~= 0 then
-            error(pwgr_errstr:format('pw', errno(), errno.strerror()))
-        end
-        return nil
-    end
+-- gr is optional
+local function pwtotable(pw, gr)
     local user = {
         name    = ffi.string(pw.pw_name),
         id      = tonumber(pw.pw_uid),
-        group   = getgr(pw.pw_gid),
         workdir = ffi.string(pw.pw_dir),
         shell   = ffi.string(pw.pw_shell),
     }
+    if gr ~= nil then
+        user.group = grtotable(gr)
+    end
     return user
 end
 
+-- }}}
+
+-- {{{ Public API functions
+
+local function getgr(gid)
+    if gid == nil then
+        gid = tonumber(ffi.C.getgid())
+    end
+    local gr = _getgr(gid)
+    if gr == nil then
+        return nil
+    end
+    return grtotable(gr)
+end
+
+local function getpw(uid)
+    if uid == nil then
+        uid = tonumber(ffi.C.getuid())
+    end
+    local pw = _getpw(uid)
+    if pw == nil then
+        return nil
+    end
+    local gr = _getgr(pw.pw_gid) -- can be nil
+    return pwtotable(pw, gr)
+end
+
 local function getpwall()
     ffi.C.setpwent()
     local pws = {}
+    -- Note: Don't call _getpw() during the loop: it leads to drop
+    -- of a getpwent() current entry to a first one on CentOS 6
+    -- and FreeBSD 12.
     while true do
         errno(0)
         local pw = ffi.C.getpwent()
         if pw == nil then
-            if errno() ~= 0 then
-                error(pwgr_errstr:format('pwall', errno(), errno.strerror()))
-            end
+            pwgrcheck('getpwall', pw)
             break
         end
-        table.insert(pws, getpw(pw))
+        local gr = _getgr(pw.pw_gid) -- can be nil
+        table.insert(pws, pwtotable(pw, gr))
     end
     ffi.C.endpwent()
     return pws
@@ -179,21 +219,24 @@ end
 local function getgrall()
     ffi.C.setgrent()
     local grs = {}
+    -- Note: Don't call _getgr() during the loop: it leads to drop
+    -- of a getgrent() current entry to a first one on CentOS 6
+    -- and FreeBSD 12.
     while true do
         errno(0)
         local gr = ffi.C.getgrent()
         if gr == nil then
-            if errno() ~= 0 then
-                error(pwgr_errstr:format('grall', errno(), errno.strerror()))
-            end
+            pwgrcheck('getgrall', gr)
             break
         end
-        table.insert(grs, getgr(gr))
+        table.insert(grs, grtotable(gr))
     end
     ffi.C.endgrent()
     return grs
 end
 
+-- }}}
+
 -- Workaround pwd.getpwall() issue on Fedora 29: successful
 -- getgrent() call that should normally return NULL and preserve
 -- errno, set it to ENOENT due to systemd-nss issue [1] when a
-- 
GitLab