This is not exactly a question about how to run a hash bang script.

In this situation, a hash bang script exists which doesn't (necessarily) have the u+s permission. Rather, the interpreter has u+s, and the script designates that interpreter: #!/path/to/interpreter.

On Mac OS, in this situation, the designated interpreter itself isn't granted the setuid implied by its own permission bits, as it would be if it were directly executed. Its real and effective user ID is just that of the user who ran the hash bang script.

Is there a workaround for this not involving the interpreter re-executing itself?

Re-executing works:

Kazs-Mac-Pro:txr kaz$ uname -a
Darwin Kazs-Mac-Pro.local 11.0.0 Darwin Kernel Version 11.0.0: Fri Apr  8 20:29:42 PDT 2011; root:xnu-1699.22.36~1/RELEASE_X86_64 x86_64
Kazs-Mac-Pro:txr kaz$ cat setuid.tl
#!./txr --reexec
(put-line `gids: @(getegid) @(getgid)`)
(put-line `uids: @(geteuid) @(getuid)`)
(put-line `groups: @(getgroups)`)
(seteuid 0) ;; throws if unable
Kazs-Mac-Pro:txr kaz$ ls -l txr setuid.tl
-rwsr-xr-x  1 root  wheel      163  5 May 15:18 setuid.tl
-rwsr-xr-x  1 root  wheel  1334500  5 May 15:17 txr
Kazs-Mac-Pro:txr kaz$ ./setuid.tl 
gids: 20 20
uids: 0 501
groups: 20 402 401 12 33 61 79 80 81 98 100 204

All that --reexec does is call execvp on the program's path name, and the remaining arguments after --reexec. Poof, setuid privs are back. This is ugly, though.

If we do not have --rexec, this is the behavior:

Kazs-Mac-Pro:txr kaz$ ./setuid.tl 
gids: 20 20
uids: 501 501
groups: 20 402 401 12 33 61 79 80 81 98 100 204
./txr: unhandled exception of type system-error:
./txr: seteuid failed: 1/"Operation not permitted"
./txr: during evaluation at ./setuid.tl:4 of form (seteuid 0)

(The u+s on the script is irrelevant from the system perspective; the interpreter uses that, and the ownership of the script, to decide whether to run the script setuid, or whether to permanently drop privileges and then run it. Of course, we wouldn't want to do this sort of thing with an interpreter that isn't aware of setuid operation and blindly confers its elevated privilege onto any piece of code that it is asked to execute.)

1 Answer 1


Most Unix variants disable setuid on scripts for security reasons. For more information, see Allow setuid on shell scripts

Earlier versions of OS X had a setting to allow setuid scripts: sysctl kernl.sugid_scripts=1, but I don't see it documented on 10.9. I don't know if it still exists but isn't documented, and if it still exists I don't know if it's secure.

The usual way to run a setuid script is through sudo, which takes care of some of the security problems with setuid scripts, in particular sanitizing the environment. Add a sudo rule (run visudo to edit the sudo configuration):

ALL ALL = (target_user : target_group) /path/to/script

This allows anyone to run sudo -u target_user -g target_group /path/to/script … (with any arguments). Replace the first ALL by %original_group to allow only the members of origininal_group to do this.

If you want this to be transparent, write a wrapper script that invokes sudo as desired:

exec sudo -u target_user -g target_group /path/to/script "$@"
  • I added --rexec option to the interpreter (described in updated question). I was wondering whether there is an efficient way not involving intermediary process images. Like some switch we can just flip somewhere. The script is a bit of a red herring here because it's just an argument to the interpreter. The interpreter (a binary written in C) isn't being run with setuid (regardless of the permissions on the script).
    – Kaz
    May 6, 2016 at 4:21
  • I will look into that sugid_scripts setting; maybe it influences this behavior too. Or perhaps at least renders it irrelevant, in any case.
    – Kaz
    May 6, 2016 at 4:29
  • @Kaz kern.sugid_scripts is the switch to flip, if it's still supported and it's secure. Many Unix variants don't have such a switch, for example Linux doesn't. The script is not an argument of the interpreter if you use shebang, that's only one possible implementation; for setuid scripts to work, the script must not be passed as an argument to the interpreter (/dev/fd/3 is used instead, see the question I link). There's nothing wrong with using an intermediate process image, that's the Unix way of doing things. May 6, 2016 at 10:20
  • I've noticed that Solaris passes /dev/fd/3 if the script is setuid. If not, the name is passed. Linux doesn't support setuid scripts, but if an interpreter is setuid, it is honored through hash ban execution. The extra process image makes numerous extra system calls related to shared lib attachment, C library startup and memory management.
    – Kaz
    May 6, 2016 at 13:45
  • For all the security theatre, OSX allows a rwsrwxrwx binary to be setuid!
    – Kaz
    May 6, 2016 at 13:45

You must log in to answer this question.

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