Script with env shebang hangs on Linux

Viewed 266

We have a script that looks something like this:

#!/usr/bin/env node --unhandled-rejections=strict
console.log("Hi!");

This has been working fine on my laptop (OSX) but when we run it on Linux it just hangs. Using strace we can see it's continuously loading libc and performing

execve("./foo.sh", ["./foo.sh"], ["YARN_VERSION=1.22.4", "HOSTNAME=307d861c7c1a", "PWD=/", "HOME=/root", "NODE_VERSION=12.18.1", "TERM=xterm", "SHLVL=1", "PATH=/usr/local/sbin:/usr/local/"..., "_=/usr/bin/strace", "node --unhandled-rejections=stri"...]) = 0

We can see here that the argument to env is being interpreted as an environment variable. We also know that "node --unhandled-rejections=strict" is passed in as a single argument.

We can see the difference in OSX by creating two scripts. b1.sh:

args=("$@")
echo \"${args[0]}\" \"${args[1]}\" \"${args[2]}\"

and b2.sh

#!/usr/bin/env /tmp/b1.sh foo=bar

When we run b2.sh on OSX we get

"foo=bar" "./b2.sh" ""

on Linux it also hangs.

So it's apparent that arguments are passed to env differently in Linux vs. OSX. In OSX "/tmp/b1.sh" and "foo=bar" are seperate arguments. Where as in Linux they are passed as the same argument.

But why does this cause env to exec the same code over and over again?

3 Answers

You can use -S of env to split node --unhandled-rejections=strict :

~/tmp$ uname -a
Linux ubuntu 5.4.0-37-generic #41-Ubuntu SMP Wed Jun 3 18:57:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
~/tmp$ cat ./test.sh
#!/usr/bin/env -S node --unhandled-rejections=strict
console.log("Hi!");
~/tmp$ ./test.sh
Hi!
~/tmp$

Now an explanation of what happened in your post.

When you run ./foo.sh, it's equivalent to run following command :

/usr/bin/env "node --unhandled-rejections=strict" ./foo.sh

Here is an extract from man env :

env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]

So env considers "node --unhandled-rejections=strict" as NAME=VALUE due to the equal sign (=) in "node --unhandled-rejections=strict"

Then env tried to run ./foo.sh as a COMMAND which now enters into an endless loop.

I believe the answer to "why does this script hang on Linux" is because the file becomes self referential.

When execve executes b2.sh it calls env with 2 arguments /tmp/b1.sh foo=bar and ./b2.sh Because the first argument is interpreted as an environment variable to be passed to the executed process (due to the equals sign), then the second argument is considered to be the executable to call.

This makes the Shebang self referential, calling itself infinitely.

As Philippe mentioned you can pass the -S option to env to force it to split the arguments by spaces and avoid this behaviour.

Read the documentation of exec(3) and execve(2) and syscalls(2) and env(1) and strace(1); see also credentials(7) and path_resolution(7).

The hash-bang line can only contain one line with a single space. But recent kernels (e.g. Linux 5.6 from Debian/Sid) may be more flexible.

You may want to start your script with

#!/usr/bin/node --unhandled-rejections=strict

Read also Advanced Linux Programming and syscalls(2)

"why does it hang looping over and over calling execve"

The Linux kernel is open source (actually free software, GPLv2 licensed).

You can download its source code and study it, and/or ask on kernelnewbies.org, and you could even patch your kernel source code to adapt it to your needs.

My opinion is that patching your kernel source code won't be a good idea. A better approach could be to write your own C program calling execve like you need, and compile your C code with e.g. GCC (perhaps enhanced by your GCC plugin; read then also this draft report). Be sure to read about GCC invocation.

When we run b2.sh on OSX we get

But Linux is not OSX. You may want to learn more about POSIX specifications and read some of them (and look at these slides)

You might write in C your own Linux kernel module providing a variant of execve system call, but I don't recommend doing so. Prefer coding in userland, not in kernel land. Your C program and user-space executable might use dlopen(3) and dlsym(3) to load plugins.

Related