Ticket #3689 (closed enhancement: duplicate)

Opened 3 years ago

Last modified 3 years ago

mc doesn't recognize shell (e.g., /bin/sh) as bash when it is a link to bash binary

Reported by: alllexx88 Owned by:
Priority: major Milestone:
Component: mc-core Version: master
Keywords: shell bash Cc: egmont
Blocked By: Blocking:
Branch state: no branch Votes for changeset:

Description

Currently mc identifies shell as bash only if this statement is true (in lib/shell.c):

(strstr (mc_shell->path, "/bash") != NULL
getenv ("BASH") != NULL)

However, this won't work if shell, e.g., is /bin/sh pointing to bash. Let's see why:

  1. (strstr (mc_shell->path, "/bash") != NULL) is obviously false, since shell path doesn't end with "/bash"
  1. (getenv ("BASH") != NULL) is false too, unless BASH env variable was somewhere intentionally set, since BASH is actually internal bash variable -- not environmental variable, and so by default getenv ("BASH") is NULL

Here's a log that illustrates the second point:

root@DiskStation:/opt# cat test.c
#include <stdio.h>      /* printf */
#include <stdlib.h>     /* getenv */

int main ()
{
  char* bash;
  bash = getenv ("BASH");
  if (bash!=NULL)
    printf ("$BASH=%s\n",bash);
  return 0;
}
root@DiskStation:/opt# gcc test.c -o test
root@DiskStation:/opt# echo $BASH
/bin/ash
root@DiskStation:/opt# env
SHELL=/bin/ash
TERM=xterm
SSH_CLIENT=192.168.133.169 63110 22
SSH_TTY=/dev/pts/5
LC_ALL=en_US.utf8
USER=root
PAGER=more
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin:/opt/bin:/opt/sbin
MAIL=/var/mail/root
PWD=/opt
EDITOR=/opt/bin/nano
LANG=en_US.utf8
PS1=\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$
HOME=/root
SHLVL=2
TERMINFO=/usr/share/terminfo
LOGNAME=root
SSH_CONNECTION=192.168.133.169 63110 192.168.133.15 22
PGDATA=/var/services/pgsql
_=/bin/env
root@DiskStation:/opt# ./test
root@DiskStation:/opt# export BASH=$BASH
root@DiskStation:/opt# ./test
$BASH=/bin/ash
root@DiskStation:/opt# /bin/ash --version
GNU bash, version 4.3.39(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

This is a log from Synology DSM 6.0. It uses /bin/ash as login shell for root user, and /bin/sh -- for admin user. Both /bin/ash and /bin/sh are links to bash. Starting mc as root leads to identifying shell as busybox ash, and as admin -- leads to 'common.c: unimplemented subshell type 1' error and no subshell as the result.

The suggested fix (attaching patch next) is to test whether mc_shell->real_path ends with "/bash". Tested to work fine on Synology DSM 6.0.

Attachments

3689-bash-subshell.patch (785 bytes) - added by alllexx88 3 years ago.
3689-bash-subshell-V2.patch (2.7 KB) - added by alllexx88 3 years ago.
3689-bash-subshell-V3.patch (2.8 KB) - added by alllexx88 3 years ago.

Change History

Changed 3 years ago by alllexx88

comment:1 Changed 3 years ago by alllexx88

  • Keywords shell bash added

comment:2 Changed 3 years ago by alllexx88

There's a better approach to test if our shell is bash: to test whether it has BASH variable set (environmental OR internal -- vs. the getenv("BASH") test we currently have in master). The idea is to use [ -z "${BASH+x}" ] test as suggested here: http://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash. The patch below runs

execl(mc_shell->path, mc_shell->path, "-c", "([ -z \"${BASH+x}\" ] && exit 1) || exit 0", (char *) NULL)

in a child fork and has parent evaluate its exit code with waitpid(). I have tested this on different corner cases, like execl() failing, or program passed as SHELL not accepting the syntax. In theory, if SHELL is set as something intentionally malicious, execl() here can do bad things, but we will still do them, if shell is recognized as supported (and that's easy to achieve with a symlink), when we try to initialize it afterwards.

Will be thankful for your feedback

Last edited 3 years ago by alllexx88 (previous) (diff)

Changed 3 years ago by alllexx88

comment:3 follow-up: ↓ 5 Changed 3 years ago by egmont

bash behaves differently (in some sh-compatible way) if invoked as sh. Are you sure it's okay to recognize it as bash then? Wouldn't it result in mc expecting certain things that it deliberate refuses to do?

comment:4 Changed 3 years ago by egmont

  • Cc egmont added

comment:5 in reply to: ↑ 3 Changed 3 years ago by alllexx88

Replying to egmont:

bash behaves differently (in some sh-compatible way) if invoked as sh. Are you sure it's okay to recognize it as bash then? Wouldn't it result in mc expecting certain things that it deliberate refuses to do?

That's a good point. To answer this, I used grep:

$ grep SHELL_BASH `find -type f -name '*.[ch]'`
./src/subshell/common.c:    case SHELL_BASH:
./src/subshell/common.c:    case SHELL_BASH:
./src/subshell/common.c:    case SHELL_BASH:
./lib/shell.h:    SHELL_BASH,
./lib/shell.c:        mc_shell->type = SHELL_BASH;

lib/shell.h defines shell types enum, lib/shell.c tries to guess the type. So, we should only care about src/subshell/common.c here. After looking at it, it's clear that the only two things unique to SHELL_BASH are:

    case SHELL_BASH:
        g_snprintf (precmd, buff_size,
                    " PROMPT_COMMAND=${PROMPT_COMMAND:+$PROMPT_COMMAND\n}'pwd>&%d;kill -STOP $$'\n"
                    "PS1='\\u@\\h:\\w\\$ '\n", subshell_pipe[WRITE]);
        break;

in function init_subshell_precmd

and

    case SHELL_BASH:
        /* Do we have a custom init file ~/.local/share/mc/bashrc? */
        init_file = mc_config_get_full_path ("bashrc");

        /* Otherwise use ~/.bashrc */
        if (!exist_file (init_file))
        {
            g_free (init_file);
            init_file = g_strdup (".bashrc");
        }

        /* Make MC's special commands not show up in bash's history and also suppress
         * consecutive identical commands*/
        putenv ((char *) "HISTCONTROL=ignoreboth");

        /* Allow alternative readline settings for MC */
        {
            char *input_file;

            input_file = mc_config_get_full_path ("inputrc");
            if (exist_file (input_file))
            {
                putenv_str = g_strconcat ("INPUTRC=", input_file, (char *) NULL);
                putenv (putenv_str);
            }
            g_free (input_file);
        }

        break;

...

    case SHELL_BASH:
        execl (mc_global.shell->path, mc_global.shell->name, "-rcfile", init_file, (char *) NULL);
        break;

in init_subshell_child (BTW, init_file = g_strdup (".bashrc") should be init_file = g_strdup_printf ("%s/.bashrc", g_getenv ("HOME")), I think: see https://www.midnight-commander.org/ticket/3684#comment:10)

So, it only affects subshell initialization, which apparently succeeds: I tested it with SHELL=/bin/sh, where sh is linked to bash. Beyond that, no assumptions are made. I should note that bash invoked as sh doesn't load ~/.bashrc by default (nor ~/.profile, /etc/profile or anything else -- at least, according to strace), but we would rather have these defaults loaded if present, wouldn't we?

Thank you for your reply

comment:6 follow-up: ↓ 7 Changed 3 years ago by egmont

Sounds good, thanks for checking this! :)

comment:7 in reply to: ↑ 6 Changed 3 years ago by alllexx88

Replying to egmont:

Sounds good, thanks for checking this! :)

No problem, it was a very valid point :)

Another thing I came to think of: bash must have BASH *internal* variable set, however, the test function will report it set if there is BASH environmental variable set too, even when there's no such internal variable, thus falsely assuming it bash. Maybe we should do unsetenv() in the child fork to make sure this doesn't happen? What do you think? I have to go to sleep now, will try and test it when I have time tomorrow.

Thanks

comment:8 Changed 3 years ago by alllexx88

Here's V3 of the patch. It's the same as V2, except that we make sure we test for internal variable by unsetting environmental variable of the same name in the child fork first. I think, this is the best test for bash we can use: shell is bash if and only if it has BASH internal variable. Actually, this test function can be used to identify other shells too, like testing for ZSH_NAME internal variable to see if we have zsh. This approach is more robust than testing for binary names, and should be applicable to most of the supported shells.

What do you think about this?

Thanks

Changed 3 years ago by alllexx88

comment:9 Changed 3 years ago by alllexx88

@egmont There's a small bug in the V2 and V3 patches. However, I went further than just fixing them. I changed shell detection mechanism to rely rather on specific variables that are unique to respective shell, rather than testing paths. It's possible to do this safe enough for bash, zsh, fish, tcsh and ordinary csh. With bsd-csh it's a bit more complicated: I test for shell variable (lower register -- that's important), and whether it errors on undefined variable without set -u -- and if both conditions are true, this is bsd-csh. I don't think we can have a better test for it. Only dash and busybox ash have no special variables, and so are still detected using paths. I've been testing this patch recently, and so far it seems to be robust. I'll be testing it for a day or two more, and then open a new ticket, since this change goes beyond this one. Should I close this one for the time being?

Thanks,
Alex

comment:10 Changed 3 years ago by alllexx88

  • Blocked By 3692 added

comment:11 Changed 3 years ago by alllexx88

I haven't figured out how to close this ticket, and it looks like I can't (while it's new?). Please discard this ticket in favour of this one: https://www.midnight-commander.org/ticket/3692

comment:12 Changed 3 years ago by andrew_b

  • Status changed from new to closed
  • Resolution set to duplicate
  • Milestone Future Releases deleted

Closed as duplicate of #3658.

comment:13 Changed 3 years ago by andrew_b

  • Blocked By 3692 removed
Note: See TracTickets for help on using tickets.