diff --git a/src/core/elf.c b/src/core/elf.c index f412ca8..0426b98 100644 --- a/src/core/elf.c +++ b/src/core/elf.c @@ -21,42 +21,32 @@ #include "debug/log.h" #include "utils.h" -int elf_load(const char *path, elf_info_t *info) +int elf_load_fd(int fd, const char *display_path, elf_info_t *info) { memset(info, 0, sizeof(*info)); - FILE *f = fopen(path, "rb"); - if (!f) { - perror(path); - return -1; - } - elf64_ehdr_t ehdr; - if (fread(&ehdr, sizeof(ehdr), 1, f) != 1) { - log_error("%s: failed to read ELF header", path); - fclose(f); + if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr)) { + log_error("%s: failed to read ELF header", display_path); return -1; } /* Reject non-ELF inputs before interpreting the rest of the header. */ if (ehdr.e_ident[0] != ELFMAG0 || ehdr.e_ident[1] != ELFMAG1 || ehdr.e_ident[2] != ELFMAG2 || ehdr.e_ident[3] != ELFMAG3) { - log_error("%s: not an ELF file", path); - fclose(f); + log_error("%s: not an ELF file", display_path); return -1; } /* elfuse only implements the 64-bit Linux ABI. */ if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) { - log_error("%s: not a 64-bit ELF", path); - fclose(f); + log_error("%s: not a 64-bit ELF", display_path); return -1; } /* aarch64-linux user binaries are little-endian in the supported mode. */ if (ehdr.e_ident[EI_DATA] != ELFDATA2LSB) { - log_error("%s: not little-endian", path); - fclose(f); + log_error("%s: not little-endian", display_path); return -1; } @@ -64,9 +54,8 @@ int elf_load(const char *path, elf_info_t *info) * diagnostic instead of a generic parse failure. */ if (ehdr.e_machine != EM_AARCH64 && ehdr.e_machine != EM_X86_64) { - log_error("%s: unsupported architecture (e_machine=%u)", path, + log_error("%s: unsupported architecture (e_machine=%u)", display_path, ehdr.e_machine); - fclose(f); return -1; } @@ -74,8 +63,8 @@ int elf_load(const char *path, elf_info_t *info) * the load base that keeps them away from elfuse's reserved regions. */ if (ehdr.e_type != ET_EXEC && ehdr.e_type != ET_DYN) { - log_error("%s: not an executable (e_type=%u)", path, ehdr.e_type); - fclose(f); + log_error("%s: not an executable (e_type=%u)", display_path, + ehdr.e_type); return -1; } @@ -89,23 +78,20 @@ int elf_load(const char *path, elf_info_t *info) /* Program headers drive both memory mappings and auxv AT_PHDR. */ if (ehdr.e_phnum == 0) { - log_error("%s: no program headers", path); - fclose(f); + log_error("%s: no program headers", display_path); return -1; } if (ehdr.e_phentsize < sizeof(elf64_phdr_t)) { - log_error("%s: e_phentsize too small (%u < %zu)", path, + log_error("%s: e_phentsize too small (%u < %zu)", display_path, ehdr.e_phentsize, sizeof(elf64_phdr_t)); - fclose(f); return -1; } /* Linux kernel caps program headers at 64KiB. Reject pathological inputs * before allocating to avoid attacker-controlled large allocations. */ if ((size_t) ehdr.e_phnum * ehdr.e_phentsize > 65536) { - log_error("%s: program header table too large (%u * %u)", path, + log_error("%s: program header table too large (%u * %u)", display_path, ehdr.e_phnum, ehdr.e_phentsize); - fclose(f); return -1; } @@ -113,15 +99,12 @@ int elf_load(const char *path, elf_info_t *info) uint8_t *ph_buf = malloc(ph_total); if (!ph_buf) { perror("malloc"); - fclose(f); return -1; } - if (fseek(f, (long) ehdr.e_phoff, SEEK_SET) != 0 || - fread(ph_buf, ph_total, 1, f) != 1) { - log_error("%s: failed to read program headers", path); + if (pread(fd, ph_buf, ph_total, ehdr.e_phoff) != (ssize_t) ph_total) { + log_error("%s: failed to read program headers", display_path); free(ph_buf); - fclose(f); return -1; } @@ -137,34 +120,29 @@ int elf_load(const char *path, elf_info_t *info) if (ph->p_type == PT_INTERP) { size_t interp_len = ph->p_filesz; if (interp_len >= sizeof(info->interp_path)) { - log_error("%s: PT_INTERP path too long (%zu >= %zu)", path, - interp_len, sizeof(info->interp_path)); + log_error("%s: PT_INTERP path too long (%zu >= %zu)", + display_path, interp_len, sizeof(info->interp_path)); free(ph_buf); - fclose(f); return -1; } if (interp_len > 0) { - long saved_pos = ftell(f); - if (fseek(f, (long) ph->p_offset, SEEK_SET) == 0) { - size_t n = fread(info->interp_path, 1, interp_len, f); - /* interp_len includes the NUL from the ELF file. On short - * read, clear the path (unusable). On full read, - * force-terminate as insurance. - */ - if (n < interp_len) - info->interp_path[0] = '\0'; - else - info->interp_path[interp_len - 1] = '\0'; - } - fseek(f, saved_pos, SEEK_SET); + ssize_t n = + pread(fd, info->interp_path, interp_len, ph->p_offset); + /* interp_len includes the NUL from the ELF file. On short + * read, clear the path (unusable). On full read, + * force-terminate as insurance. + */ + if (n < (ssize_t) interp_len) + info->interp_path[0] = '\0'; + else + info->interp_path[interp_len - 1] = '\0'; } } if (ph->p_type == PT_LOAD) { if (seg_count >= ELF_MAX_SEGMENTS) { - log_error("%s: too many PT_LOAD segments", path); + log_error("%s: too many PT_LOAD segments", display_path); free(ph_buf); - fclose(f); return -1; } @@ -189,9 +167,8 @@ int elf_load(const char *path, elf_info_t *info) info->num_segments = seg_count; if (seg_count == 0) { - log_error("%s: no PT_LOAD segments", path); + log_error("%s: no PT_LOAD segments", display_path); free(ph_buf); - fclose(f); return -1; } @@ -202,17 +179,29 @@ int elf_load(const char *path, elf_info_t *info) info->phdr_gpa = info->load_min + ehdr.e_phoff; free(ph_buf); - fclose(f); return 0; } -int elf_map_segments(const elf_info_t *info, - const char *path, - void *guest_base, - uint64_t guest_size, - uint64_t load_base, - uint64_t infra_lo, - uint64_t infra_hi) +int elf_load(const char *path, elf_info_t *info) +{ + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + perror(path); + return -1; + } + int rc = elf_load_fd(fd, path, info); + close(fd); + return rc; +} + +int elf_map_segments_fd(const elf_info_t *info, + int fd, + const char *display_path, + void *guest_base, + uint64_t guest_size, + uint64_t load_base, + uint64_t infra_lo, + uint64_t infra_hi) { /* Half-open intersection test for [a, a+alen) and [b, b+blen). When * infra_lo == infra_hi the caller opted out (early bring-up before guest_t @@ -220,16 +209,10 @@ int elf_map_segments(const elf_info_t *info, * guest_size bound check. */ bool infra_active = infra_lo < infra_hi; - FILE *f = fopen(path, "rb"); - if (!f) { - perror(path); - return -1; - } /* Re-read ELF header to get phoff */ elf64_ehdr_t ehdr; - if (fread(&ehdr, sizeof(ehdr), 1, f) != 1) { - fclose(f); + if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr)) { return -1; } @@ -239,19 +222,15 @@ int elf_map_segments(const elf_info_t *info, */ size_t ph_total = (size_t) ehdr.e_phnum * ehdr.e_phentsize; if (ph_total == 0 || ph_total > 65536) { - fclose(f); return -1; } uint8_t *ph_buf = malloc(ph_total); if (!ph_buf) { - fclose(f); return -1; } - if (fseek(f, (long) ehdr.e_phoff, SEEK_SET) != 0 || - fread(ph_buf, ph_total, 1, f) != 1) { + if (pread(fd, ph_buf, ph_total, ehdr.e_phoff) != (ssize_t) ph_total) { free(ph_buf); - fclose(f); return -1; } @@ -268,10 +247,9 @@ int elf_map_segments(const elf_info_t *info, log_error( "%s: program headers at 0x%llx exceed guest memory " "(size 0x%llx)", - path, (unsigned long long) (phdr_dest + ph_total), + display_path, (unsigned long long) (phdr_dest + ph_total), (unsigned long long) guest_size); free(ph_buf); - fclose(f); return -1; } if (infra_active && phdr_dest < infra_hi && @@ -279,10 +257,9 @@ int elf_map_segments(const elf_info_t *info, log_error( "%s: program headers at 0x%llx overlap infra reserve " "[0x%llx, 0x%llx)", - path, (unsigned long long) phdr_dest, (unsigned long long) infra_lo, - (unsigned long long) infra_hi); + display_path, (unsigned long long) phdr_dest, + (unsigned long long) infra_lo, (unsigned long long) infra_hi); free(ph_buf); - fclose(f); return -1; } memcpy((uint8_t *) guest_base + phdr_dest, ph_buf, ph_total); @@ -312,20 +289,19 @@ int elf_map_segments(const elf_info_t *info, log_error( "%s: segment at 0x%llx has filesz > memsz " "(0x%llx > 0x%llx)", - path, (unsigned long long) gpa, (unsigned long long) filesz, - (unsigned long long) memsz); + display_path, (unsigned long long) gpa, + (unsigned long long) filesz, (unsigned long long) memsz); free(ph_buf); - fclose(f); return -1; } /* Keep the mapped segment inside the configured IPA-sized guest slab. */ if (memsz > guest_size || gpa > guest_size - memsz) { - log_error("%s: segment at 0x%llx+0x%llx exceeds guest memory", path, - (unsigned long long) gpa, (unsigned long long) memsz); + log_error("%s: segment at 0x%llx+0x%llx exceeds guest memory", + display_path, (unsigned long long) gpa, + (unsigned long long) memsz); free(ph_buf); - fclose(f); return -1; } @@ -359,11 +335,10 @@ int elf_map_segments(const elf_info_t *info, log_error( "%s: segment at 0x%llx+0x%llx (zero-extent 0x%llx) overlaps " "infra reserve [0x%llx, 0x%llx)", - path, (unsigned long long) gpa, (unsigned long long) memsz, - (unsigned long long) zero_len, (unsigned long long) infra_lo, - (unsigned long long) infra_hi); + display_path, (unsigned long long) gpa, + (unsigned long long) memsz, (unsigned long long) zero_len, + (unsigned long long) infra_lo, (unsigned long long) infra_hi); free(ph_buf); - fclose(f); return -1; } @@ -378,22 +353,14 @@ int elf_map_segments(const elf_info_t *info, memset((uint8_t *) guest_base + gpa + filesz, 0, zero_len - filesz); if (filesz > 0) { - if (fseek(f, (long) ph->p_offset, SEEK_SET) != 0) { - log_error("%s: seek failed for segment at 0x%llx", path, - (unsigned long long) gpa); - free(ph_buf); - fclose(f); - return -1; - } - size_t nread = fread((uint8_t *) guest_base + gpa, 1, filesz, f); - if (nread != filesz) { + if (pread(fd, (uint8_t *) guest_base + gpa, filesz, ph->p_offset) != + (ssize_t) filesz) { log_error( "%s: short read for segment at 0x%llx " - "(got %zu, expected %llu)", - path, (unsigned long long) gpa, nread, + "(expected %llu)", + display_path, (unsigned long long) gpa, (unsigned long long) filesz); free(ph_buf); - fclose(f); return -1; } } @@ -402,10 +369,28 @@ int elf_map_segments(const elf_info_t *info, } free(ph_buf); - fclose(f); return 0; } +int elf_map_segments(const elf_info_t *info, + const char *path, + void *guest_base, + uint64_t guest_size, + uint64_t load_base, + uint64_t infra_lo, + uint64_t infra_hi) +{ + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + perror(path); + return -1; + } + int rc = elf_map_segments_fd(info, fd, path, guest_base, guest_size, + load_base, infra_lo, infra_hi); + close(fd); + return rc; +} + void elf_resolve_interp(const char *sysroot, const char *interp_path, char *out, @@ -430,19 +415,14 @@ void elf_resolve_interp(const char *sysroot, str_copy_trunc(out, interp_path, out_sz); } -int elf_read_shebang(const char *host_path, - char *interp_out, - size_t interp_sz, - char *arg_out, - size_t arg_sz) +int elf_read_shebang_fd(int fd, + char *interp_out, + size_t interp_sz, + char *arg_out, + size_t arg_sz) { - int fd = open(host_path, O_RDONLY); - if (fd < 0) - return -errno; - char buf[512]; - ssize_t nread = read(fd, buf, sizeof(buf) - 1); - close_keep_errno(fd); + ssize_t nread = pread(fd, buf, sizeof(buf) - 1, 0); if (nread < 0) { return -errno; @@ -514,3 +494,17 @@ int elf_read_shebang(const char *host_path, return 1; /* Successfully parsed shebang */ } + +int elf_read_shebang(const char *host_path, + char *interp_out, + size_t interp_sz, + char *arg_out, + size_t arg_sz) +{ + int fd = open(host_path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return -errno; + int rc = elf_read_shebang_fd(fd, interp_out, interp_sz, arg_out, arg_sz); + close_keep_errno(fd); + return rc; +} diff --git a/src/core/elf.h b/src/core/elf.h index 2d1c017..0361785 100644 --- a/src/core/elf.h +++ b/src/core/elf.h @@ -106,6 +106,7 @@ typedef struct { * Returns 0 on success, -1 on failure. Does NOT copy to guest yet. */ int elf_load(const char *path, elf_info_t *info); +int elf_load_fd(int fd, const char *display_path, elf_info_t *info); /* Copy ELF segments into guest memory. Call after elf_load() and guest_init(). * Also copies program headers into guest memory for AT_PHDR. load_base is added @@ -125,6 +126,14 @@ int elf_map_segments(const elf_info_t *info, uint64_t load_base, uint64_t infra_lo, uint64_t infra_hi); +int elf_map_segments_fd(const elf_info_t *info, + int fd, + const char *display_path, + void *guest_base, + uint64_t guest_size, + uint64_t load_base, + uint64_t infra_lo, + uint64_t infra_hi); /* Resolve a PT_INTERP path against a sysroot directory. Tries three strategies: * 1. sysroot + interp_path (standard /lib/ld-musl-*.so.1) @@ -143,10 +152,10 @@ void elf_resolve_interp(const char *sysroot, */ #define ELF_SHEBANG_MAX_DEPTH 5 -/* Read, probe, and parse a shebang script header from host_path. Writes - * interpreter path to interp_out and the single optional argument (if present) - * to arg_out. arg_out will be set to an empty string if there is no optional - * argument. +/* Parse the first line of a file to check for a binfmt_script shebang + * interpreter. Reads the first line into a local buffer and extracts the + * interpreter path and a single optional argument. Trailing whitespace is + * stripped. * * Supports LF (\n), CRLF (\r\n), and CR (\r) line endings. If the shebang line * is not terminated within the 511-byte buffer limit, returns -ENOEXEC. @@ -162,6 +171,11 @@ int elf_read_shebang(const char *host_path, size_t interp_sz, char *arg_out, size_t arg_sz); +int elf_read_shebang_fd(int fd, + char *interp_out, + size_t interp_sz, + char *arg_out, + size_t arg_sz); /* Translate ELF program-header flags (PF_R=4, PF_W=2, PF_X=1) into the * R=1/W=2/X=4 bitset shared by both MEM_PERM_R/W/X (page-table permissions) and diff --git a/src/syscall/exec.c b/src/syscall/exec.c index d694ba7..72bc766 100644 --- a/src/syscall/exec.c +++ b/src/syscall/exec.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "debug/log.h" @@ -314,6 +315,50 @@ static int read_string_array(guest_t *g, return count; } +static int check_exec_permission(const struct stat *st) +{ + uint32_t uid = proc_get_euid(); + uint32_t gid = proc_get_egid(); + + /* Root can execute if any execute bit is set */ + if (uid == 0) { + if (st->st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) + return 0; + return -LINUX_EACCES; + } + + if (uid == (uint32_t) st->st_uid) { + if (st->st_mode & S_IXUSR) + return 0; + return -LINUX_EACCES; + } + + bool in_group = (gid == (uint32_t) st->st_gid); + if (!in_group) { + gid_t groups[64]; + int ngroups = getgroups(64, groups); + if (ngroups > 0) { + for (int i = 0; i < ngroups; i++) { + if (groups[i] == (gid_t) st->st_gid) { + in_group = true; + break; + } + } + } + } + + if (in_group) { + if (st->st_mode & S_IXGRP) + return 0; + return -LINUX_EACCES; + } + + if (st->st_mode & S_IXOTH) + return 0; + + return -LINUX_EACCES; +} + int64_t sys_execve(hv_vcpu_t vcpu, guest_t *g, uint64_t path_gva, @@ -353,6 +398,9 @@ int64_t sys_execve(hv_vcpu_t vcpu, size_t argv_buf_size = 0; size_t envp_buf_size = 0; size_t running_bytes = 0; + int exec_fd = -1; + int interp_fd = -1; + char *temp_str = malloc(131072); if (!temp_str) { @@ -421,11 +469,37 @@ int64_t sys_execve(hv_vcpu_t vcpu, elf_info_t elf_info; int shebang_depth = 0; + /* Open the directly-executed file first and bind it to an fd to avoid + * TOCTOU. */ + exec_fd = open(path_host, O_RDONLY | O_CLOEXEC); + if (exec_fd < 0) { + err = linux_errno(); + goto fail; + } + + /* 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 = (fstat(exec_fd, &exec_st) == 0); + if (!have_exec_st) { + err = linux_errno(); + goto fail; + } + + err = check_exec_permission(&exec_st); + if (err < 0) { + goto fail; + } + while (true) { char interp_start[256]; char interp_arg[256]; - int rc = elf_read_shebang(path_host, interp_start, sizeof(interp_start), - interp_arg, sizeof(interp_arg)); + int rc = + elf_read_shebang_fd(exec_fd, interp_start, sizeof(interp_start), + interp_arg, sizeof(interp_arg)); if (rc < 0) { errno = -rc; err = linux_errno(); @@ -434,6 +508,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). @@ -544,13 +621,48 @@ int64_t sys_execve(hv_vcpu_t vcpu, sizeof(path_host_buf)); path_host = path_host_buf; } + + /* Close old fd and open the new interpreter */ + close(exec_fd); + exec_fd = open(path_host, O_RDONLY | O_CLOEXEC); + if (exec_fd < 0) { + err = linux_errno(); + goto fail; + } + struct stat interp_st; + if (fstat(exec_fd, &interp_st) < 0) { + err = linux_errno(); + goto fail; + } + err = check_exec_permission(&interp_st); + if (err < 0) { + goto fail; + } } - if (elf_load(path_host, &elf_info) < 0) { + if (elf_load_fd(exec_fd, path_host, &elf_info) < 0) { err = -LINUX_ENOEXEC; goto fail; } + /* Compute 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. + */ + uint32_t new_euid = proc_get_euid(); + uint32_t new_egid = proc_get_egid(); + if (have_exec_st && !exec_is_script && S_ISREG(exec_st.st_mode)) { + if (exec_st.st_mode & S_ISUID) { + new_euid = (uint32_t) exec_st.st_uid; + } + if ((exec_st.st_mode & S_ISGID) && (exec_st.st_mode & S_IXGRP)) { + new_egid = (uint32_t) exec_st.st_gid; + } + } + /* 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 @@ -642,7 +754,26 @@ int64_t sys_execve(hv_vcpu_t vcpu, log_debug("execve: pre-validating interpreter: %s", interp_resolved); - if (elf_load(interp_resolved, &interp_info) < 0) { + interp_fd = open(interp_resolved, O_RDONLY | O_CLOEXEC); + if (interp_fd < 0) { + log_error("execve: failed to open interpreter: %s", + interp_resolved); + err = linux_errno(); + goto fail; + } + + struct stat interp_st; + if (fstat(interp_fd, &interp_st) < 0) { + err = linux_errno(); + goto fail; + } + + err = check_exec_permission(&interp_st); + if (err < 0) { + goto fail; + } + + if (elf_load_fd(interp_fd, interp_resolved, &interp_info) < 0) { log_error("execve: failed to load interpreter: %s", interp_resolved); err = -LINUX_ENOEXEC; @@ -660,8 +791,18 @@ int64_t sys_execve(hv_vcpu_t vcpu, /* Past pre-PNR validation. Fall through to point of no return. The fail * label below handles all pre-PNR error paths. */ + /* Commit credentials right before the Point of No Return. + * Saved UID/GID are refreshed from the final effective IDs. + */ + proc_set_ids(proc_get_uid(), new_euid, new_euid, proc_get_gid(), new_egid, + new_egid); + if (0) { fail: + if (exec_fd >= 0) + close(exec_fd); + if (interp_fd >= 0) + close(interp_fd); free(temp_str); exec_cleanup_inputs(argv, envp, argv_buf, envp_buf, path_host_buf, path_host_temp, interp_host_buf, interp_host_temp); @@ -881,6 +1022,12 @@ int64_t sys_execve(hv_vcpu_t vcpu, log_debug("execve: rosetta target %s, entry=0x%llx sp=0x%llx", path, (unsigned long long) entry_ipa, (unsigned long long) sp_ipa); free(temp_str); + if (exec_fd >= 0) { + close(exec_fd); + } + if (interp_fd >= 0) { + close(interp_fd); + } exec_cleanup_inputs(argv, envp, argv_buf, envp_buf, path_host_buf, path_host_temp, interp_host_buf, interp_host_temp); return SYSCALL_EXEC_HAPPENED; @@ -889,8 +1036,9 @@ int64_t sys_execve(hv_vcpu_t vcpu, /* Load the executable image that was validated before guest_reset(). */ uint64_t infra_lo = g->interp_base - INFRA_RESERVE; uint64_t infra_hi = g->interp_base; - if (elf_map_segments(&elf_info, path_host, g->host_base, g->guest_size, - elf_load_base, infra_lo, infra_hi) < 0) { + if (elf_map_segments_fd(&elf_info, exec_fd, path_host, g->host_base, + g->guest_size, elf_load_base, infra_lo, + infra_hi) < 0) { log_fatal( "execve failed after point of no return: " "failed to map ELF segments for %s", @@ -910,9 +1058,9 @@ int64_t sys_execve(hv_vcpu_t vcpu, if (elf_info.interp_path[0] != '\0') { interp_base = g->interp_base; - if (elf_map_segments(&interp_info, interp_resolved, g->host_base, - g->guest_size, interp_base, infra_lo, - infra_hi) < 0) { + if (elf_map_segments_fd(&interp_info, interp_fd, interp_resolved, + g->host_base, g->guest_size, interp_base, + infra_lo, infra_hi) < 0) { log_fatal( "execve failed after point of no return: " "failed to map interpreter segments"); @@ -1184,6 +1332,10 @@ int64_t sys_execve(hv_vcpu_t vcpu, (unsigned long long) entry_ipa, (unsigned long long) sp_ipa); free(temp_str); + if (exec_fd >= 0) + close(exec_fd); + if (interp_fd >= 0) + close(interp_fd); exec_cleanup_inputs(argv, envp, argv_buf, envp_buf, path_host_buf, path_host_temp, interp_host_buf, interp_host_temp);