Issue #16787 has been updated by salewski (Alan Salewski).


Just noting that I rebased (and re-tested) the Ruby 2.7 backport PR on top of the latest changes in the 'ruby-2_7' branch.

The Ruby 2.6 backport PR did not need rebasing.


----------------------------------------
Bug #16787: [patch] allow Dir.home to work for non-login procs when $HOME not set
https://bugs.ruby-lang.org/issues/16787#change-87349

* Author: salewski (Alan Salewski)
* Status: Closed
* Priority: Normal
* ruby -v: ruby 2.8.0dev (2020-05-14T10:58:44Z master d7d0d01401) [x86_64-linux]
* Backport: 2.5: UNKNOWN, 2.6: REQUIRED, 2.7: REQUIRED
----------------------------------------
The 'Dir.home' method in versions of Ruby 2.x through the latest (2.7.1,
released 2020-03-31) is unable 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)

When the above conditions are met, the condition can be triggered simply:

    $ unset HOME

    $ ruby -e print "home is: #{Dir.home}\n";
        -e:1:in `home': couldn't find login name -- expanding `~' (ArgumentError)
                from -e:1:in `<main>'

The expectation is that Dir.home should be able to obtain the user's default
home directory regardless of whether or not the process is a (grand)child of
login(1). This behavior surfaced when running unit tests on GitHub Actions,
where the driving process did not use a login session. The unit tests failed
due to the different behavior of Dir.home in this scenario, but Dir.home ought
to behave the same either way.

The actual observed behavior is that Dir.home is able to obtain the user's
default home directory only for processes that are (grand)children of
login(1).

This behavior has been confirmed directly on (at least) the following
versions, though it is clear from browsing the code that this is long standing
behavior:

    $ ruby --version
    ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-linux-gnu]

    $ ruby --version
    ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]

    $ ruby --version
    ruby 2.8.0dev (2020-04-15T07:06:48Z master 69b3e0ac59) [x86_64-linux]

On a Unix-like OS, when the $HOME environment variable is not set, Ruby
attempts to obtain the user's home directory from the password database, as
one would expect. But the mechanism it uses only works for (grand)children of
login(1) (or work-alikes). In particular, it uses getlogin(3) to obtain the
username, with the intent to then obtain the user's password record (and its
'pw_dir' member) by looking it up by name (getpwnam(3)). That getlogin() call
fails, of course, because there is no logged-in user for the process.

The attached patch preserves the basic intent of the existing code, but allows
it work in the above scenario because the lookup for the user's record in the
password database is done directly by uid (getpwuid_r(3)), which is always
available, regardless of whether or not the process was launched by a
subprocess of login(1).

The patch applies cleanly against the HEAD of both 'master' and 'ruby_2_7',
and was tested against both on Debian GNU/Linux (buster/bullseye mix).

Motivation
----------
This issue [surfaced this past week](https://github.com/heroku/netrc/issues/50)
in the [heroku/netrc](https://github.com/heroku/netrc) project when CI builds
were first setup for the project using the GitHub Actions service. The process
that runs the unit tests there is not a (grand)child of login(1), so failed on
unit tests that exercise logic in that library when the $HOME environment
variable is not set (changing its value and/or unsetting it are legitimate
user activities; the tests were exercising that legitimate code path).

How to reproduce
----------------
In order to reproduce the issue you need to get some startup daemon process to
launch your ruby program; triggering the issue will not work for the ruby
process to be a subprocess of any process that is itself a (grand)child of a
login process. The GitHub Actions service happens to run code that way (see
above issue link for an example), but it can be simulated locally fairly
easily, too, using atd(8).

A process that is not a (grand)child of login(1) will not have its 'loginuid'
attribute set, so there will discrepancy between the values reported by id(1)
and the never-initialized value in '/proc/self/loginuid':

    $ /usr/bin/id
    uid=1001(runner) gid=115(docker) groups=115(docker)

    $ /usr/bin/id --user
    1001

    /usr/bin/getent passwd 1001
    runner:x:1001:115:,,,:/home/runner:/bin/bash

    $ cat /proc/self/loginuid
    4294967295

Note that '4294967295' is the largest unsigned value that will fit in 32 bits,
so it's signed value interpretation is '-1'. A 'loginuid' attribute with that
value is an indication that it has never been set. In a typical configuration,
it would be set as a side effect of the login process by PAM (see
pam_loginuid(8)).

The out-of-the-box 'atd(8)' configuration on Debian is also configured to have
PAM account for the 'loginuid' attribute, but for the purpose of testing the
fix for this issue, it can be easily disabled by editing the '/etc/pam.d/atd'
file. Find the line that looks like this:

    session    required   pam_loginuid.so

and comment it out so it looks like this:

    #session    required   pam_loginuid.so

That change will take effect as soon as you save the file; there is no need to
restart any services or anything like that.

To test the before and after behaviors, I simply ran a pristine and a patched
version of the code side-by-side, indirectly via at(1).

    $ cat /tmp/algo-doit2
    #!/bin/bash -

    set -x

    my_log_fpath='/tmp/algo-doit2.log'

    #RUBY_UNPATCHED='/usr/bin/ruby2.5'
    RUBY_UNPATCHED='/tmp/aljunk-ruby-from-git/bin/ruby'

    #RUBY_PATCHED='/tmp/aljunk-ruby-from-git-patched/bin/ruby'
    RUBY_PATCHED='/tmp/aljunk-ruby-from-git-patched-master/bin/ruby'

    (
      set -x

      /usr/bin/id
      /usr/bin/id --user

      printf '%s\n' $(cat /proc/self/loginuid)

      : DEBUG 1 unpatched: good
      "${RUBY_UNPATCHED}" -e 'print "home is: #{Dir.home}\n";'

      :
      : DEBUG 2 unpatched: now bad
      unset HOME
      "${RUBY_UNPATCHED}" -e 'print "home is: #{Dir.home}\n";'

    ) 1>> "${my_log_fpath}" 2>&1

    (
      set -x

      /usr/bin/id
      /usr/bin/id --user

      printf '%s\n' $(cat /proc/self/loginuid)

      : DEBUG 3 patched: good
      "${RUBY_PATCHED}" -e 'print "home is: #{Dir.home}\n";'

      :
      : DEBUG 4 patched: still good
      unset HOME
      "${RUBY_PATCHED}" -e 'print "home is: #{Dir.home}\n";'

    ) 1>> "${my_log_fpath}" 2>&1


For best results, run 'tail -F' on the output log in the background in your
shell:

    $ tail -F /tmp/algo-doit2.log &

With that setup, now each time you run the at(1) command you'll see the
output (from the log file) right away:

    $ at now < /tmp/algo-doit2
    warning: commands will be executed using /bin/sh
    job 17 at Wed Apr 15 06:04:00 2020
    + set -x
    + /usr/bin/id
    uid=1000(someuser) gid=1000(someuser) groups=1000(someuser)
    + /usr/bin/id --user
    1000
    + cat /proc/self/loginuid
    + printf %s\n 4294967295
    4294967295
    + : DEBUG 1 unpatched: good
    + /tmp/aljunk-ruby-from-git/bin/ruby -e print "home is: #{Dir.home}\n";
    home is: /home/someuser
    + :
    + : DEBUG 2 unpatched: now bad
    + unset HOME
    + /tmp/aljunk-ruby-from-git/bin/ruby -e print "home is: #{Dir.home}\n";
    -e:1:in `home': couldn't find login name -- expanding `~' (ArgumentError)
            from -e:1:in `<main>'
    + set -x
    + /usr/bin/id
    uid=1000(someuser) gid=1000(someuser) groups=1000(someuser)
    + /usr/bin/id --user
    1000
    + cat /proc/self/loginuid
    + printf %s\n 4294967295
    4294967295
    + : DEBUG 3 patched: good
    + /tmp/aljunk-ruby-from-git-patched-master/bin/ruby -e print "home is: #{Dir.home}\n";
    home is: /home/someuser
    + :
    + : DEBUG 4 patched: still good
    + unset HOME
    + /tmp/aljunk-ruby-from-git-patched-master/bin/ruby -e print "home is: #{Dir.home}\n";
    home is: /home/someuser

After testing, be sure to restore your atd(8) PAM configuration.


Legal
-----
I agree that the code in the attached patch may be distributed and/or modified
under Ruby's License.


Related Bugs
------------
Bug #12226 seems as if it might be related "in spirit", but that bug is
specific to MS Windows, and the current issue (and patch) is specific to
Unix-like systems.

    "Dir.home with valid named user raises ArgumentError on Windows"
    https://bugs.ruby-lang.org/issues/12226


---Files--------------------------------
allow-dir.home-for-non-login-procs.patch (2.79 KB)
allow-dir.home-for-non-login-procs-v2.patch (4.52 KB)
allow-dir.home-for-non-login-procs-v3.patch (16.8 KB)
allow-dir.home-for-non-login-procs-v4.patch (11.9 KB)
allow-dir.home-for-non-login-procs-v5.patch (12.6 KB)
allow-dir.home-for-non-login-procs-v6.patch (11.6 KB)
allow-dir.home-for-non-login-procs-v7.patch (14.6 KB)
allow-dir.home-for-non-login-procs-v7-rebased-2020-05-14.patch (14.6 KB)
ruby-2.6-backport-allow-dir.home-for-non-login-procs.patch (10.8 KB)
ruby-2.7-backport-allow-dir.home-for-non-login-procs.patch (10.8 KB)


-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>