diff --git a/src/syscall/exec.c b/src/syscall/exec.c index d694ba7..ddce726 100644 --- a/src/syscall/exec.c +++ b/src/syscall/exec.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #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) { + 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) { + proc_set_ids(proc_get_uid(), new_euid, new_euid, proc_get_gid(), + 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