Follow me on Twitter for my latest adventures!
This is going to be an article for beginners but with a spin at the end even for the most advanced users that involves analyzing source code of bash to determine how it actually works.
Let's say you want to run a command inside of chroot as another user all in a single command and be back at your shell. How do you do it? Turns out it can be easily done by combining several commands in a beautiful way:
sudo chroot /chroot su - user -c "cmd args"
This command combines four commands together - sudeo, chroot, su and cmd. Each command calls the next and when the last command finishes we return back to our shell (instead of being left inside of chroot.) It may seem like a pretty crazy combination of commands but that's the only way to get it working.
First, let's take a look at
man chroot. It says that to run
command inside chroot, you do this:
sudo chroot /chroot command
With this command we can run
command inside of chroot as root, but how do we run it as another user? How about using
command? Let's try that:
sudo chroot /chroot su - user
Nice! This runs
su inside of chroot as
user but we get an interactive shell and we don't return back to our shell. Let's take a closer look at
man su. It says that we could try:
sudo su - user -c "cmd args"
Man page says that this makes
cmd with arguments
args as a shell command. Great! So let's use this together with
sudo chroot /chroot su - user -c "cmd args"
Excellent! So are we done? Turns out not quite. There's still one problem. If the
user has a custom environment (such as different
PATHs to executables or some other customizations), it won't get initialized and we've to source the initialization file(s) ourselves:
sudo chroot /chroot su - user -c ". ~/.bash_profile; cmd args"
This does what we wanted and runs
cmd args inside of chroot located at
.bash_profile and after running
cmd it quits, and we're back at our shell.
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 one hell of an adventure debugging this. So 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!