-
Notifications
You must be signed in to change notification settings - Fork 15
Suppress setuid and setgid on shebang scripts #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| #include <unistd.h> | ||
| #include <fcntl.h> | ||
| #include <errno.h> | ||
| #include <sys/stat.h> | ||
| #include <libkern/OSCacheControl.h> | ||
|
|
||
| #include "debug/log.h" | ||
|
|
@@ -421,6 +422,14 @@ int64_t sys_execve(hv_vcpu_t vcpu, | |
| elf_info_t elf_info; | ||
| int shebang_depth = 0; | ||
|
|
||
| /* Snapshot the directly-executed file's metadata before shebang | ||
| * resolution overwrites path_host with the interpreter path. The | ||
| * setuid/setgid check below needs the *original* file's mode bits. | ||
| */ | ||
| bool exec_is_script = false; | ||
| struct stat exec_st; | ||
| bool have_exec_st = (stat(path_host, &exec_st) == 0); | ||
|
|
||
| while (true) { | ||
| char interp_start[256]; | ||
| char interp_arg[256]; | ||
|
|
@@ -434,6 +443,9 @@ int64_t sys_execve(hv_vcpu_t vcpu, | |
| if (rc == 0) | ||
| break; | ||
|
|
||
| /* The file at the current path is a shebang script. */ | ||
| exec_is_script = true; | ||
|
|
||
| /* The current path is a script. Bound the resolution chain only once a | ||
| * further shebang is confirmed, so a max-depth chain ending in a real | ||
| * ELF still loads (matches the prior elf_load-first loop). | ||
|
|
@@ -551,6 +563,31 @@ int64_t sys_execve(hv_vcpu_t vcpu, | |
| goto fail; | ||
| } | ||
|
|
||
| /* Apply setuid/setgid from the directly-executed file, matching Linux | ||
| * kernel behaviour (fs/exec.c bprm_fill_uid). Scripts are | ||
| * deliberately excluded: the kernel ignores setuid/setgid on shebang | ||
| * scripts to prevent privilege escalation via interpreter manipulation. | ||
| * S_ISGID is only effective when the group-execute bit is also set, | ||
| * matching the kernel's mandatory-locking vs setgid distinction. | ||
| */ | ||
| if (have_exec_st && !exec_is_script && S_ISREG(exec_st.st_mode)) { | ||
| uint32_t new_euid = proc_get_euid(); | ||
| uint32_t new_egid = proc_get_egid(); | ||
| bool cred_changed = false; | ||
| if (exec_st.st_mode & S_ISUID) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: A readable but non-executable setuid ELF is accepted by the loader and receives the file owner's effective UID. Require Linux execute permission for the target and interpreter before applying set-id credentials or loading the image. Prompt for AI agents |
||
| new_euid = (uint32_t) exec_st.st_uid; | ||
| cred_changed = true; | ||
| } | ||
| if ((exec_st.st_mode & S_ISGID) && (exec_st.st_mode & S_IXGRP)) { | ||
| new_egid = (uint32_t) exec_st.st_gid; | ||
| cred_changed = true; | ||
| } | ||
| if (cred_changed) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: A normal exec after dropping set-id privilege retains the previous saved ID, letting the replacement program regain it with Prompt for AI agents |
||
| proc_set_ids(proc_get_uid(), new_euid, new_euid, proc_get_gid(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P0: A rejected set-id exec can return to the old program with elevated credentials, e.g. when its PT_INTERP is invalid or Rosetta is disabled. Commit credentials only after all recoverable validation succeeds, immediately before the point of no return. Prompt for AI agents |
||
| new_egid, new_egid); | ||
| } | ||
| } | ||
|
|
||
| /* Pre-PNR validation. All checks that can fail gracefully MUST happen | ||
| * before guest_reset(). After guest_reset(), the old process image is gone. | ||
| * Failures are unrecoverable, matching the Linux kernel's behavior | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P0: Replacing a set-id target between this
stat()and the later pathname opens can run attacker-selected ELF bytes with the replaced file's effective IDs. Bind the executable to an fd, derive metadata withfstat, and load/map that same fd.Prompt for AI agents