rmyorston / busybox-w32

WIN32 native port of BusyBox.
https://frippery.org/busybox
Other
674 stars 124 forks source link

drop and su as "man in the middle" #438

Closed ale5000-git closed 1 month ago

ale5000-git commented 1 month ago

Hi, since bash for Windows doesn't offer drop and su I would like to use busybox for these from inside real bash. Now the problem is that the command to preserve parameters with spaces became hellish complicated and error prone. Also if I just run bash it use the bash alias of busybox.

From real bash I want to do: bash => drop => bash => my_script.sh

Sample command:

export SHELL_CMD='C:/Program Files/Git/usr/bin/bash.exe'
busybox drop -c "'${SHELL_CMD:?}' -c '${MAIN_DIR:?}/cmdline.sh \"\${@}\"' \"\${0}\" \"\${@}\"" -- "${0}" "${@}"

Is it possible to add support for the -s parameter as in the real su on Linux to both su and drop of busybox for Windows?

       -s, --shell=shell
           Run the specified shell instead of the default.

For the -s parameter it should ignore (or at least unprefer) the internal alias: sh ash bash since if one want to use busybox it won't specify the parameter.

This would reduce the complexity of the command a lot.

rmyorston commented 1 month ago

The su applet now has a -s option which behaves much like that of util-linux.

Some features of su only work if the built-in shell is used:

Prerelease binaries are available (PRE-5446 or above).

ale5000-git commented 1 month ago

Thanks, with full path it seems to work fine but it would be nice that it also works simply as su -s 'bash'.

Having:

$ which -a bash
bash
C:/Program Files/Git/usr/bin/bash.exe

Could it just skip the internal one and use the one found on PATH?

Edit: This would also allow su -s cmd and su -s powershell.


Some features of su only work if the built-in shell is used:

  • The -N option, which keeps the console open when the shell is finished.
  • The ability to retain the current working directory when the busybox-w32 binary is in certain 'system' directories.
  • The friendly message in the console title bar.

These were expected.

avih commented 1 month ago

it would be nice that it also works simply as su -s 'bash'

No, because bash is a busybox applet.

But you can disable the "bash" applet using BB_OVERRIDE_APPLETS=bash or make it prefer bash at $PATH over the applet using BB_OVERRIDE_APPLETS=";bash".

ale5000-git commented 1 month ago

Currently it doesn't search in $PATH at all and just fail. Also in this specific case we aren't using busybox completely but only in the middle so having to set a busybox specific env var and then unset it later is a bit unpleasant. To note also that using the -s parameter automatically exclude busybox specific behaviours so it make sense.

rmyorston commented 1 month ago

Currently it doesn't search in $PATH at all and just fail.

Correct. That's what util-linux su does too: it calls execv(3) on the supplied shell option argument, so there's no PATH search.

There is a slight difference: busybox-w32 su performs the check on the shell path before calling ShellExecuteEx(), so in case of error there's no UAC prompt. util-linux su calls execv(3) (and so detects any error) after asking for the password.

This only affects an error case and the busybox-w32 behaviour can be considered less bothersome.

If you want to run PowerShell with elevated privileges you can do su -s $(which powershell). (Though perhaps with more error checking.)

rmyorston commented 1 month ago

The drop applet now has a -s option too. PRE-5447 or above.

ale5000-git commented 1 month ago

If you want to run PowerShell with elevated privileges you can do su -s $(which powershell). (Though perhaps with more error checking.)

Just as quick test I have tried: su -s "$(which powershell)" -c 'dir c:/; sleep 5' and it works perfectly.

Instead this: su -s "$(which cmd)" -c 'dir c:\ && sleep 5' just open cmd as interactive and no command is executed.

rmyorston commented 1 month ago

su assumes that the shell takes a -c option for the commands to be executed.

But cmd.exe doesn't: it expects a /c option.

(That's one reason why drop has a separate cdrop alias to run cmd.exe, so it can use the correct /c option.)

ale5000-git commented 1 month ago

I admit it isn't nice but isn't possible to check if the -s option end in /cmd.exe and then behave differently (since the use of cmd.exe is quite common on Windows)?

rmyorston commented 1 month ago

Sure, I've already got the code that does it. Give me a few minutes to push it through the system...

ale5000-git commented 1 month ago

Thanks.

rmyorston commented 1 month ago

OK. PRE-5448 has special treatment when cmd.exe is used as the 'shell' with su -c.

ale5000-git commented 1 month ago

I'm not sure what is wrong because the window last only one instant but this fail: su -W -s "$(which cmd)" -c 'dir c:\'; echo $?

rmyorston commented 1 month ago

The syntax of cmd.exe is not like a Unix shell:

ale5000-git commented 1 month ago

The ; echo $? is outside the cmd.exe. The command for cmd.exe was just dir c:\.

This is the same:

su -W -s "$(which cmd)" -c 'dir c:\'
echo $? # This one is still in busybox
rmyorston commented 1 month ago

Oops. I misread the command.

I've no idea why the exit code is 1.

ale5000-git commented 1 month ago

There is probably an error message but I cannot see it due to window disappearance. If you change the code (on your pc) to use /k instead of /c on cmd.exe you can probably see the error message.

rmyorston commented 1 month ago

Using /k shows the error is "The file name, directory name or volume label syntax is incorrect".

I can make the problem go away using this command:

su -W -s "$(which cmd)" -c 'dir c:\ '

It's something to do with quoting things so the shell, the intermediate process that runs cmd.exe and cmd.exe are all happy.

avih commented 1 month ago

It's something to do with quoting things so the shell

I didn't look closely into it, but the quoting rules for cmd.exe are not the same as other apps. Specifically, if one wants to run foo ARG... where quote_arg would correctly encode the arguments when running foo directly from busybox, then it's not guaranteed to create a correct line which cmd.exe would invoke so that foo sees the original arguments.

That's because cmd.exe interprets env vars (using %), also inside strings, interpret unquoted pipe or redirections, and other factors.

So foo still needs to see what quote_arg produces, but now it needs to be encoded in such a way that it's what cmd.exe will produce.

Building a line for cmd.exe requires a different encoding, and especially because cmd.exe commands interet arguments differently than what CommandLineToArgvW does. I'd say the most compatible method would be to not try to interpret or quote it at all somehow. I.e. just build the line content, either from one user arg, or space-combine the args like eval or echo do, and then use this line with CreateProcess without trying to quote anything in it, in the hope that the user knows how to quote it themselves for cmd.exe.

At the very least quoting args would break the cmd.exe echo, because this echo simply dumps everything till the end of the line, and if that happens to include quotes, then they'll be visible at the output as well. For instance (in busybox sh):

$ cmd /c echo 'foo bar'
"foo bar"

Or this which quote_arg doesn't quote:

$ cmd /c echo 123 \> 456
$ cat 456
123

You simply can't win by trying to be smart with cmd.exe line, so the best way is to be the dumbest possible.

If you're not familiar with this page, it's worth reading https://www.daviddeley.com/autohotkey/parameters/parameters.htm It's fascinating, amazing, and depressing at the same time.

ale5000-git commented 1 month ago

Wasn't there a similar issue in drop that was fixed?

avih commented 1 month ago

You simply can't win by trying to be smart with cmd.exe line, so the best way is to be the dumbest possible.

There might be one way to be smart about it in busybox-w32:

rmyorston commented 1 month ago

This works. It looks weird, but it works:

su -W -s $(which cmd) -c dir root 'c:\'

I have little inclination for adding code to support cmd.exe's peculiarities.

ale5000-git commented 1 month ago

Is it possible to share the parsing code of the common parts between su/drop by delegate them to a normal or inline function? So the commands can be tested on drop that it is much easier since it doesn't need a new window.

Excluding the additionl parameters and the additional uses of su they are almost identical:

su   -s SHELL -c CMD_STRING -- root ARG0 ARG...
drop -s SHELL -c CMD_STRING --      ARG0 ARG...
rmyorston commented 1 month ago

I didn't like the idea of introducing a dependency between su and drop.

Instead I've added a -t (test mode) option to su. This uses the same mechanism to start the new shell, but without elevated privileges so it can run in the same console window.

All other options and arguments are accepted as before, though they may behave slightly differently in the new circumstances.

rmyorston commented 1 month ago

I forgot to upload the prereleases. They're there now, PRE-5449 or above is the one you want.

ale5000-git commented 1 month ago

Thank you very much. The new option is very good and seems to work perfectly and also everything else seems to works as intended.

If possible I would like just two more things before closing this ticket: 1) Mention in the help that -W is implied inside -t, example:

-t        Test mode, no elevation (-W is implied)

2) Add this cmd.exe trick also to drop: https://github.com/rmyorston/busybox-w32/commit/e5244175baa5c94726670a6aeb8645428306011a (cdrop is nice when you can't provide parameters but for other things I think it is nice to have a single drop for all shells)

rmyorston commented 1 month ago

@ale5000-git Both your requests are addressed in the latest prereleases (PRE-5451 or above).

ale5000-git commented 1 month ago

Now it seems everything is working as intended, thanks for all the work.

ale5000-git commented 1 month ago

As a bonus I'll post the functions I use to elevate/drop admin rights and re-execute the initialization script:

sume()
{
  local _fix_pwd

  if test "${PLATFORM:?}" != 'win'; then
    ui_warning 'sume not supported!!!'
    return 1
  fi
  ! is_root || return 0

  if test "${IS_BUSYBOX:?}" = 'true'; then
    # shellcheck disable=SC2016 # Ignore: Expressions don't expand in single quotes
    su -c "${MAIN_DIR:?}"'/cmdline.bat "${@}"' -- root "${0-}" "${@}"
  elif test -n "${BB_CMD?}" && test -n "${SHELL_CMD?}"; then
    _fix_pwd="cd '${PWD:?}'"
    # shellcheck disable=SC2016 # Ignore: Expressions don't expand in single quotes
    "${BB_CMD:?}" su -s "${SHELL_CMD:?}" -c "${_fix_pwd:?}; ${MAIN_DIR:?}"'/cmdline.sh "${@}"' -- root "${0-}" "${@}"
  else
    ui_warning 'sume failed!!!'
    return 125
  fi
}

dropme()
{
  if test "${PLATFORM:?}" != 'win'; then
    ui_warning 'dropme not supported!!!'
    return 1
  fi
  is_root || return 0

  if test "${IS_BUSYBOX:?}" = 'true'; then
    # shellcheck disable=SC2016 # Ignore: Expressions don't expand in single quotes
    drop -c "${MAIN_DIR:?}"'/cmdline.sh "${@}"' -- "${0-}" "${@}"
  elif test -n "${BB_CMD?}" && test -n "${SHELL_CMD?}"; then
    # shellcheck disable=SC2016 # Ignore: Expressions don't expand in single quotes
    "${BB_CMD:?}" drop -s "${SHELL_CMD:?}" -c "${MAIN_DIR:?}"'/cmdline.sh "${@}"' -- "${0-}" "${@}"
  else
    ui_warning 'dropme failed!!!'
    return 125
  fi
}
ale5000-git commented 1 month ago

@rmyorston While experimenting I have just typed a wrong command: su -t -s "$(cmd.exe)" and I wonder why prompt disappears and ctrl+c do nothing.

rmyorston commented 1 month ago

The command is waiting for cmd.exe to finish so it can return its output. When it's in that state ps from a different console shows:

 2368   424 rmy       0:00  4:56   sh -l
 2668  2368 rmy       0:00  0:07   sh --fs 00000188
 1516  2668 rmy       0:00  0:07   cmd.exe

Killing process 1516 results in this in the hung console:

~ $ su -t -s "$(cmd.exe)"
su: Microsoft Windows [Version 6.3.9600]
(c) 2013 Microsoft Corporation. All rights reserved.

C:\Users\rmy>: Not found
~ $
ale5000-git commented 1 month ago

@rmyorston Is it supposed to be that ctrl-c do nothing?

rmyorston commented 1 month ago

Is it supposed to be that ctrl-c do nothing?

It would be good if ctrl-c worked, but in this case it doesn't.

The problem is with the command substitution, $(cmd.exe). It isn't specific to su.

When a shell command includes a command substitution the parent shell waits in a non-interruptible state for it to finish. If the user presses ctrl-c it's the child process that receives it. So:

~ $ echo $(sleep 100)
^C
~ $

The sleep was interrupted, not the parent shell. Now:

~ $ echo $(sh)

~ $

The output of sh is being intercepted for use by the parent so, recalling #442, it's a non-interactive shell and it can be interrupted. (I don't know where the '^C' went.)

Also from #442 we know that we get different behaviour if we force the shell to be interactive. Here I use a different prompt to distinguish between the shells:

~ $ echo $(PS1='# ' sh -i)
# echo yo
# exit
yo
~ $

I can interact with the child shell, and when I exit from it the parent echoes its output.

Suppose I try interrupting the child shell by pressing 'ctrl-c' three times:

~ $ echo $(PS1='# ' sh -i)
#
#
#
# exit
^C^C^C
~ $

The interrupts are received by the child shell, but it ignores them. I had to exit from the shell by hand.

Now let's try cmd.exe:

~ $ echo $(cmd)
echo yo
exit
Microsoft Windows [Version 6.3.9600] (c) 2013 Microsoft Corporation. All rights reserved. C:\Users\rmy>yo C:\Users\rmy>
~ $

Again, I can interact with the child process. When I exit from it the parent echoes its output (including the copyright message and prompts, which evidently are written to stdout).

Now try interrupting cmd.exe:

~ $ echo $(cmd)
exit
Microsoft Windows [Version 6.3.9600] (c) 2013 Microsoft Corporation. All rights reserved. C:\Users\rmy> C:\Users\rmy> C:\Users\rmy> C:\Users\rmy>
~ $

Again, it doesn't work and I have to exit by hand.

Similar thing with upstream BusyBox:

$ busybox sh
~ $ echo $(sh -i)
sh-5.2$ echo yo
sh-5.2$ ^C
sh-5.2$ ^C
sh-5.2$ ^C
sh-5.2$ exit
exit
yo
~ $

In summary: an interactive process that doesn't respond to ctrl-c by exiting can't be interrupted in a command substitution. You either need to interact with it to shut it down or kill it.