| // Copyright 2020 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // Copyright 2019 Google LLC |
| // |
| // 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. |
| |
| #ifndef _GNU_SOURCE |
| #define _GNU_SOURCE |
| #endif |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <poll.h> |
| #include <signal.h> |
| #include <stdarg.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/mman.h> |
| #include <sys/time.h> |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <time.h> |
| #include <unistd.h> |
| |
| #include "libreprl.h" |
| |
| // Well-known file descriptor numbers for reprl <-> child communication, child process side |
| #define REPRL_CHILD_CTRL_IN 100 |
| #define REPRL_CHILD_CTRL_OUT 101 |
| #define REPRL_CHILD_DATA_IN 102 |
| #define REPRL_CHILD_DATA_OUT 103 |
| |
| #define MIN(x, y) ((x) < (y) ? (x) : (y)) |
| |
| static uint64_t current_millis() |
| { |
| struct timespec ts; |
| clock_gettime(CLOCK_REALTIME, &ts); |
| return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; |
| } |
| |
| static char** copy_string_array(const char** orig) |
| { |
| size_t num_entries = 0; |
| for (const char** current = orig; *current; current++) { |
| num_entries += 1; |
| } |
| char** copy = calloc(num_entries + 1, sizeof(char*)); |
| for (size_t i = 0; i < num_entries; i++) { |
| copy[i] = strdup(orig[i]); |
| } |
| return copy; |
| } |
| |
| static void free_string_array(char** arr) |
| { |
| if (!arr) return; |
| for (char** current = arr; *current; current++) { |
| free(*current); |
| } |
| free(arr); |
| } |
| |
| // A unidirectional communication channel for larger amounts of data, up to a maximum size (REPRL_MAX_DATA_SIZE). |
| // Implemented as a (RAM-backed) file for which the file descriptor is shared with the child process and which is mapped into our address space. |
| struct data_channel { |
| // File descriptor of the underlying file. Directly shared with the child process. |
| int fd; |
| // Memory mapping of the file, always of size REPRL_MAX_DATA_SIZE. |
| char* mapping; |
| }; |
| |
| struct reprl_context { |
| // Whether reprl_initialize has been successfully performed on this context. |
| int initialized; |
| |
| // Read file descriptor of the control pipe. Only valid if a child process is running (i.e. pid is nonzero). |
| int ctrl_in; |
| // Write file descriptor of the control pipe. Only valid if a child process is running (i.e. pid is nonzero). |
| int ctrl_out; |
| |
| // Data channel REPRL -> Child |
| struct data_channel* data_in; |
| // Data channel Child -> REPRL |
| struct data_channel* data_out; |
| |
| // Optional data channel for the child's stdout and stderr. |
| struct data_channel* stdout; |
| struct data_channel* stderr; |
| |
| // PID of the child process. Will be zero if no child process is currently running. |
| int pid; |
| |
| // Arguments and environment for the child process. |
| char** argv; |
| char** envp; |
| |
| // A malloc'd string containing a description of the last error that occurred. |
| char* last_error; |
| }; |
| |
| static int reprl_error(struct reprl_context* ctx, const char *format, ...) |
| { |
| va_list args; |
| va_start(args, format); |
| free(ctx->last_error); |
| vasprintf(&ctx->last_error, format, args); |
| return -1; |
| } |
| |
| static struct data_channel* reprl_create_data_channel(struct reprl_context* ctx) |
| { |
| #ifdef __linux__ |
| int fd = memfd_create("REPRL_DATA_CHANNEL", MFD_CLOEXEC); |
| #else |
| char path[] = "/tmp/reprl_data_channel_XXXXXXXX"; |
| if (mktemp(path) < 0) { |
| reprl_error(ctx, "Failed to create temporary filename for data channel: %s", strerror(errno)); |
| return NULL; |
| } |
| int fd = open(path, O_RDWR | O_CREAT| O_CLOEXEC); |
| unlink(path); |
| #endif |
| if (fd == -1 || ftruncate(fd, REPRL_MAX_DATA_SIZE) != 0) { |
| reprl_error(ctx, "Failed to create data channel file: %s", strerror(errno)); |
| return NULL; |
| } |
| char* mapping = mmap(0, REPRL_MAX_DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); |
| if (mapping == MAP_FAILED) { |
| reprl_error(ctx, "Failed to mmap data channel file: %s", strerror(errno)); |
| return NULL; |
| } |
| |
| struct data_channel* channel = malloc(sizeof(struct data_channel)); |
| channel->fd = fd; |
| channel->mapping = mapping; |
| return channel; |
| } |
| |
| static void reprl_destroy_data_channel(struct reprl_context* ctx, struct data_channel* channel) |
| { |
| if (!channel) return; |
| close(channel->fd); |
| munmap(channel->mapping, REPRL_MAX_DATA_SIZE); |
| free(channel); |
| } |
| |
| static void reprl_child_terminated(struct reprl_context* ctx) |
| { |
| if (!ctx->pid) return; |
| ctx->pid = 0; |
| close(ctx->ctrl_in); |
| close(ctx->ctrl_out); |
| } |
| |
| static void reprl_terminate_child(struct reprl_context* ctx) |
| { |
| if (!ctx->pid) return; |
| int status; |
| kill(ctx->pid, SIGKILL); |
| waitpid(ctx->pid, &status, 0); |
| reprl_child_terminated(ctx); |
| } |
| |
| static int reprl_spawn_child(struct reprl_context* ctx) |
| { |
| // This is also a good time to ensure the data channel backing files don't grow too large. |
| ftruncate(ctx->data_in->fd, REPRL_MAX_DATA_SIZE); |
| ftruncate(ctx->data_out->fd, REPRL_MAX_DATA_SIZE); |
| if (ctx->stdout) ftruncate(ctx->stdout->fd, REPRL_MAX_DATA_SIZE); |
| if (ctx->stderr) ftruncate(ctx->stderr->fd, REPRL_MAX_DATA_SIZE); |
| |
| int crpipe[2] = { 0, 0 }; // control pipe child -> reprl |
| int cwpipe[2] = { 0, 0 }; // control pipe reprl -> child |
| |
| if (pipe(crpipe) != 0) { |
| return reprl_error(ctx, "Could not create pipe for REPRL communication: %s", strerror(errno)); |
| } |
| if (pipe(cwpipe) != 0) { |
| close(crpipe[0]); |
| close(crpipe[1]); |
| return reprl_error(ctx, "Could not create pipe for REPRL communication: %s", strerror(errno)); |
| } |
| |
| ctx->ctrl_in = crpipe[0]; |
| ctx->ctrl_out = cwpipe[1]; |
| fcntl(ctx->ctrl_in, F_SETFD, FD_CLOEXEC); |
| fcntl(ctx->ctrl_out, F_SETFD, FD_CLOEXEC); |
| |
| int pid = fork(); |
| if (pid == 0) { |
| dup2(cwpipe[0], REPRL_CHILD_CTRL_IN); |
| dup2(crpipe[1], REPRL_CHILD_CTRL_OUT); |
| close(cwpipe[0]); |
| close(crpipe[1]); |
| |
| dup2(ctx->data_out->fd, REPRL_CHILD_DATA_IN); |
| dup2(ctx->data_in->fd, REPRL_CHILD_DATA_OUT); |
| |
| int devnull = open("/dev/null", O_RDWR); |
| dup2(devnull, 0); |
| if (ctx->stdout) dup2(ctx->stdout->fd, 1); |
| else dup2(devnull, 1); |
| if (ctx->stderr) dup2(ctx->stderr->fd, 2); |
| else dup2(devnull, 2); |
| close(devnull); |
| |
| // close all other FDs. We try to use FD_CLOEXEC everywhere, but let's be extra sure we don't leak any fds to the child. |
| int tablesize = getdtablesize(); |
| for (int i = 3; i < tablesize; i++) { |
| if (i == REPRL_CHILD_CTRL_IN || i == REPRL_CHILD_CTRL_OUT || i == REPRL_CHILD_DATA_IN || i == REPRL_CHILD_DATA_OUT) { |
| continue; |
| } |
| close(i); |
| } |
| |
| execve(ctx->argv[0], ctx->argv, ctx->envp); |
| |
| fprintf(stderr, "Failed to execute child process %s: %s\n", ctx->argv[0], strerror(errno)); |
| fflush(stderr); |
| _exit(-1); |
| } |
| |
| close(crpipe[1]); |
| close(cwpipe[0]); |
| |
| if (pid < 0) { |
| close(ctx->ctrl_in); |
| close(ctx->ctrl_out); |
| return reprl_error(ctx, "Failed to fork: %s", strerror(errno)); |
| } |
| ctx->pid = pid; |
| |
| char helo[4] = { 0 }; |
| if (read(ctx->ctrl_in, helo, 4) != 4) { |
| reprl_terminate_child(ctx); |
| return reprl_error(ctx, "Did not receive HELO message from child"); |
| } |
| |
| if (strncmp(helo, "HELO", 4) != 0) { |
| reprl_terminate_child(ctx); |
| return reprl_error(ctx, "Received invalid HELO message from child"); |
| } |
| |
| if (write(ctx->ctrl_out, helo, 4) != 4) { |
| reprl_terminate_child(ctx); |
| return reprl_error(ctx, "Failed to send HELO reply message to child"); |
| } |
| |
| return 0; |
| } |
| |
| struct reprl_context* reprl_create_context() |
| { |
| struct reprl_context* ctx = malloc(sizeof(struct reprl_context)); |
| memset(ctx, 0, sizeof(struct reprl_context)); |
| return ctx; |
| } |
| |
| int reprl_initialize_context(struct reprl_context* ctx, const char** argv, const char** envp, int capture_stdout, int capture_stderr) |
| { |
| if (ctx->initialized) { |
| return reprl_error(ctx, "Context is already initialized"); |
| } |
| |
| // We need to ignore SIGPIPE since we could end up writing to a pipe after our child process has exited. |
| signal(SIGPIPE, SIG_IGN); |
| |
| ctx->argv = copy_string_array(argv); |
| ctx->envp = copy_string_array(envp); |
| |
| ctx->data_in = reprl_create_data_channel(ctx); |
| ctx->data_out = reprl_create_data_channel(ctx); |
| if (capture_stdout) { |
| ctx->stdout = reprl_create_data_channel(ctx); |
| } |
| if (capture_stderr) { |
| ctx->stderr = reprl_create_data_channel(ctx); |
| } |
| if (!ctx->data_in || !ctx->data_out || (capture_stdout && !ctx->stdout) || (capture_stderr && !ctx->stderr)) { |
| // Proper error message will have been set by reprl_create_data_channel |
| return -1; |
| } |
| |
| ctx->initialized = 1; |
| return 0; |
| } |
| |
| void reprl_destroy_context(struct reprl_context* ctx) |
| { |
| reprl_terminate_child(ctx); |
| |
| free_string_array(ctx->argv); |
| free_string_array(ctx->envp); |
| |
| reprl_destroy_data_channel(ctx, ctx->data_in); |
| reprl_destroy_data_channel(ctx, ctx->data_out); |
| reprl_destroy_data_channel(ctx, ctx->stdout); |
| reprl_destroy_data_channel(ctx, ctx->stderr); |
| |
| free(ctx->last_error); |
| free(ctx); |
| } |
| |
| int reprl_execute(struct reprl_context* ctx, const char* script, uint64_t script_length, uint64_t timeout, uint64_t* execution_time, int fresh_instance) |
| { |
| if (!ctx->initialized) { |
| return reprl_error(ctx, "REPRL context is not initialized"); |
| } |
| if (script_length > REPRL_MAX_DATA_SIZE) { |
| return reprl_error(ctx, "Script too large"); |
| } |
| |
| // Terminate any existing instance if requested. |
| if (fresh_instance && ctx->pid) { |
| reprl_terminate_child(ctx); |
| } |
| |
| // Reset file position so the child can simply read(2) and write(2) to these fds. |
| lseek(ctx->data_out->fd, 0, SEEK_SET); |
| lseek(ctx->data_in->fd, 0, SEEK_SET); |
| if (ctx->stdout) { |
| lseek(ctx->stdout->fd, 0, SEEK_SET); |
| } |
| if (ctx->stderr) { |
| lseek(ctx->stderr->fd, 0, SEEK_SET); |
| } |
| |
| // Spawn a new instance if necessary. |
| if (!ctx->pid) { |
| int r = reprl_spawn_child(ctx); |
| if (r != 0) return r; |
| } |
| |
| // Copy the script to the data channel. |
| memcpy(ctx->data_out->mapping, script, script_length); |
| |
| // Tell child to execute the script. |
| if (write(ctx->ctrl_out, "exec", 4) != 4 || |
| write(ctx->ctrl_out, &script_length, 8) != 8) { |
| // These can fail if the child unexpectedly terminated between executions. |
| // Check for that here to be able to provide a better error message. |
| int status; |
| if (waitpid(ctx->pid, &status, WNOHANG) == ctx->pid) { |
| reprl_child_terminated(ctx); |
| if (WIFEXITED(status)) { |
| return reprl_error(ctx, "Child unexpectedly exited with status %i between executions", WEXITSTATUS(status)); |
| } else { |
| return reprl_error(ctx, "Child unexpectedly terminated with signal %i between executions", WTERMSIG(status)); |
| } |
| } |
| return reprl_error(ctx, "Failed to send command to child process: %s", strerror(errno)); |
| } |
| |
| // Wait for child to finish execution (or crash). |
| uint64_t start_time = current_millis(); |
| struct pollfd fds = {.fd = ctx->ctrl_in, .events = POLLIN, .revents = 0}; |
| int res = poll(&fds, 1, (int)timeout); |
| *execution_time = current_millis() - start_time; |
| if (res == 0) { |
| // Execution timed out. Kill child and return a timeout status. |
| reprl_terminate_child(ctx); |
| return 1 << 16; |
| } else if (res != 1) { |
| // An error occurred. |
| // We expect all signal handlers to be installed with SA_RESTART, so receiving EINTR here is unexpected and thus also an error. |
| return reprl_error(ctx, "Failed to poll: %s", strerror(errno)); |
| } |
| |
| // Poll succeeded, so there must be something to read now (either the status or EOF). |
| int status; |
| ssize_t rv = read(ctx->ctrl_in, &status, 4); |
| if (rv < 0) { |
| return reprl_error(ctx, "Failed to read from control pipe: %s", strerror(errno)); |
| } else if (rv != 4) { |
| // Most likely, the child process crashed and closed the write end of the control pipe. |
| // Unfortunately, there probably is nothing that guarantees that waitpid() will immediately succeed now, |
| // and we also don't want to block here. So just retry waitpid() a few times... |
| int success = 0; |
| do { |
| success = waitpid(ctx->pid, &status, WNOHANG) == ctx->pid; |
| if (!success) usleep(10); |
| } while (!success && current_millis() - start_time < timeout); |
| |
| if (!success) { |
| // Wait failed, so something weird must have happened. Maybe somehow the control pipe was closed without the child exiting? |
| // Probably the best we can do is kill the child and return an error. |
| reprl_terminate_child(ctx); |
| return reprl_error(ctx, "Child in weird state after execution"); |
| } |
| |
| // Cleanup any state related to this child process. |
| reprl_child_terminated(ctx); |
| |
| if (WIFEXITED(status)) { |
| status = WEXITSTATUS(status) << 8; |
| } else if (WIFSIGNALED(status)) { |
| status = WTERMSIG(status); |
| } else { |
| // This shouldn't happen, since we don't specify WUNTRACED for waitpid... |
| return reprl_error(ctx, "Waitpid returned unexpected child state %i", status); |
| } |
| } |
| |
| // The status must be a positive number, see the status encoding format below. |
| // We also don't allow the child process to indicate a timeout. If we wanted, |
| // we could treat it as an error if the upper bits are set. |
| status &= 0xffff; |
| |
| return status; |
| } |
| |
| /// The 32bit REPRL exit status as returned by reprl_execute has the following format: |
| /// [ 00000000 | did_timeout | exit_code | terminating_signal ] |
| /// Only one of did_timeout, exit_code, or terminating_signal may be set at one time. |
| int RIFSIGNALED(int status) |
| { |
| return (status & 0xff) != 0; |
| } |
| |
| int RIFEXITED(int status) |
| { |
| return !RIFSIGNALED(status) && !RIFTIMEDOUT(status); |
| } |
| |
| int RIFTIMEDOUT(int status) |
| { |
| return (status & 0xff0000) != 0; |
| } |
| |
| int RTERMSIG(int status) |
| { |
| return status & 0xff; |
| } |
| |
| int REXITSTATUS(int status) |
| { |
| return (status >> 8) & 0xff; |
| } |
| |
| static const char* fetch_data_channel_content(struct data_channel* channel) |
| { |
| if (!channel) return ""; |
| size_t pos = lseek(channel->fd, 0, SEEK_CUR); |
| pos = MIN(pos, REPRL_MAX_DATA_SIZE - 1); |
| channel->mapping[pos] = 0; |
| return channel->mapping; |
| } |
| |
| const char* reprl_fetch_fuzzout(struct reprl_context* ctx) |
| { |
| return fetch_data_channel_content(ctx->data_in); |
| } |
| |
| const char* reprl_fetch_stdout(struct reprl_context* ctx) |
| { |
| return fetch_data_channel_content(ctx->stdout); |
| } |
| |
| const char* reprl_fetch_stderr(struct reprl_context* ctx) |
| { |
| return fetch_data_channel_content(ctx->stderr); |
| } |
| |
| const char* reprl_get_last_error(struct reprl_context* ctx) |
| { |
| return ctx->last_error; |
| } |