commit 5369b67fc8798517526ba767a64538643426da39
Author: Alan D. Salewski <salewski@att.net>
Date:   Wed Apr 15 07:29:16 2020 -0400

    Allow Dir.home to work for non-login procs when $HOME not set
    
    Allow the 'Dir.home' method to reliably locate the user's home directory when
    all three of the following are true at the same time:
    
        1. Ruby is running on a Unix-like OS
        2. The $HOME environment variable is not set
        3. The process is not a descendant of login(1) (or a work-alike)
    
    The prior behavior was that the lookup could only work for login-descended
    processes.
    
    This is accomplished by looking up the user's record in the password database
    by uid (getpwuid_r(3)) as a fallback to the lookup by name (getpwname_r(3))
    which is still attempted first (based on the name, if any, returned by
    getlogin_r(3)).
    
    If getlogin_r(3), getpwnam_r(3), and/or getpwuid_r(3) is not available at
    compile time, will fallback on using their respective non-*_r() variants:
    getlogin(3), getpwnam(3), and/or getpwuid(3).
    
    The rationale for attempting to do the lookup by name prior to doing it by uid
    is to accommodate the possibility of multiple login names (each with its own
    record in the password database, so each with a potentially different home
    directory) being mapped to the same uid (as is explicitly allowed for by
    POSIX; see getlogin(3posix)).
    
    Preserves the existing behavior for login-descended processes, and adds the
    new capability of having Dir.home being able to find the user's home directory
    for non-login-descended processes.
    
    Fixes [Bug #16787]
    
    Related discussion:
        https://bugs.ruby-lang.org/issues/16787
        https://github.com/ruby/ruby/pull/3034

diff --git a/configure.ac b/configure.ac
index ba86deffbe..36fa1a117c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1877,10 +1877,15 @@ AC_CHECK_FUNCS(getgidx)
 AC_CHECK_FUNCS(getgrnam)
 AC_CHECK_FUNCS(getgrnam_r)
 AC_CHECK_FUNCS(getgroups)
+AC_CHECK_FUNCS(getlogin)
+AC_CHECK_FUNCS(getlogin_r)
 AC_CHECK_FUNCS(getpgid)
 AC_CHECK_FUNCS(getpgrp)
 AC_CHECK_FUNCS(getpriority)
+AC_CHECK_FUNCS(getpwnam)
 AC_CHECK_FUNCS(getpwnam_r)
+AC_CHECK_FUNCS(getpwuid)
+AC_CHECK_FUNCS(getpwuid_r)
 AC_CHECK_FUNCS(getrandom)
 AC_CHECK_FUNCS(getresgid)
 AC_CHECK_FUNCS(getresuid)
diff --git a/file.c b/file.c
index 542a4fc7ee..3a467df7ba 100644
--- a/file.c
+++ b/file.c
@@ -171,6 +171,41 @@ int flock(int, int);
 #include "ruby/thread.h"
 #include "ruby/util.h"
 
+#if defined(HAVE_UNISTD_H)
+# if defined(HAVE_GETLOGIN_R)
+#  define USE_GETLOGIN_R 1
+#  define GETLOGIN_R_SIZE_DEFAULT   0x100
+#  define GETLOGIN_R_SIZE_LIMIT    0x1000
+#  if defined(_SC_LOGIN_NAME_MAX)
+#    define GETLOGIN_R_SIZE_INIT sysconf(_SC_LOGIN_NAME_MAX)
+#  else
+#    define GETLOGIN_R_SIZE_INIT GETLOGIN_R_SIZE_DEFAULT
+#  endif
+# elif defined(HAVE_GETLOGIN)
+#  define USE_GETLOGIN 1
+# endif
+#endif
+
+#if defined(HAVE_PWD_H)
+# define GETPW_R_SIZE_DEFAULT 0x1000
+# define GETPW_R_SIZE_LIMIT  0x10000
+# if defined(_SC_GETPW_R_SIZE_MAX)
+#  define GETPW_R_SIZE_INIT sysconf(_SC_GETPW_R_SIZE_MAX)
+# else
+#  define GETPW_R_SIZE_INIT GETPW_R_SIZE_DEFAULT
+# endif
+# if defined(HAVE_GETPWNAM_R)
+#  define USE_GETPWNAM_R 1
+# elif defined(HAVE_GETPWNAM)
+#  define USE_GETPWNAM 1
+# endif
+# if defined(HAVE_GETPWUID_R)
+#  define USE_GETPWUID_R 1
+# elif defined(HAVE_GETPWUID)
+#  define USE_GETPWUID 1
+# endif
+#endif
+
 VALUE rb_cFile;
 VALUE rb_mFileTest;
 VALUE rb_cStat;
@@ -3596,34 +3631,274 @@ rb_home_dir_of(VALUE user, VALUE result)
 }
 
 #ifndef _WIN32
+VALUE rb_getlogin(void);
+VALUE rb_getpwdirnam_for_login(void);  /* read as: "get pwd db home dir by username for login" */
+VALUE rb_getpwdiruid(void);            /* read as: "get pwd db home dir by uid" */
+
 VALUE
 rb_default_home_dir(VALUE result)
 {
     const char *dir = getenv("HOME");
+    if (dir)
+        return copy_home_path(result, dir);
 
-#if defined HAVE_PWD_H
-    if (!dir) {
-	const char *login = getlogin();
-	if (login) {
-	    struct passwd *pw = getpwnam(login);
-	    if (pw) {
-		copy_home_path(result, pw->pw_dir);
-		endpwent();
-		return result;
-	    }
-	    endpwent();
-	    rb_raise(rb_eArgError, "couldn't find HOME for login `%s' -- expanding `~'",
-		     login);
-	}
-	else {
-	    rb_raise(rb_eArgError, "couldn't find login name -- expanding `~'");
-	}
+    /* We'll look up the user's default home dir in the password db by login
+     * name, if possible, and failing that will fall back to looking the
+     * information up by uid (as would be needed for processes that are not a
+     * descendant of login(1) or a work-alike).
+     *
+     * While the lookup by uid is more likely to succeed (since we always have
+     * a uid, but may or may not have a login name), we prefer first looking
+     * up by name to accommodate the possibility of multiple login names (each
+     * with its own record in the password database, so each with a
+     * potentially different home directory) being mapped to the same uid (as
+     * explicitly allowed for by POSIX; see getlogin(3posix)).
+     */
+    VALUE pw_dir = rb_getpwdirnam_for_login();
+    if (NIL_P(pw_dir)) {
+        pw_dir = rb_getpwdiruid();
+        if (NIL_P(pw_dir)) {
+            /* Please keep this message "as is" for backward-compatibility */
+            rb_raise(rb_eArgError, "couldn't find login name -- expanding `~'");
+        }
+    }
+
+    /* found it */
+    copy_home_path(result, RSTRING_PTR(pw_dir));
+    rb_str_resize(pw_dir, 0);
+    return result;
+}
+
+/**
+ * Best-effort attempt to obtain the name of the login user, if any,
+ * associated with the process. Processes not descended from login(1) (or
+ * similar) may not have a logged-in user; returns Qnil in that case.
+ */
+VALUE
+rb_getlogin(void)
+{
+#if ( !defined(USE_GETLOGIN_R) && !defined(USE_GETLOGIN) )
+    return Qnil;
+#else
+    char MAYBE_UNUSED(*login) = NULL;
+
+# ifdef USE_GETLOGIN_R
+
+    long loginsize = GETLOGIN_R_SIZE_INIT;  /* maybe -1 */
+
+    if (loginsize < 0)
+        loginsize = GETLOGIN_R_SIZE_DEFAULT;
+
+    VALUE maybe_result = rb_str_buf_new(loginsize);
+
+    login = RSTRING_PTR(maybe_result);
+    loginsize = rb_str_capacity(maybe_result);
+    rb_str_set_len(maybe_result, loginsize);
+
+    int gle;
+    errno = 0;
+    while ((gle = getlogin_r(login, loginsize)) != 0) {
+
+        if (gle == ENOTTY || gle == ENXIO || gle == ENOENT) {
+            rb_str_resize(maybe_result, 0);
+            return Qnil;
+        }
+
+        if (gle != ERANGE || loginsize >= GETLOGIN_R_SIZE_LIMIT) {
+            rb_str_resize(maybe_result, 0);
+            rb_syserr_fail(gle, "getlogin_r");
+        }
+
+        rb_str_modify_expand(maybe_result, loginsize);
+        login = RSTRING_PTR(maybe_result);
+        loginsize = rb_str_capacity(maybe_result);
+    }
+
+    if (login == NULL) {
+        rb_str_resize(maybe_result, 0);
+        return Qnil;
+    }
+
+    return maybe_result;
+
+# elif USE_GETLOGIN
+
+    errno = 0;
+    login = getlogin();
+    if (errno) {
+        if (errno == ENOTTY || errno == ENXIO || errno == ENOENT) {
+            return Qnil;
+        }
+        rb_syserr_fail(errno, "getlogin");
+    }
+
+    return login ? rb_str_new_cstr(login) : Qnil;
+# endif
+
+#endif
+}
+
+VALUE
+rb_getpwdirnam_for_login(void)
+{
+#if ( !defined(USE_GETPWNAM_R) && !defined(USE_GETPWNAM) )
+    return Qnil;
+#else
+    VALUE login_name = rb_getlogin();
+
+    if (NIL_P(login_name)) {
+        /* nothing to do; no name with which to query the password database */
+        return Qnil;
     }
+
+    char *login = RSTRING_PTR(login_name);
+
+    struct passwd *pwptr;
+
+# ifdef USE_GETPWNAM_R
+
+    struct passwd pwdnm;
+    char *bufnm;
+    long bufsizenm = GETPW_R_SIZE_INIT;  /* maybe -1 */
+
+    if (bufsizenm < 0)
+        bufsizenm = GETPW_R_SIZE_DEFAULT;
+
+    VALUE getpwnm_tmp = rb_str_tmp_new(bufsizenm);
+
+    bufnm = RSTRING_PTR(getpwnm_tmp);
+    bufsizenm = rb_str_capacity(getpwnm_tmp);
+    rb_str_set_len(getpwnm_tmp, bufsizenm);
+
+    int enm;
+    errno = 0;
+    while ((enm = getpwnam_r(login, &pwdnm, bufnm, bufsizenm, &pwptr)) != 0) {
+
+        if (enm == ENOENT || enm== ESRCH || enm == EBADF || enm == EPERM) {
+            /* not found; non-errors */
+            rb_str_resize(getpwnm_tmp, 0);
+            return Qnil;
+        }
+
+        if (enm != ERANGE || bufsizenm >= GETPW_R_SIZE_LIMIT) {
+            rb_str_resize(getpwnm_tmp, 0);
+            rb_syserr_fail(enm, "getpwnam_r");
+        }
+
+        rb_str_modify_expand(getpwnm_tmp, bufsizenm);
+        bufnm = RSTRING_PTR(getpwnm_tmp);
+        bufsizenm = rb_str_capacity(getpwnm_tmp);
+    }
+
+    if (pwptr == NULL) {
+        /* no record in the password database for the login name */
+        rb_str_resize(getpwnm_tmp, 0);
+        return Qnil;
+    }
+
+    /* found it */
+    VALUE result = rb_str_new_cstr(pwptr->pw_dir);
+    rb_str_resize(getpwnm_tmp, 0);
+    return result;
+
+# elif USE_GETPWNAM
+
+    errno = 0;
+    pwptr = getpwnam(login);
+    if (pwptr) {
+        /* found it */
+        return rb_str_new_cstr(pwptr->pw_dir);
+    }
+    if (errno
+        /*   avoid treating as errors errno values that indicate "not found" */
+        && ( errno != ENOENT && errno != ESRCH && errno != EBADF && errno != EPERM)) {
+        rb_syserr_fail(errno, "getpwnam");
+    }
+
+    return Qnil;  /* not found */
+# endif
+
 #endif
-    if (!dir) {
-	rb_raise(rb_eArgError, "couldn't find HOME environment -- expanding `~'");
+}
+
+/**
+ * Look up the user's dflt home dir in the password db, by uid.
+ */
+VALUE
+rb_getpwdiruid(void)
+{
+# if !defined(USE_GETPWUID_R) && !defined(USE_GETPWUID)
+    /* Should never happen... </famous-last-words> */
+    return Qnil;
+# else
+    uid_t ruid = getuid();
+
+    struct passwd *pwptr;
+
+# ifdef USE_GETPWUID_R
+
+    struct passwd pwdid;
+    char *bufid;
+    long bufsizeid = GETPW_R_SIZE_INIT;  /* maybe -1 */
+
+    if (bufsizeid < 0)
+        bufsizeid = GETPW_R_SIZE_DEFAULT;
+
+    VALUE getpwid_tmp = rb_str_tmp_new(bufsizeid);
+
+    bufid = RSTRING_PTR(getpwid_tmp);
+    bufsizeid = rb_str_capacity(getpwid_tmp);
+    rb_str_set_len(getpwid_tmp, bufsizeid);
+
+    int eid;
+    errno = 0;
+    while ((eid = getpwuid_r(ruid, &pwdid, bufid, bufsizeid, &pwptr)) != 0) {
+
+        if (eid == ENOENT || eid== ESRCH || eid == EBADF || eid == EPERM) {
+            /* not found; non-errors */
+            rb_str_resize(getpwid_tmp, 0);
+            return Qnil;
+        }
+
+        if (eid != ERANGE || bufsizeid >= GETPW_R_SIZE_LIMIT) {
+            rb_str_resize(getpwid_tmp, 0);
+            rb_syserr_fail(eid, "getpwuid_r");
+        }
+
+        rb_str_modify_expand(getpwid_tmp, bufsizeid);
+        bufid = RSTRING_PTR(getpwid_tmp);
+        bufsizeid = rb_str_capacity(getpwid_tmp);
+    }
+
+    if (pwptr == NULL) {
+        /* no record in the password database for the uid */
+        rb_str_resize(getpwid_tmp, 0);
+        return Qnil;
     }
-    return copy_home_path(result, dir);
+
+    /* found it */
+    VALUE result = rb_str_new_cstr(pwptr->pw_dir);
+    rb_str_resize(getpwid_tmp, 0);
+    return result;
+
+# elif defined(USE_GETPWUID)
+
+    errno = 0;
+    pwptr = getpwuid(ruid);
+    if (pwptr) {
+        /* found it */
+        return rb_str_new_cstr(pwptr->pw_dir);
+    }
+    if (errno
+        /*   avoid treating as errors errno values that indicate "not found" */
+        && ( errno == ENOENT || errno == ESRCH || errno == EBADF || errno == EPERM)) {
+        rb_syserr_fail(errno, "getpwuid");
+    }
+
+    return Qnil;  /* not found */
+# endif
+
+#endif /* !defined(USE_GETPWUID_R) && !defined(USE_GETPWUID) */
 }
 
 static VALUE
