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

    16787: 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)
    
    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), respectively.
    
    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 explicitly allowed for by POSIX;
    see getlogin(3posix)). So we preserve that existing behavior for
    login-descended processes, and add the new capability of having Dir.home being
    able to find the user's home directory for non-login-descended processes.
    
    THIS CHANGESET IS NOT YET READY FOR PRIME TIME. PLEASE DO NOT MERGE THIS INTO
    THE MASTER BRANCH.
    
    While this version works, has been descently tested, and should be maximally
    portable, the downside is that the code is now barely readable. It is
    basically hundreds of lines of cpp conditionals and is difficult to
    follow. Some of the cpp junk is '#error' statements, intended to help detect
    bugs accidentally introduced into this somewhat delicate arrangment in the
    future; those also make the cpp noise even more verbose, though.
    
    I'm committing this as a checkpoint, as I want to see if the build
    infra. turns up anything I'll need to fix before the next leg of this
    thing. While that runs, I will be looking for ways to split this apart and
    otherwise restore readability.

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..c899b5c156 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;
@@ -3600,30 +3635,242 @@ 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);
+#if !defined(USE_GETLOGIN_R) && !defined(USE_GETLOGIN) \
+ && !defined(USE_GETPWNAM_R) && !defined(USE_GETPWNAM) \
+ && !defined(USE_GETPWUID_R) && !defined(USE_GETPWUID)
+    rb_raise(rb_eArgError, "couldn't find HOME environment -- expanding `~'");
+
+#else
+
+    /* 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)).
+     */
+    struct passwd MAYBE_UNUSED(*pwptr);
+
+    char MAYBE_UNUSED(*login) = NULL;
+
+# if ( defined(USE_GETLOGIN_R) || defined(USE_GETLOGIN) ) \
+  && ( defined(USE_GETPWNAM_R) || defined(USE_GETPWNAM) )
+
+    /*
+     * Try to look up the user's dflt home dir in the password db, by login name
+     */
+#  ifdef USE_GETLOGIN_R
+
+    long loginsize = GETLOGIN_R_SIZE_INIT;  /* maybe -1 */
+
+    if (loginsize < 0)
+	loginsize = GETLOGIN_R_SIZE_DEFAULT;
+
+    VALUE getlogin_tmp = rb_str_tmp_new(loginsize);
+
+    login = RSTRING_PTR(getlogin_tmp);
+    loginsize = rb_str_capacity(getlogin_tmp);
+    rb_str_set_len(getlogin_tmp, loginsize);
+
+    int gle;
+    errno = 0;
+    while ((gle = getlogin_r(login, loginsize)) != 0) {
+
+        if (gle == ENOTTY || gle == ENXIO) {
+	    rb_str_resize(getlogin_tmp, 0);
+            goto MAYBE_TRY_LOOKUP_BY_UID;
+        }
+
+	if (gle != ERANGE || loginsize >= GETLOGIN_R_SIZE_LIMIT) {
+	    rb_str_resize(getlogin_tmp, 0);
+	    rb_syserr_fail(gle, "getlogin_r");
 	}
-	else {
-	    rb_raise(rb_eArgError, "couldn't find login name -- expanding `~'");
+	rb_str_modify_expand(getlogin_tmp, loginsize);
+	login = RSTRING_PTR(getlogin_tmp);
+	loginsize = rb_str_capacity(getlogin_tmp);
+    }
+
+    if (login == NULL) {
+	rb_str_resize(getlogin_tmp, 0);
+        /* Fall through to lookup by uid functionality (if possible) */
+    }
+
+#  elif USE_GETLOGIN
+
+    errno = 0;
+    login = getlogin();
+    if (errno == ENOMEM) {
+        rb_syserr_fail(errno, "getlogin");
+    }
+    /* Ignore all other possible values for errno here; none
+     * would cause us to not want to attempt our lookup by uid
+     * below. Keep going...
+     */
+#  else
+#   error Hello!  Ruby developers believe this message must not happen.
+#   error BUG: Either USE_GETLOGIN_R or USE_GETLOGIN should be defined here.
+#   error If you encounter this message, can you file a bug report?
+#   error Remember to attach a detailed description of your environment.
+#   error Thank you!
+#  endif
+
+    if (!login)
+        goto MAYBE_TRY_LOOKUP_BY_UID;
+
+#  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) {
+	    rb_str_resize(getpwnm_tmp, 0);
+            goto MAYBE_TRY_LOOKUP_BY_UID;
+        }
+
+	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);
     }
-#endif
-    if (!dir) {
-	rb_raise(rb_eArgError, "couldn't find HOME environment -- expanding `~'");
+
+    if (pwptr == NULL) {
+	/* no record in the password database for the login name */
+	rb_str_resize(getpwnm_tmp, 0);
+        goto MAYBE_TRY_LOOKUP_BY_UID;
+    }
+
+    copy_home_path(result, pwptr->pw_dir);
+    rb_str_resize(getpwnm_tmp, 0);
+
+    return result;
+
+#  elif USE_GETPWNAM
+
+    errno = 0;
+    pwptr = getpwnam(login);
+    if (pwptr) {
+        copy_home_path(result, pwptr->pw_dir);
+        return result;
+    }
+    if (errno && ( errno == EINTR || errno == EIO || errno == ENOMEM )) {
+        rb_syserr_fail(errno, "getpwnam");
     }
-    return copy_home_path(result, dir);
+    /* Ignore all other possible values for errno here; none
+     * would cause us to not want to attempt our lookup by uid
+     * below. Keep going...
+     */
+#  else
+#   error Hello!  Ruby developers believe this message must not happen.
+#   error BUG: Either USE_GETPWNAM_R or USE_GETPWNAM should be defined here.
+#   error If you encounter this message, can you file a bug report?
+#   error Remember to attach a detailed description of your environment.
+#   error Thank you!
+#  endif
+
+# endif
+
+MAYBE_TRY_LOOKUP_BY_UID: MAYBE_UNUSED();
+
+# if !defined(USE_GETPWUID_R) && !defined(USE_GETPWUID)
+    /* Should never happen... </famous-last-words> */
+    rb_raise(rb_eArgError, "couldn't find home dir by login name (%s)",
+             ((login) ? login : "<UNAVAILABLE>"));
+# else
+    /*
+     * Try to look up the user's dflt home dir in the password db, by uid
+     */
+    uid_t ruid = getuid();
+
+#  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 != 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);
+	rb_raise(rb_eArgError, "getpwuid_r couldn't find HOME for uid `%ld' -- expanding `~'",
+		 (long)ruid);
+    }
+
+    copy_home_path(result, pwptr->pw_dir);
+    rb_str_resize(getpwid_tmp, 0);
+
+    return result;
+
+#  elif defined(USE_GETPWUID)
+
+    errno = 0;
+    pwptr = getpwuid(ruid);
+    if (pwptr == NULL) {
+	if (errno) {
+	    rb_syserr_fail(errno, "getpwuid");
+	}
+	rb_raise(rb_eArgError, "getpwuid couldn't find HOME for uid `%ld' -- expanding `~'",
+		 (long)ruid);
+    }
+    
+    copy_home_path(result, pwptr->pw_dir);
+
+    return result;
+
+#  else
+#    error Hello!  Ruby developers believe this message must not happen.
+#    error BUG: Either USE_GETPWUID_R or USE_GETPWUID should be defined here.
+#    error If you encounter this message, can you file a bug report?
+#    error Remember to attach a detailed description of your environment.
+#    error Thank you!
+#  endif
+# endif /* !defined(USE_GETPWUID_R) && !defined(USE_GETPWUID) */
+
+#endif
 }
 
 static VALUE
