393 lines
9.0 KiB
C
393 lines
9.0 KiB
C
/*
|
|
* Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#if defined(__linux__) || defined(__APPLE__)
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <dirent.h>
|
|
#include <limits.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#if defined(__linux__)
|
|
#include <sys/prctl.h>
|
|
#endif
|
|
|
|
#include "exec_command.h"
|
|
|
|
#ifndef SYS_close_range
|
|
#define SYS_close_range 436
|
|
#endif
|
|
|
|
#ifndef CLOSE_RANGE_CLOEXEC
|
|
#define CLOSE_RANGE_CLOEXEC 0x4
|
|
#endif
|
|
|
|
static int mark_cloexec(int fd) {
|
|
int flags = fcntl(fd, F_GETFD);
|
|
|
|
if (flags == -1) return flags;
|
|
if (flags & FD_CLOEXEC) return 0;
|
|
|
|
return fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
|
|
}
|
|
|
|
static int cloexec_from(int min_fd) {
|
|
#if defined(__linux__)
|
|
// First try close_range.
|
|
long ret = syscall(SYS_close_range, min_fd, ~0U, CLOSE_RANGE_CLOEXEC);
|
|
if (ret == 0) {
|
|
return 0;
|
|
}
|
|
const char* dirpath = "/proc/self/fd";
|
|
#elif defined(__APPLE__)
|
|
const char* dirpath = "/dev/fd";
|
|
#endif
|
|
DIR *dp = opendir(dirpath);
|
|
if (!dp) return -1;
|
|
|
|
int dp_fd = dirfd(dp);
|
|
struct dirent *de;
|
|
|
|
while ((de = readdir(dp))) {
|
|
if (de->d_name[0] == '.') continue;
|
|
|
|
char *end;
|
|
long val = strtol(de->d_name, &end, 10);
|
|
if (*end || val < 0 || val > INT_MAX) continue;
|
|
|
|
int fd = (int)val;
|
|
if (fd < min_fd || fd == dp_fd) continue;
|
|
|
|
int ret = mark_cloexec(fd);
|
|
if (ret != 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
close(dp_fd);
|
|
closedir(dp);
|
|
return 0;
|
|
}
|
|
|
|
void exec_command_attrs_init(struct exec_command_attrs *attrs) {
|
|
attrs->setpgid = 0;
|
|
attrs->pgid = 0;
|
|
attrs->setsid = 0;
|
|
attrs->setctty = 0;
|
|
attrs->ctty = 0;
|
|
attrs->mask = 0;
|
|
attrs->uid = -1;
|
|
attrs->gid = -1;
|
|
attrs->pdeathSignal = 0;
|
|
attrs->setfgpgrp = 0;
|
|
}
|
|
|
|
static void child_handler(const int sync_pipes[2], const char *executable,
|
|
char *const args[], char *const environment[],
|
|
const int file_handles[], const int file_handle_count,
|
|
const char *cwd, const sigset_t old_mask,
|
|
const struct exec_command_attrs attrs) {
|
|
int i = 0;
|
|
int err = 0;
|
|
int fd_index = 0;
|
|
int fd_table[file_handle_count];
|
|
struct rlimit limits = {0};
|
|
int syncfd = sync_pipes[1];
|
|
struct sigaction action = {0};
|
|
|
|
// Closing our parent's side of the pipe
|
|
if (close(sync_pipes[0]) < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
// Setup process group and foreground before clearing signal mask.
|
|
if (attrs.setpgid) {
|
|
if (setpgid(0, attrs.pgid) < 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
// Make the new process group the foreground process group so it can read from the TTY.
|
|
if (attrs.setfgpgrp) {
|
|
if (tcsetpgrp(STDIN_FILENO, getpgrp()) < 0) {
|
|
if (errno != ENOTTY && errno != ENXIO) {
|
|
goto fail;
|
|
}
|
|
}
|
|
}
|
|
|
|
// clear sighandlers
|
|
action.sa_flags = 0;
|
|
action.sa_handler = SIG_DFL;
|
|
sigemptyset(&action.sa_mask);
|
|
for (i = 0; i < NSIG; i++) {
|
|
sigaction(i, &action, 0);
|
|
}
|
|
|
|
sigset_t local_mask;
|
|
sigemptyset(&local_mask);
|
|
if (pthread_sigmask(SIG_SETMASK, &local_mask, NULL) < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
// start shuffling fds.
|
|
// look at all the file handles and find the highest one,
|
|
// use that for our pipe,
|
|
//
|
|
// Then, we need to start dup2 the fds starting for the final process
|
|
// at 0-n.
|
|
// as an example we have this list of FDs that should be passed to the
|
|
// process:
|
|
//
|
|
/*
|
|
The index of this list is the final result that the new process expects.
|
|
The values are open fds provided from the parent process.
|
|
[0] == 12
|
|
[1] == 7
|
|
[2] == 9
|
|
[3] == 0
|
|
|
|
We also have a pipe to sync the child and parent so that adds an additional
|
|
parameter to consider.
|
|
|
|
So we start by finding the highest open fd in the list, then move our pipe to
|
|
the next.
|
|
|
|
i.e. fd12 is highest so move our pipe to fd13
|
|
|
|
Now start moving all the fds above our pipe as we will need to start placing
|
|
the fds in the child process into the right order. Make sure they are all
|
|
marked cloexec.
|
|
|
|
pipe == 13
|
|
[0] == 12 dup2 14
|
|
[1] == 7 dup2 15
|
|
[2] == 9 dup2 16
|
|
[3] == 0 dup2 17
|
|
|
|
Now overwrite the fd table for the child with the current index.
|
|
|
|
Make index == fd.
|
|
|
|
pipe == 13
|
|
[0] == 14 dup2 0
|
|
[1] == 15 dup2 1
|
|
[2] == 16 dup2 2
|
|
[3] == 17 dup2 3
|
|
|
|
Clear cloexec on this new fds.
|
|
*/
|
|
|
|
// find the highest fd value in our list.
|
|
for (i = 0; i < file_handle_count; i++) {
|
|
if (file_handles[i] > fd_index) {
|
|
fd_index = file_handles[i];
|
|
}
|
|
fd_table[i] = file_handles[i];
|
|
}
|
|
// now fd_index is == to the highest fd in our list of handles.
|
|
// Increment it and set our pipe to it.
|
|
fd_index++;
|
|
|
|
if (syncfd != fd_index) {
|
|
if (dup2(syncfd, fd_index) < 0) {
|
|
goto fail;
|
|
}
|
|
if (close(syncfd) < 0) {
|
|
goto fail;
|
|
}
|
|
syncfd = fd_index;
|
|
}
|
|
fd_index++;
|
|
|
|
// make sure our syncfd retains its cloexec
|
|
if (fcntl(syncfd, F_SETFD, FD_CLOEXEC) == -1) {
|
|
goto fail;
|
|
}
|
|
|
|
// move the rest of the fds up above our index if they don't match the index.
|
|
for (i = 0; i < file_handle_count; i++) {
|
|
if (fd_table[i] == i) {
|
|
continue;
|
|
}
|
|
if (dup2(fd_table[i], fd_index) < 0) {
|
|
goto fail;
|
|
}
|
|
if (fcntl(fd_index, F_SETFD, FD_CLOEXEC) == -1) {
|
|
goto fail;
|
|
}
|
|
fd_table[i] = fd_index;
|
|
fd_index++;
|
|
}
|
|
|
|
// now create the child process's final fd table. where i == i
|
|
for (i = 0; i < file_handle_count; i++) {
|
|
if (fd_table[i] != i) {
|
|
if (dup2(fd_table[i], i) < 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
// now fd[i] should == i
|
|
// clear cloexec as this fd is where we want it.
|
|
if (fcntl(i, F_SETFD, 0) == -1) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
if (attrs.setsid) {
|
|
if (setsid() == -1) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
if (attrs.setctty) {
|
|
if (ioctl(attrs.ctty, TIOCSCTTY, 0)) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
#if defined(__linux__)
|
|
// Set parent death signal if specified
|
|
if (attrs.pdeathSignal != 0) {
|
|
if (prctl(PR_SET_PDEATHSIG, attrs.pdeathSignal) != 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// close exec everything outside of our child's fd_table.
|
|
if (cloexec_from(file_handle_count) != 0) {
|
|
goto fail;
|
|
}
|
|
|
|
// set gid
|
|
if (attrs.gid != -1) {
|
|
if (setgid(attrs.gid) != 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
// set uid
|
|
if (attrs.uid != -1) {
|
|
if (setreuid(attrs.uid, attrs.uid) != 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
if (cwd != NULL) {
|
|
if (chdir(cwd)) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
execve(executable, args, environment);
|
|
fail:
|
|
err = errno;
|
|
if (err) {
|
|
// send our error to the parent
|
|
while (write(syncfd, &err, sizeof(err)) < 0)
|
|
;
|
|
}
|
|
exit(127);
|
|
}
|
|
|
|
int exec_command(pid_t *result, const char *executable, char *const args[],
|
|
char *const envp[], const int file_handles[],
|
|
const int file_handle_count, const char *working_directory,
|
|
struct exec_command_attrs *attrs) {
|
|
pid_t pid = 0;
|
|
int err = 0;
|
|
int sync_pipe[2];
|
|
sigset_t old_mask;
|
|
|
|
sigset_t all;
|
|
sigfillset(&all);
|
|
|
|
if (pipe(sync_pipe)) {
|
|
goto fail;
|
|
}
|
|
|
|
if (pthread_sigmask(SIG_SETMASK, &all, &old_mask) < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
pid = fork();
|
|
if (pid == -1) {
|
|
close(sync_pipe[0]);
|
|
close(sync_pipe[1]);
|
|
goto fail;
|
|
}
|
|
|
|
if (pid == 0) {
|
|
// hand off to child
|
|
child_handler(sync_pipe, executable, args, envp, file_handles,
|
|
file_handle_count, working_directory, old_mask, *attrs);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// handle parent operations
|
|
if (close(sync_pipe[1]) < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
// sync with our child process
|
|
err = 0;
|
|
ssize_t size = read(sync_pipe[0], &err, sizeof(err));
|
|
// -- we didn't get an errno back
|
|
if (size != sizeof(err)) {
|
|
// will be used as return result
|
|
err = 0;
|
|
} else {
|
|
// we did get an errno back from the child process and our
|
|
// err var is set to that errno
|
|
// lets set our errno and then reap the process
|
|
errno = err;
|
|
int status = 0;
|
|
waitpid(pid, &status, 0);
|
|
// lets continue our journey below
|
|
}
|
|
|
|
if (close(sync_pipe[0]) < 0) {
|
|
goto fail;
|
|
}
|
|
if (err) {
|
|
goto fail;
|
|
}
|
|
|
|
(*result) = pid;
|
|
err = 0;
|
|
fail:
|
|
if (pthread_sigmask(SIG_SETMASK, &old_mask, 0) < 0) {
|
|
printf("restoring signal mask: %s\n", strerror(errno));
|
|
}
|
|
if (err) {
|
|
printf("exec_command execve: %s\n", strerror(err));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#endif |