Follow me on Twitter for my latest adventures!
I was solving a problem for Testling and run into difficulties. Every Github user who signs up for Testling gets a Unix user on Testling's servers. When they commit code, we get a Github hook, switch to their user, clone their repo, and run their tests.
Since we're releasing early and releasing often, and following worse is better philosophy, we just setup a chroot environment for all Testling users, then we just
chroot to this environment, and
su - username and run scripts that run the tests all with a single command:
sudo chroot /chroot su - user -c "cmd args"
This is pretty nifty! This command combines four commands together -
cmd. Each command calls the next one and when the last command finishes we return back to our shell and exit (instead of being left inside of chroot in an interactive shell mode as
Now here's the problem I run into. For some reason
user's environment never got executed. Our test runners setup
PATHs and other customizations but
.bash_profile was never ran.
The solution that I found was to run
.bash_profile yourself, like this:
sudo chroot /chroot su - user -c ". ~/.bash_profile; cmd args"
This does what I wanted and runs
cmd args inside of chroot located at
.bash_profile and after running
cmd it quits.
On Sourcing ~/.bash_profile
Some people couldn't understand why I was sourcing
~/.bash_profile. The reason is because I was unable to find another way to invoke the login shell and have the environment initialized.
I tried this:
sudo chroot /chroot su -l user -c "cmd args"
And it just wouldn't execute
~/.bash_profile. Su's man page says that
-l invokes a login shell, however I don't see that happening. Here's the transcript that demonstrates it:
pkrumins$ pwd /chroot pkrumins$ sudo cat ./home/testuser1/.bash_profile echo "moo" PATH="/test/bin" pkrumins$ sudo chroot /chroot su -l testuser1 -c 'echo $PATH' /bin:/usr/bin pkrumins$ sudo chroot /chroot su -l testuser1 moo testuser1$ echo $PATH /test/bin testuser1$ ^D logout pkrumins$ sudo chroot /chroot su -l testuser1 -c '. ~/.bash_profile; echo $PATH' moo /test/bin pkrumins$ sudo chroot /chroot su -l testuser1 moo testuser1$ /bin/bash -l -c 'echo $PATH' moo /test/bin
This definitely looks like a bug in
su to me when
-c are used together. So the only way to get the environment loaded is by sourcing the initialization files yourself.
I went through
su's source code (can be found in util-linux package) and it turns out it's not a bug in
Here are the arguments that
su passes to
execv when I run
su user -c 'echo $PATH' (see
run_shell function in
shell: /bin/bash args: -bash args: -c args: echo $PATH
As you can see,
-, which makes bash a login shell, so bash should be executing the startup files, but it does not!
To figure out what was happening, I built a custom version of bash and added a bunch of debugging statements. I found that there are two different login-shell states! Who'd have thought? One that you get when you've set
-, the other that you get when you've used the
-l argument (to bash, not su!).
-, and you're using
-c to execute code, bash will not execute startup files because it's in this "non-interactive positive-login-shell" state (see
run_startup_files function in bash's
shell.c source file.)
However, if you use
-c, it goes into "non-interactive negative-login-shell" state, and it will execute startup files. But we can't pass
-l argument to
bash, we can only pass it to
su, which is not the same as passing it to
That was an exciting adventure debugging this. If you want bash to execute startup files through
su, use the
. ~/.bash_profile sourcing trick! (Or alternatively define
NON_INTERACTIVE_LOGIN_SHELLS when building bash.)
Until next time!