7

I am aware that aliases can be bypassed by quoting the command itself.

However, it seems that if builtin commands are "shadowed" by functions with the same names, there is no way to execute the underlying builtin command except...by using a builtin command. If you can get to it.

To quote the bash man page (at LESS='+/^COMMAND EXECUTION' man bash):

COMMAND EXECUTION
       After a command has been split into words, if it results  in  a  simple
       command  and  an  optional list of arguments, the following actions are
       taken.

       If the command name contains no slashes, the shell attempts  to  locate
       it.   If  there  exists a shell function by that name, that function is
       invoked as described above in FUNCTIONS.  If the name does not match  a
       function,  the shell searches for it in the list of shell builtins.  If
       a match is found, that builtin is invoked.

So, is it possible to recover from the following, without starting a new shell?

unset() { printf 'Haha, nice try!\n%s\n' "$*";}
builtin() { printf 'Haha, nice try!\n%s\n' "$*";}
command() { printf 'Haha, nice try!\n%s\n' "$*";}

I didn't even add readonly -f unset builtin command. If it is possible to recover from the above, consider this a bonus question: can you still recover if all three functions are marked readonly?


I came up with this question in Bash, but I'm interested in its applicability to other shells as well.

5
  • 2
    set -o posix, then you have unset is builtin instead of your fucntion, then unset builtin unset command.
    – cuonglm
    Commented Nov 10, 2017 at 7:06
  • 1
    @cuonglm, that looks like an answer.
    – Wildcard
    Commented Nov 10, 2017 at 7:20
  • 1
    @cuonglm I do hope you post an answer about that, not just so I can upvote it but also because, although I've done my very best to write in such a way as to avoid giving this impression, I fear that--as long as mine is the only answer--a few less careful readers might think the method I've shown is necessary even when readonly is not used. My solution is a hackish curiosity; yours is a real solution that might be used even for reasons other than fun, and that transmits important knowledge about the effects of POSIX mode in Bash. Commented Nov 10, 2017 at 8:06
  • 2
    In bash, you can also do: enable -n builtin unset command enable Commented Nov 10, 2017 at 8:32
  • @StéphaneChazelas, that looks like another way to "hose" any possibility to access builtins. (At a glance I thought it was a way to recover them.) Interesting.
    – Wildcard
    Commented Nov 10, 2017 at 8:35

2 Answers 2

6

can you still recover if all three functions are marked readonly?

Yes, usually you can, though that does not mean you should.

Just as you can unset readonly variables by attaching a debugger and calling unbind_variable as shown in anishsane's answer to that question, you can also unset readonly functions passing their names to unbind_func using a debugger.

This is not a reasonable approach when they aren't readonly (if it indeed ever is). In that situation you should use cuonglm's solution, which takes advantage of how unset is treated in POSIX mode. That solution is something you might actually use in real life.

Since there's no actual guarantee that your shell will behave reasonably after you circumvent readonly with a debugger, I suggest avoiding it whenever a more reasonable alternative, like quitting and restarting your shell or replacing your shell with a new one using exec, is available.

With that said, here's anishsane's method adapted to unset functions instead of a variable:

cat <<EOF | sudo gdb
attach $$
call unbind_func("unset")
call unbind_func("builtin")
call unbind_func("command")
detach
EOF

Note that $$ is expanded into the shell's process ID, because no part of EOF in <<EOF is quoted.

I tested this on Bash 4.3.48(1)-release on Ubuntu 16.04 LTS, and it worked. You need gdb for this, though it could be adapted to other debuggers. As anishsane commented, piping from cat is intended to avoid a deadlock where the process that gives input to gdb is the one that gdb has stopped. I believe it achieves that goal, because in a pipeline of two or more commands, Bash runs each command in a subshell. But I am unsure if it is the most robust way. Ultimately, however, there's no actual guarantee that this works anyway, since it's entirely reasonable for Bash to assume readonly variables and functions won't change. In practice, my guess is that this does virtually always work.

To use this technique as written, you need sudo installed and you need to be able to sudo to root. You can, of course, replace it with another privilege-elevation method. Depending on what OS you are running and how it is configured, you might be able to omit sudo altogether and run gdb as yourself instead of root. For example, the Linux kernel will consult the value of /proc/sys/kernel/yama/ptrace_scope, which you can set through sysctl and may read or (as root) write, to determine what processes may debug other processes. If the value is 1, then only a process's direct parent--or any process running as root--may debug it. Most recent GNU/Linux systems have it set to 1, which is why I included sudo.

That description of Linux kernel behavior is somewhat oversimplified, in that other ptrace_scope values are allowed and in that the relationship required by 1 can be adjusted. See the relevant documentation for full details.

3
  • Using gdb with only 1 attach directive could be passed as gdb argiumnent: --pid. See my destroy function. (at same SO post you pointed out) Commented Aug 29, 2023 at 8:13
  • Avoid useless cat !!! Use: sudo gdb <<EOF ... Commented Aug 29, 2023 at 8:14
  • That's already directly addressed in my answer: "As anishsane commented, piping from cat is intended to avoid a deadlock where the process that gives input to gdb is the one that gdb has stopped." (I think you may have seen that comment. But even if you've found the command you recommend to sometimes work, it is not obvious to me that it works as often. If you believe you have a solid analysis that it does, then I think that may make for a whole new answer, which could be of value.) Commented Aug 29, 2023 at 17:50
5

When bash is in posix mode, some builtins are considered special, which is compliant with POSIX standard.

One special thing about those special builtins, they are found before function in command lookup process. Taking this advantage, you can try:

$ unset builtin
Haha, nice try!
builtin
$ set -o posix
$ unset builtin
$ builtin command -v echo
echo

though it does not work if set is overridden by a function named set:

$ set() { printf 'Haha, nice try!\n%s\n' "$*";}
$ set -o posix
Haha, nice try!

In this case, you just have to set POSIXLY_CORRECT to make bash enter posix mode, then you have all special builtins:

$ POSIXLY_CORRECT=1
2
  • 1
    ... you don't even need the export for that (also handling the case where export is masked)
    – muru
    Commented Nov 11, 2017 at 3:58
  • @muru yes, copy-paste issue, fixed.
    – cuonglm
    Commented Nov 11, 2017 at 6:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .