diff --git a/infra/experimental/chronos/README.md b/infra/experimental/chronos/README.md index df93e68fd3ac..0ed9a9e6d3a9 100644 --- a/infra/experimental/chronos/README.md +++ b/infra/experimental/chronos/README.md @@ -8,12 +8,12 @@ export FUZZING_LANGUAGE=c infra/experimental/chronos/prepare-recompile "$PROJECT" "$FUZZ_TARGET" "$FUZZING_LANGUAGE" python infra/helper.py build_image "$PROJECT" # AddressSanitizer. -docker run -ti --entrypoint="/bin/sh" --env SANITIZER="address" --name "${PROJECT}-origin-asan" "gcr.io/oss-fuzz/${PROJECT}" -c "compile && rm -rf /out/*" +docker run --cap-add=SYS_PTRACE -ti --entrypoint="/bin/sh" --env SANITIZER="address" --name "${PROJECT}-origin-asan" "gcr.io/oss-fuzz/${PROJECT}" -c "compile && rm -rf /out/*" docker commit "${PROJECT}-origin-asan" "gcr.io/oss-fuzz/${PROJECT}-ofg-cached-asan" docker run -ti --entrypoint="recompile" "gcr.io/oss-fuzz/${PROJECT}-ofg-cached-asan" # Coverage measurement. -docker run -ti --entrypoint="/bin/sh" --env SANITIZER="coverage" --name "${PROJECT}-origin-cov" "gcr.io/oss-fuzz/${PROJECT}" -c "compile && rm -rf /out/*" +docker run --cap-add=SYS_PTRACE -ti --entrypoint="/bin/sh" --env SANITIZER="coverage" --name "${PROJECT}-origin-cov" "gcr.io/oss-fuzz/${PROJECT}" -c "compile && rm -rf /out/*" docker commit "${PROJECT}-origin-cov" "gcr.io/oss-fuzz/${PROJECT}-ofg-cached-cov" docker run -ti --entrypoint="recompile" "gcr.io/oss-fuzz/${PROJECT}-ofg-cached-cov" ``` diff --git a/infra/experimental/chronos/chronos-build.sh b/infra/experimental/chronos/chronos-build.sh new file mode 100755 index 000000000000..e6179a58cac4 --- /dev/null +++ b/infra/experimental/chronos/chronos-build.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Copyright 2024 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 +# +# http://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. +# +################################################################################ +/src/tracer $FUZZ_TARGET /usr/local/bin/recompile $SRC/real_build.sh diff --git a/infra/experimental/chronos/chronos.sh b/infra/experimental/chronos/chronos.sh deleted file mode 100644 index bd83b49095ea..000000000000 --- a/infra/experimental/chronos/chronos.sh +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2024 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 -# -# http://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. -# -################################################################################ - -# This script records the ENV and commands needed for fuzz target recompilation. -# It intercepts bash commands to save: 1) the ENV variable values before -# building the fuzz target (`recompile_env.sh`) and 2) all subsequent bash -# commands from that point (`recompile`). Combined with Docker, this setup -# allows for recompiling the fuzz target without rebuilding the entire project. -# Usage: -# 1. Set FUZZ_TARGET (e.g., in project's Dockerfile) -# 2. Source this file before compiling the fuzz target (e.g., source chronos.sh -# at the beginning of project's build.sh). - -export START_RECORDING="false" -RECOMPILE_ENV="/usr/local/bin/recompile_env.sh" - - -# Initialize the recompile script as compile in case Chronos did not trap any -# command containing the fuzz target. -initialize_recompile_script() { - export RECOMPILE_SCRIPT="/usr/local/bin/recompile" - cp "/usr/local/bin/compile" "$RECOMPILE_SCRIPT" -} - -reset_recompile_script() { - rm "$RECOMPILE_SCRIPT" - echo "#!/bin/bash" > "$RECOMPILE_SCRIPT" - echo "source $RECOMPILE_ENV" >> "$RECOMPILE_SCRIPT" - chmod +x "$RECOMPILE_SCRIPT" -} - - -# Execute or record command for recompilation. -execute_or_record_command() { - record_command() { - echo "cd \"$(pwd)\"" >> "$RECOMPILE_SCRIPT" - echo "$@" >> "$RECOMPILE_SCRIPT" - } - - # Check if any element in the command array contains the FUZZ_TARGET. - if [[ "$BASH_COMMAND" == *"$FUZZ_TARGET"* ]]; then - export START_RECORDING="true" - # Save all environment variables, excluding read-only ones - reset_recompile_script - declare -p | grep -Ev 'declare -[^ ]*r[^ ]*' > "$RECOMPILE_ENV" - fi - - if [[ "$START_RECORDING" == "true" ]]; then - record_command "$BASH_COMMAND" - echo "Recorded execution of: $BASH_COMMAND" - fi -} - - -main() { - # Initialize. - initialize_recompile_script - - # Set up trap for DEBUG to intercept commands. - trap 'execute_or_record_command' DEBUG - - # Enable extended debugging mode - shopt -s extdebug - # Ensure trap works in subshells and functions. - set -T -} - -main diff --git a/infra/experimental/chronos/prepare-recompile b/infra/experimental/chronos/prepare-recompile index 7c6726b471da..2b36aeda557e 100755 --- a/infra/experimental/chronos/prepare-recompile +++ b/infra/experimental/chronos/prepare-recompile @@ -28,13 +28,14 @@ FUZZ_TARGET=$2 FUZZING_LANGUAGE=$3 # Step 1: Copy chronos.sh to its project directory. -cp infra/experimental/chronos/chronos.sh "projects/$PROJECT/" +cp infra/experimental/chronos/chronos-build.sh "projects/$PROJECT/" # Step 2: Copy chronos.sh to image and set FUZZ_TARGET and FUZZING_LANGUAGE in its Dockerfile. { - echo "COPY chronos.sh /src"; + echo "ADD https://clusterfuzz-builds.storage.googleapis.com/tracer \$SRC/"; + echo "RUN chmod +x \$SRC/tracer"; + echo "RUN cp \$SRC/build.sh \$SRC/real_build.sh"; + echo "COPY chronos-build.sh \$SRC/build.sh"; echo "ENV FUZZ_TARGET=\"$FUZZ_TARGET\""; echo "ENV FUZZING_LANGUAGE=\"$FUZZING_LANGUAGE\""; - # Step 3: Source chronos.sh at the beginning of its build.sh. - echo "RUN sed -i.bak \"1s|^|source \\\"/src/chronos.sh\\\"\\n|\" \"/src/build.sh\"" } >> "projects/$PROJECT/Dockerfile" diff --git a/infra/experimental/chronos/tracer/.gitignore b/infra/experimental/chronos/tracer/.gitignore new file mode 100644 index 000000000000..282dd6b725c1 --- /dev/null +++ b/infra/experimental/chronos/tracer/.gitignore @@ -0,0 +1 @@ +tracer diff --git a/infra/experimental/chronos/tracer/Makefile b/infra/experimental/chronos/tracer/Makefile new file mode 100644 index 000000000000..7c87f6eebdac --- /dev/null +++ b/infra/experimental/chronos/tracer/Makefile @@ -0,0 +1,11 @@ +.POSIX: +CXX = clang++ +CFLAGS = -std=c++20 -Wall -Wextra -O3 -g3 -Werror -static + +all: tracer + +tracer: tracer.cpp inspect_utils.cpp + $(CXX) $(CFLAGS) -lpthread -o $@ $^ + +clean: + rm -f tracer diff --git a/infra/experimental/chronos/tracer/README.md b/infra/experimental/chronos/tracer/README.md new file mode 100644 index 000000000000..fee7c77263c8 --- /dev/null +++ b/infra/experimental/chronos/tracer/README.md @@ -0,0 +1,6 @@ +# Deployment + +``` +$ make +$ gsutil cp tracer gs://clusterfuzz-builds/ +``` diff --git a/infra/experimental/chronos/tracer/inspect_utils.cpp b/infra/experimental/chronos/tracer/inspect_utils.cpp new file mode 100644 index 000000000000..5942c600f125 --- /dev/null +++ b/infra/experimental/chronos/tracer/inspect_utils.cpp @@ -0,0 +1,87 @@ +/* + * Copyright 2022 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 + + * http://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. + */ +/* A detector that uses ptrace to identify DNS arbitrary resolutions. */ + +/* C standard library */ +#include + +/* POSIX */ +#include + +/* Linux */ +#include + +#include +#include +#include +#include + +#include "inspect_utils.h" + +extern pid_t g_root_pid; +extern std::map root_pids; + +std::vector read_memory(pid_t pid, unsigned long long address, + size_t size) { + std::vector memory; + + for (size_t i = 0; i < size; i += sizeof(long)) { + long word = ptrace(PTRACE_PEEKTEXT, pid, address + i, 0); + if (word == -1) { + return memory; + } + + std::byte *word_bytes = reinterpret_cast(&word); + memory.insert(memory.end(), word_bytes, word_bytes + sizeof(long)); + } + + return memory; +} + +// Construct a string with the memory specified in a register. +std::string read_string(pid_t pid, unsigned long long reg, unsigned long length) { + auto memory = read_memory(pid, reg, length); + if (!memory.size()) { + return ""; + } + + std::string content(reinterpret_cast(memory.data()), + std::min(memory.size(), length)); + return content.c_str(); +} + +unsigned long long read_pointer(pid_t pid, unsigned long long address) { + auto memory = read_memory(pid, address, sizeof(unsigned long long)); + return *reinterpret_cast(memory.data()); +} + +// Read null pointer terminated array. +std::vector read_null_pointer_terminated_array( + pid_t pid, unsigned long long address, const int max_item_len, const int max_array_len) { + std::vector result; + + for (int i = 0; i < max_array_len; ++i) { + auto ptr = read_pointer(pid, address); + if (ptr == 0) { + break; + } + auto value = read_string(pid, ptr, max_item_len); + result.push_back(value); + address += sizeof(unsigned long long); + } + + return result; +} diff --git a/infra/experimental/chronos/tracer/inspect_utils.h b/infra/experimental/chronos/tracer/inspect_utils.h new file mode 100644 index 000000000000..d750a4f2d4fc --- /dev/null +++ b/infra/experimental/chronos/tracer/inspect_utils.h @@ -0,0 +1,42 @@ +/* + * Copyright 2022 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 + + * http://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. + */ +/* A detector that uses ptrace to identify DNS arbitrary resolutions. */ + + +/* POSIX */ +#include + +#include +#include + +// Structure to know which thread id triggered the bug. +struct ThreadParent { + // Parent thread ID, ie creator. + pid_t parent_tid; + // Current thread ID ran exec to become another process. + bool ran_exec = false; + + ThreadParent() : parent_tid(0) {} + ThreadParent(pid_t tid) : parent_tid(tid) {} +}; + +std::vector read_memory(pid_t pid, unsigned long long address, + size_t size); +std::string read_string(pid_t pid, unsigned long long reg, unsigned long length); +unsigned long long read_pointer(pid_t pid, unsigned long long address); + +std::vector read_null_pointer_terminated_array( + pid_t pid, unsigned long long address, const int max_item_len, const int max_array_len); diff --git a/infra/experimental/chronos/tracer/tracer.cpp b/infra/experimental/chronos/tracer/tracer.cpp new file mode 100644 index 000000000000..1abefc38abcf --- /dev/null +++ b/infra/experimental/chronos/tracer/tracer.cpp @@ -0,0 +1,285 @@ +/* + * Copyright 2024 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 + + * http://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. + */ +/* A syscall tracer that finds where a fuzz target is being built. + +Usage: +``` +$ tracer +``` + +Output (written to ): +``` +#!/bin/sh +export ENV=val +export ENV=val + +cd /path/to/cwd +clang -o fuzz_target ... +``` +*/ + +/* C standard library */ +#include +#include +#include +#include + +/* POSIX */ +#include +#include +#include +#include + +/* Linux */ +#include +#include +#include + +#include +#include +#include +#include + +#include "inspect_utils.h" + +#define DEBUG_LOGS 0 + +#if DEBUG_LOGS +#define debug_log(...) \ + do { \ + fprintf(stderr, __VA_ARGS__); \ + fflush(stdout); \ + fputc('\n', stderr); \ + } while (0) +#else +#define debug_log(...) +#endif + +#define fatal_log(...) \ + do { \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + exit(EXIT_FAILURE); \ + } while (0) + +// The PID of the root process we're fuzzing. +pid_t g_root_pid; + +// Map of a PID/TID its PID/TID creator and wether it ran exec. +std::map root_pids; + +const int kMaxStringLength = 512; + +struct Tracee { + pid_t pid; + bool syscall_enter = true; + + Tracee(pid_t pid) : pid(pid) {} +}; + +pid_t run_child(char **argv) { + // Run the program under test with its args as a child process + pid_t pid = fork(); + switch (pid) { + case -1: + fatal_log("Fork failed: %s", strerror(errno)); + case 0: + raise(SIGSTOP); + execvp(argv[0], argv); + fatal_log("execvp: %s", strerror(errno)); + } + return pid; +} + +bool contains_fuzz_target_ref(const std::vector& args, std::string target_name) { + for (const auto& arg : args) { + if (arg.find(target_name) != std::string::npos) { + return true; + } + } + return false; +} + +int trace(std::map pids, std::string fuzz_target_name, std::string output_path) { + unsigned long exit_status = 0; + while (!pids.empty()) { + std::vector new_pids; + + auto it = pids.begin(); + + while (it != pids.end()) { + auto pid = it->first; + auto &tracee = it->second; + int status = 0; + + int result = waitpid(pid, &status, __WALL | WNOHANG); + if (result == -1) { + it = pids.erase(it); + continue; + } + + if (result == 0) { + // Nothing to report yet. + ++it; + continue; + } + + if (WIFEXITED(status) || WIFSIGNALED(status)) { + debug_log("%d exited", pid); + it = pids.erase(it); + // Remove pid from the watchlist when it exits + root_pids.erase(pid); + continue; + } + + // ptrace sets 0x80 for syscalls (with PTRACE_O_TRACESYSGOOD set). + bool is_syscall = + WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80); + int sig = 0; + if (!is_syscall) { + // Handle generic signal. + siginfo_t siginfo; + if (ptrace(PTRACE_GETSIGINFO, pid, nullptr, &siginfo) == -1) { + debug_log("ptrace(PTRACE_GETSIGINFO, %d): %s", pid, strerror(errno)); + continue; + } + sig = siginfo.si_signo; + debug_log("forwarding signal %d to %d", sig, pid); + } + + if ((status >> 8 == (SIGTRAP | (PTRACE_EVENT_EXIT << 8)))) { + debug_log("%d exiting", pid); + if (pid == g_root_pid) { + if (ptrace(PTRACE_GETEVENTMSG, pid, 0, &exit_status) == -1) { + debug_log("ptrace(PTRACE_GETEVENTMSG, %d): %s", pid, strerror(errno)); + } + debug_log("got exit status from root process: %lu", exit_status); + } + + if (ptrace(PTRACE_DETACH, pid, 0, 0) == -1) { + debug_log("ptrace(PTRACE_DETACH, %d): %s", pid, strerror(errno)); + } + continue; + } + + if (WIFSTOPPED(status) && + (status >> 8 == (SIGTRAP | (PTRACE_EVENT_CLONE << 8)) || + status >> 8 == (SIGTRAP | (PTRACE_EVENT_FORK << 8)) || + status >> 8 == (SIGTRAP | (PTRACE_EVENT_VFORK << 8)))) { + long new_pid; + if (ptrace(PTRACE_GETEVENTMSG, pid, 0, &new_pid) == -1) { + debug_log("ptrace(PTRACE_GETEVENTMSG, %d): %s", pid, strerror(errno)); + continue; + } + debug_log("forked %ld", new_pid); + new_pids.push_back(new_pid); + root_pids.emplace(new_pid, ThreadParent(pid)); + } + + if (is_syscall) { + user_regs_struct regs; + if (ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1) { + debug_log("ptrace(PTRACE_GETREGS, %d): %s", pid, strerror(errno)); + continue; + } + + if (tracee.syscall_enter) { + if (regs.orig_rax == __NR_execve) { + // This is a new process. + auto parent = root_pids[pid]; + parent.ran_exec = true; + root_pids[pid] = parent; + + auto pathname = read_string(pid, regs.rdi, kMaxStringLength); + auto argv = read_null_pointer_terminated_array(pid, regs.rsi, kMaxStringLength, 128); + auto envp = read_null_pointer_terminated_array(pid, regs.rdx, kMaxStringLength, 128); + + if (contains_fuzz_target_ref(argv, fuzz_target_name)) { + // We found the fuzz target build command! + FILE* fp = fopen(output_path.c_str(), "w"); + fprintf(fp, "#!/bin/sh\n"); + for (auto& env : envp) { + auto pos = env.find("="); + // TODO: Properly shell escape this. + env = env.insert(pos + 1, "'"); + env += "'"; + fprintf(fp, "export %s\n", env.c_str()); + } + + std::string cwd_path = std::format("/proc/{}/cwd", pid); + char real_cwd[kMaxStringLength] = {0}; + readlink(cwd_path.c_str(), real_cwd, kMaxStringLength - 1); + + fprintf(fp, "cd %s\n", real_cwd); + for (const auto& arg : argv) { + fprintf(fp, "%s ", arg.c_str()); + } + fprintf(fp, "\n"); + fclose(fp); + chmod(output_path.c_str(), 0755); + } + } + } + + tracee.syscall_enter = !tracee.syscall_enter; + } + + if (ptrace(PTRACE_SYSCALL, pid, nullptr, sig) == -1) { + debug_log("ptrace(PTRACE_SYSCALL, %d): %s", pid, strerror(errno)); + continue; + } + + ++it; + } + + for (const auto &pid : new_pids) { + pids.emplace(pid, Tracee(pid)); + } + } + return static_cast(exit_status >> 8); +} + +int main(int argc, char **argv) { + if (argc <= 3) { + fatal_log("Expecting at least three arguments, received %d", argc - 1); + } + + std::string fuzz_target_name = argv[1]; + std::string output_path = argv[2]; + pid_t pid = run_child(argv + 3); + + long options = PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACEFORK | + PTRACE_O_TRACEVFORK | PTRACE_O_TRACECLONE | + PTRACE_O_TRACEEXIT; + + if (ptrace(PTRACE_SEIZE, pid, nullptr, options) == -1) { + fatal_log("ptrace(PTRACE_SEIZE): %s", strerror(errno)); + } + + if (waitpid(pid, nullptr, __WALL) == -1) { + fatal_log("waitpid: %s", strerror(errno)); + } + + if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) { + fatal_log("ptrace(PTRACE_SYSCALL): %s", strerror(errno)); + } + + g_root_pid = pid; + std::map pids; + pids.emplace(pid, Tracee(pid)); + root_pids.emplace(pid, ThreadParent(pid)); + return trace(pids, fuzz_target_name, output_path); +} diff --git a/projects/libiec61850/Dockerfile b/projects/libiec61850/Dockerfile index 17b63d358b99..5b5b72fda08e 100755 --- a/projects/libiec61850/Dockerfile +++ b/projects/libiec61850/Dockerfile @@ -20,3 +20,9 @@ RUN git clone https://github.com/mz-automation/libiec61850 WORKDIR $SRC COPY build.sh $SRC/ COPY fuzz_decode.options $SRC/fuzz_decode.options +ADD https://clusterfuzz-builds.storage.googleapis.com/tracer $SRC/ +RUN chmod +x $SRC/tracer +RUN cp $SRC/build.sh $SRC/real_build.sh +COPY chronos-build.sh $SRC/build.sh +ENV FUZZ_TARGET="fuzz_mms_decode.c" +ENV FUZZING_LANGUAGE="c" diff --git a/projects/libiec61850/build.sh b/projects/libiec61850/build.sh index 51af3615f68b..7722950ec1fb 100755 --- a/projects/libiec61850/build.sh +++ b/projects/libiec61850/build.sh @@ -18,7 +18,7 @@ cd libiec61850 mkdir build && cd build cmake ../ -make +make -j $CC $CFLAGS $LIB_FUZZING_ENGINE ../fuzz/fuzz_mms_decode.c -c \ -I../src/iec61850/inc -I../src/mms/inc -I../src/common/inc \