diff --git a/CMakeLists.txt b/CMakeLists.txt index b1ae451..648ff63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,8 @@ set(profiler_INC ${CMAKE_SOURCE_DIR}/profiler/inc) set(profiler_SRC ${CMAKE_SOURCE_DIR}/profiler/src) set(procdump_INC ${CMAKE_SOURCE_DIR}/include) set(procdump_SRC ${CMAKE_SOURCE_DIR}/src) +set(lib_INC ${CMAKE_SOURCE_DIR}/lib) +set(lib_SRC ${CMAKE_SOURCE_DIR}/lib) set(procdump_Test ${CMAKE_SOURCE_DIR}/tests/integration) set(LD "/usr/bin/ld") set(libbpf_SOURCE_DIR ${CMAKE_BINARY_DIR}/libbpf/src/libbpf) @@ -192,7 +194,12 @@ endif() if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") message(STATUS "Building for Linux") - add_executable(procdump + + # Build the ProcDump core dump engine as a static library (libprocdump.a). + # External programs can link against this library and call the on-demand + # dump API declared in lib/ProcDumpLib.h. The procdump executable is a thin + # command-line front-end on top of the same library. + add_library(procdumplib STATIC ${procdump_SRC}/CoreDumpWriter.cpp ${procdump_SRC}/DotnetHelpers.cpp ${procdump_SRC}/Events.cpp @@ -201,11 +208,11 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") ${procdump_SRC}/Handle.cpp ${procdump_SRC}/Logging.cpp ${procdump_SRC}/Monitor.cpp - ${procdump_SRC}/Procdump.cpp ${procdump_SRC}/ProcDumpConfiguration.cpp ${procdump_SRC}/Process.cpp ${procdump_SRC}/ProfilerHelpers.cpp ${procdump_SRC}/Restrack.cpp + ${lib_SRC}/ProcDumpLib.cpp ${sym_SOURCE_DIR}/bcc_proc.cpp ${sym_SOURCE_DIR}/bcc_syms.cc ${sym_SOURCE_DIR}/bcc_elf.cpp @@ -213,6 +220,12 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") ${sym_SOURCE_DIR}/bcc_zip.cpp ${PROJECT_BINARY_DIR}/ProcDumpProfiler.o ) + # Produce libprocdump.a (strip the default "libprocdumplib" naming). + set_target_properties(procdumplib PROPERTIES OUTPUT_NAME procdump) + + add_executable(procdump + ${procdump_SRC}/Procdump.cpp + ) # Build corex as a separate static C library add_library(corex STATIC @@ -225,6 +238,21 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") ) target_include_directories(corex PRIVATE ${corex_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/include) target_compile_options(corex PRIVATE -g -pthread -fstack-protector-all -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -Werror -D_GNU_SOURCE -O2) + + target_compile_options(procdumplib PRIVATE -g -pthread -fstack-protector-all -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -Werror -D_GNU_SOURCE -std=c++11 -O2) + target_include_directories(procdumplib PUBLIC + ${procdump_INC} + ${lib_INC} + ${PROJECT_BINARY_DIR} + /usr/include + ${sym_SOURCE_DIR} + ${procdump_ebpf_SOURCE_DIR} + ${corex_SOURCE_DIR} + ) + if(NOT APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") + target_include_directories(procdumplib PUBLIC /usr/include/aarch64-linux-gnu) + endif() + add_dependencies(procdumplib libbpf procdump_ebpf) else() add_executable(procdump ${procdump_SRC}/CoreDumpWriter.cpp @@ -252,6 +280,7 @@ target_compile_options(procdump PRIVATE -g -pthread -fstack-protector-all -U_FOR target_include_directories(procdump PUBLIC ${procdump_INC} + ${lib_INC} ${PROJECT_BINARY_DIR} /usr/include ${sym_SOURCE_DIR} @@ -268,7 +297,8 @@ endif() if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") add_dependencies(procdump libbpf procdump_ebpf) - target_link_libraries(procdump ${libbpf_SOURCE_DIR}/src/libbpf.a elf z pthread corex) + target_link_libraries(procdumplib PUBLIC ${libbpf_SOURCE_DIR}/src/libbpf.a elf z pthread corex) + target_link_libraries(procdump procdumplib) else() target_link_libraries(procdump z pthread) endif() @@ -297,6 +327,28 @@ target_include_directories(ProcDumpTestApplication PUBLIC target_link_libraries(ProcDumpTestApplication pthread) +# +# Make ProcDump library test driver +# +# Thin CLI that exercises the public on-demand dump API (pdWriteDump / +# pdFreeError) declared in lib/ProcDumpLib.h by linking against libprocdump.a. +# Used by the integration test scenarios (lib_api_*.sh) to validate the library. +# +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + add_executable(ProcDumpLibTestDriver + ${procdump_Test}/ProcDumpLibTestDriver.cpp + ) + + target_compile_options(ProcDumpLibTestDriver PRIVATE -g -pthread -fstack-protector-all -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -Werror -D_GNU_SOURCE -std=c++11 -O2) + + target_include_directories(ProcDumpLibTestDriver PUBLIC + ${lib_INC} + ) + + add_dependencies(ProcDumpLibTestDriver procdumplib) + target_link_libraries(ProcDumpLibTestDriver procdumplib) +endif() + # # Make package(s) # diff --git a/README.md b/README.md index 897a923..40b3f16 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# ProcDump [![Build Status](https://dev.azure.com/sysinternals/Tools/_apis/build/status/Sysinternals.ProcDump-for-Linux?branchName=master)](https://dev.azure.com/sysinternals/Tools/_build/latest?definitionId=341&branchName=master) -ProcDump is a Linux and Mac reimagining of the classic ProcDump tool from the Sysinternals suite of tools for Windows. ProcDump provides a convenient way for Linux and Mac developers to create core dumps of their application based on performance triggers. ProcDump for Linux and Mac is part of [Sysinternals](https://sysinternals.com). +# Sysinternals ProcDump [![Build Status](https://dev.azure.com/sysinternals/Tools/_apis/build/status/Sysinternals.ProcDump-for-Linux?branchName=master)](https://dev.azure.com/sysinternals/Tools/_build/latest?definitionId=341&branchName=master) +ProcDump is a Linux and Mac reimagining of the classic Sysinternals ProcDump tool. ProcDump provides a convenient way for Linux and Mac developers to create core dumps of their application based on performance triggers. ProcDump for Linux and Mac is part of [Sysinternals](https://sysinternals.com). ![ProcDump in use](procdump.gif "Procdump in use") diff --git a/include/CoreDumpWriter.h b/include/CoreDumpWriter.h index f1d8eae..e991955 100644 --- a/include/CoreDumpWriter.h +++ b/include/CoreDumpWriter.h @@ -80,6 +80,7 @@ enum ECoreDumpType { struct CoreDumpWriter { struct ProcDumpConfiguration *Config; enum ECoreDumpType Type; + char *ErrorMessage; // Set to a descriptive, caller-owned string when dump generation fails (else NULL) }; struct CoreDumpWriter *NewCoreDumpWriter(enum ECoreDumpType type, struct ProcDumpConfiguration *config); diff --git a/lib/ProcDumpLib.cpp b/lib/ProcDumpLib.cpp new file mode 100644 index 0000000..06e922d --- /dev/null +++ b/lib/ProcDumpLib.cpp @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +//-------------------------------------------------------------------- +// +// ProcDumpLib - Implementation of the public on-demand dump API. +// +// This wraps ProcDump's existing core dump engine (CoreDumpWriter) behind +// a small, dependency-free C API so that other programs can link against +// libprocdump.a and generate a dump without spawning the procdump tool. +// +//-------------------------------------------------------------------- + +#include "Includes.h" +#include "ProcDumpLib.h" + +#include + +//-------------------------------------------------------------------- +// +// pdWriteDump - Immediately generate a core dump of the target process. +// +//-------------------------------------------------------------------- +extern "C" int pdWriteDump( + pid_t processId, + const char* dumpPath, + int dumpMask, + bool bOverwrite, + char** error) +{ + if(error != NULL) + { + *error = NULL; + } + + if(processId <= 0 || dumpPath == NULL || dumpPath[0] == '\0') + { + if(error != NULL) + { + *error = strdup("Invalid argument: a valid processId and dumpPath are required."); + } + return -1; + } + + // + // The existing writer constructs the final dump name from a directory + // (CoreDumpPath) plus a file name prefix (CoreDumpName). Split the caller + // supplied full path accordingly. dirname()/basename() may modify their + // argument, so operate on copies. + // + char* dirCopy = strdup(dumpPath); + char* baseCopy = strdup(dumpPath); + if(dirCopy == NULL || baseCopy == NULL) + { + free(dirCopy); + free(baseCopy); + if(error != NULL) + { + *error = strdup("Out of memory."); + } + return -1; + } + + char* dir = dirname(dirCopy); + char* base = basename(baseCopy); + + // + // Build a minimal configuration sufficient to generate a single dump. + // Value-initialize so every POD field defaults to 0/false (matching the + // zero-initialized global config the procdump tool relies on); a few + // fields read during monitoring (e.g. bTerminated) are not set by + // InitProcDumpConfiguration. + // + struct ProcDumpConfiguration config = {}; + InitProcDumpConfiguration(&config); + + config.ProcessId = processId; + config.ProcessName = GetProcessName(processId); + if(config.ProcessName == NULL) + { + config.ProcessName = strdup("process"); + } + config.CoreDumpPath = strdup(dir); + config.CoreDumpName = strdup(base); + config.NumberOfDumpsToCollect = 1; + config.NumberOfDumpsCollected = 0; + config.bOverwriteExisting = bOverwrite; + config.CoreDumpMask = dumpMask; + config.bUseGcore = false; + config.nQuit = 0; + config.bTerminated = false; + + free(dirCopy); + free(baseCopy); + + int ret = 0; + struct CoreDumpWriter* writer = NewCoreDumpWriter(MANUAL, &config); + + char* dumpName = WriteCoreDump(writer); + if(dumpName == NULL) + { + ret = -1; + if(error != NULL) + { + if(writer->ErrorMessage != NULL) + { + *error = strdup(writer->ErrorMessage); + } + else + { + *error = strdup("Failed to generate core dump."); + } + } + } + else + { + free(dumpName); + } + + if(writer->ErrorMessage != NULL) + { + free(writer->ErrorMessage); + } + free(writer); + + // FreeProcDumpConfiguration releases ProcessName, CoreDumpPath and CoreDumpName. + FreeProcDumpConfiguration(&config); + + return ret; +} + +//-------------------------------------------------------------------- +// +// pdFreeError - Free an error string returned by pdWriteDump. +// +//-------------------------------------------------------------------- +extern "C" void pdFreeError(char* error) +{ + if(error != NULL) + { + free(error); + } +} diff --git a/lib/ProcDumpLib.h b/lib/ProcDumpLib.h new file mode 100644 index 0000000..e2ad5eb --- /dev/null +++ b/lib/ProcDumpLib.h @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +//-------------------------------------------------------------------- +// +// ProcDumpLib - Public API for on-demand core dump generation. +// +// Link against libprocdump.a and include this header to generate a core +// dump of a target process programmatically (without running the procdump +// command-line tool). +// +//-------------------------------------------------------------------- + +#ifndef PROCDUMP_LIB_H +#define PROCDUMP_LIB_H + +#include // pid_t +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// +// Pass as 'dumpMask' to leave the target process' coredump_filter unchanged. +// +#define PD_DUMP_MASK_DEFAULT (-1) + +//--------------------------------------------------------------------------------------------------------- +// pdWriteDump +// +// Immediately generates a core dump of the target process. +// +// processId - Process ID of the target process. +// dumpPath - Full path of the resulting dump file. On Linux the file that is actually produced +// has "." appended to this path (consistent with the procdump tool). +// dumpMask - Linux coredump_filter bitmask to apply while generating the dump, or +// PD_DUMP_MASK_DEFAULT (-1) to leave the process' existing filter unchanged. +// See https://man7.org/linux/man-pages/man5/core.5.html (coredump_filter) for bit meanings. +// bOverwrite - If true, overwrite an existing dump file of the same name; otherwise the call fails +// if the dump file already exists. +// error - Optional. If non-NULL and the function fails, receives a newly allocated, NUL-terminated +// UTF-8 error string that the caller must release with pdFreeError(). Set to NULL on success. +// +// Returns 0 on success, non-zero on failure. +//--------------------------------------------------------------------------------------------------------- +int pdWriteDump( + pid_t processId, + const char* dumpPath, + int dumpMask, + bool bOverwrite, + char** error); + +//--------------------------------------------------------------------------------------------------------- +// pdFreeError +// +// Frees an error string returned by pdWriteDump. +// +// error - Pointer to the error string to free. May be NULL. +//--------------------------------------------------------------------------------------------------------- +void pdFreeError( + char* error); + +#ifdef __cplusplus +} +#endif + +#endif // PROCDUMP_LIB_H diff --git a/lib/sample.cpp b/lib/sample.cpp new file mode 100644 index 0000000..58bcb7d --- /dev/null +++ b/lib/sample.cpp @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +//-------------------------------------------------------------------- +// +// Sample program demonstrating the ProcDump on-demand dump API. +// +// Build (from the build directory after `make`): +// clang++ -I../lib ../lib/sample.cpp libprocdump.a libcorex.a \ +// libbpf/src/libbpf/src/libbpf.a -lelf -lz -lpthread -o pd_sample +// +// Usage: +// ./pd_sample +// +//-------------------------------------------------------------------- + +#include "ProcDumpLib.h" +#include +#include + +int main(int argc, char* argv[]) +{ + if(argc < 3) + { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 2; + } + + pid_t pid = (pid_t)atoi(argv[1]); + const char* path = argv[2]; + + char* error = NULL; + int rc = pdWriteDump(pid, path, PD_DUMP_MASK_DEFAULT, /*bOverwrite*/ true, &error); + if(rc != 0) + { + fprintf(stderr, "pdWriteDump failed: %s\n", error ? error : "(no detail)"); + pdFreeError(error); + return 1; + } + + printf("Dump generated successfully (prefix: %s)\n", path); + return 0; +} diff --git a/src/CoreDumpWriter.cpp b/src/CoreDumpWriter.cpp index acd752c..2bbbaa1 100644 --- a/src/CoreDumpWriter.cpp +++ b/src/CoreDumpWriter.cpp @@ -12,9 +12,40 @@ #endif #include +#include static const char *CoreDumpTypeStrings[] = { "commit", "cpu", "thread", "filedesc", "signal", "time", "exception", "manual", "perfcounter" }; +//-------------------------------------------------------------------- +// +// SetWriterError - Records a descriptive, caller-owned error string on the +// writer so that library callers (pdWriteDump) can surface the failure +// reason instead of the process aborting. +// +//-------------------------------------------------------------------- +static void SetWriterError(struct CoreDumpWriter *self, const char *fmt, ...) +{ + if(self == NULL) + { + return; + } + + if(self->ErrorMessage != NULL) + { + free(self->ErrorMessage); + self->ErrorMessage = NULL; + } + + va_list args; + va_start(args, fmt); + char *msg = NULL; + if(vasprintf(&msg, fmt, args) >= 0) + { + self->ErrorMessage = msg; + } + va_end(args); +} + //-------------------------------------------------------------------- // // NewCoreDumpWriter - Helper function for newing a struct CoreDumpWriter @@ -33,6 +64,7 @@ struct CoreDumpWriter *NewCoreDumpWriter(enum ECoreDumpType type, struct ProcDum writer->Config = config; writer->Type = type; + writer->ErrorMessage = NULL; return writer; } @@ -291,7 +323,9 @@ char* WriteCoreDumpInternal(struct CoreDumpWriter *self, char* socketName) Log(error, INTERNAL_ERROR); Trace("WriteCoreDumpInternal: no write permission to core dump target file %s", coreDumpFileName); - exit(-1); + SetWriterError(self, "No write permission to core dump target directory: %s", self->Config->CoreDumpPath); + free(name); + return NULL; } if(socketName!=NULL) @@ -301,6 +335,9 @@ char* WriteCoreDumpInternal(struct CoreDumpWriter *self, char* socketName) if(GenerateCoreClrDump(socketName, coreDumpFileName)==false) { Log(error, "An error occurred while generating the core dump for the specified .NET process"); + SetWriterError(self, "An error occurred while generating the core dump for the specified .NET process (pid %d)", pid); + free(name); + return NULL; } else { @@ -331,7 +368,10 @@ char* WriteCoreDumpInternal(struct CoreDumpWriter *self, char* socketName) { Log(error, "An error occurred while generating the core dump"); Trace("WriteCoreDumpInternal: Failed to open pipe to gcore"); - exit(1); + SetWriterError(self, "Failed to start gcore for process %d", pid); + free(outputBuffer); + free(name); + return NULL; } // read all output from gcore command @@ -399,7 +439,16 @@ char* WriteCoreDumpInternal(struct CoreDumpWriter *self, char* socketName) Log(error, "GCORE - %s", outputBuffer[j]); } } - exit(-1); + + SetWriterError(self, "gcore failed to generate core dump for process %d (exit status %d)", pid, gcoreStatus); + + for(int j = 0; j < i; j++) + { + free(outputBuffer[j]); + } + free(outputBuffer); + free(name); + return NULL; } else { @@ -451,7 +500,9 @@ char* WriteCoreDumpInternal(struct CoreDumpWriter *self, char* socketName) if(corexRet != COREX_OK) { Log(error, "An error occurred while generating the core dump: %s", corex_strerror()); - exit(-1); + SetWriterError(self, "An error occurred while generating the core dump: %s", corex_strerror()); + free(name); + return NULL; } else { diff --git a/src/corex/arch/aarch64.c b/src/corex/arch/aarch64.c index 89d6b9f..828f9f0 100644 --- a/src/corex/arch/aarch64.c +++ b/src/corex/arch/aarch64.c @@ -15,6 +15,10 @@ #include "arch/arch.h" #include "corex_internal.h" +#ifndef NT_ARM_PAC_MASK +#define NT_ARM_PAC_MASK 0x406 +#endif + int arch_read_gp_regs(pid_t tid, corex_gp_regs_t *regs) { struct iovec iov; @@ -39,6 +43,23 @@ int arch_read_fp_regs(pid_t tid, corex_fp_regs_t *regs) return 0; } +int arch_read_pac_mask(pid_t tid, corex_pac_mask_t *out) +{ + /* + * Capture the pointer-authentication masks. GDB needs the NT_ARM_PAC_MASK + * note to strip PAC bits from signed return addresses; without it stack + * unwinding stops at the first PAC-signed frame. + */ + struct iovec iov; + iov.iov_base = out; + iov.iov_len = sizeof(*out); + + if (ptrace(PTRACE_GETREGSET, tid, (void *)(uintptr_t)NT_ARM_PAC_MASK, &iov) < 0) + return -1; + + return 0; +} + void arch_fill_prstatus(struct elf_prstatus *prs, pid_t pid, pid_t tid, int signo, const corex_gp_regs_t *gp_regs) { diff --git a/src/corex/arch/arch.h b/src/corex/arch/arch.h index e1cb870..db8e30a 100644 --- a/src/corex/arch/arch.h +++ b/src/corex/arch/arch.h @@ -36,6 +36,13 @@ typedef struct user_fpsimd_state corex_fp_regs_t; #error "Unsupported architecture" #endif +/* AArch64 pointer-authentication masks (NT_ARM_PAC_MASK descriptor layout). + * On architectures without pointer authentication this is simply unused. */ +typedef struct { + uint64_t data_mask; + uint64_t insn_mask; +} corex_pac_mask_t; + /* Read general-purpose registers for a stopped thread via ptrace. * Returns 0 on success. */ int arch_read_gp_regs(pid_t tid, corex_gp_regs_t *regs); @@ -44,6 +51,11 @@ int arch_read_gp_regs(pid_t tid, corex_gp_regs_t *regs); * Returns 0 on success. */ int arch_read_fp_regs(pid_t tid, corex_fp_regs_t *regs); +/* Read the architecture's pointer-authentication masks for a stopped thread. + * Only meaningful on AArch64 with pointer authentication; returns 0 and fills + * *out when available, or -1 when unsupported/unavailable (e.g. on x86_64). */ +int arch_read_pac_mask(pid_t tid, corex_pac_mask_t *out); + /* Fill an elf_prstatus structure from our captured GP register state. */ void arch_fill_prstatus(struct elf_prstatus *prs, pid_t pid, pid_t tid, int signo, const corex_gp_regs_t *gp_regs); diff --git a/src/corex/arch/x86_64.c b/src/corex/arch/x86_64.c index b5f38de..5d0d1ae 100644 --- a/src/corex/arch/x86_64.c +++ b/src/corex/arch/x86_64.c @@ -39,6 +39,14 @@ int arch_read_fp_regs(pid_t tid, corex_fp_regs_t *regs) return 0; } +int arch_read_pac_mask(pid_t tid, corex_pac_mask_t *out) +{ + /* x86_64 has no pointer authentication. */ + (void)tid; + (void)out; + return -1; +} + void arch_fill_prstatus(struct elf_prstatus *prs, pid_t pid, pid_t tid, int signo, const corex_gp_regs_t *gp_regs) { diff --git a/src/corex/note_builder.c b/src/corex/note_builder.c index 0dfff07..ed9f7e8 100644 --- a/src/corex/note_builder.c +++ b/src/corex/note_builder.c @@ -21,6 +21,10 @@ #include "arch/arch.h" #include "corex/corex.h" +#ifndef NT_ARM_PAC_MASK +#define NT_ARM_PAC_MASK 0x406 +#endif + #define NOTE_INITIAL_CAPACITY (256 * 1024) #define NOTE_ALIGN 4 @@ -125,6 +129,19 @@ static int build_prstatus_notes(corex_note_buf_t *buf, &threads[i].fp_regs, arch_fp_regset_size()); if (rc != 0) return rc; + + /* + * NT_ARM_PAC_MASK (AArch64 pointer-authentication masks). GDB uses it + * to strip PAC bits from signed return addresses; without it stack + * unwinding halts at the first PAC-signed frame. Emitted per-thread, + * matching the kernel's core-dump layout. Uses the "LINUX" note owner. + */ + if (threads[i].has_pac_mask) { + rc = note_append(buf, "LINUX", NT_ARM_PAC_MASK, + &threads[i].pac_mask, sizeof(threads[i].pac_mask)); + if (rc != 0) + return rc; + } } return 0; } diff --git a/src/corex/ptrace_utils.c b/src/corex/ptrace_utils.c index 364b334..849b7f8 100644 --- a/src/corex/ptrace_utils.c +++ b/src/corex/ptrace_utils.c @@ -74,6 +74,10 @@ int ptrace_read_all_regs(const corex_proc_info_t *info, (int)tid, strerror(errno)); return COREX_ERR_PTRACE; } + + /* Pointer-auth masks are optional (only present on AArch64 with PAC); + * their absence must not fail the dump. */ + ts->has_pac_mask = (arch_read_pac_mask(tid, &ts->pac_mask) == 0) ? 1 : 0; } return 0; diff --git a/src/corex/ptrace_utils.h b/src/corex/ptrace_utils.h index 39f11aa..e3fcc41 100644 --- a/src/corex/ptrace_utils.h +++ b/src/corex/ptrace_utils.h @@ -14,6 +14,8 @@ typedef struct { int signo; /* Signal that stopped this thread */ corex_gp_regs_t gp_regs; /* General-purpose registers */ corex_fp_regs_t fp_regs; /* Floating-point registers */ + corex_pac_mask_t pac_mask; /* AArch64 pointer-auth masks (if available) */ + int has_pac_mask; /* Non-zero if pac_mask is valid */ } corex_thread_state_t; /* Attach to all threads of a process. Stops all threads. diff --git a/tests/integration/ProcDumpLibTestDriver.cpp b/tests/integration/ProcDumpLibTestDriver.cpp new file mode 100644 index 0000000..8e04d59 --- /dev/null +++ b/tests/integration/ProcDumpLibTestDriver.cpp @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +//-------------------------------------------------------------------- +// +// ProcDumpLibTestDriver - Thin CLI used by the integration tests to +// exercise the public on-demand dump API (pdWriteDump / pdFreeError) +// declared in lib/ProcDumpLib.h. +// +// The driver performs no validation of its own; it simply forwards the +// arguments to pdWriteDump and reports the outcome so that the bash +// scenarios (lib_api_*.sh) can decide pass/fail. This keeps the test +// logic in the existing shell-based framework. +// +// Usage: +// ProcDumpLibTestDriver [mask] [overwrite] +// +// pid Target process id. +// path Full dump path prefix. Two sentinels are recognised: +// NULL -> pass a NULL pointer (negative test) +// EMPTY -> pass an empty string (negative test) +// mask Optional. coredump_filter bitmask, or "default" for +// PD_DUMP_MASK_DEFAULT. Defaults to "default". +// overwrite Optional. 1 (default) to overwrite, 0 to fail if the +// dump file already exists. +// +// Exit codes: +// 0 pdWriteDump returned success +// 1 pdWriteDump returned failure +// 2 bad usage +// +// On failure the error string returned by pdWriteDump is printed to +// stderr and released with pdFreeError. pdFreeError is always invoked +// (including with a NULL argument on success) to exercise that path. +// +//-------------------------------------------------------------------- + +#include "ProcDumpLib.h" + +#include +#include +#include +#include + +int main(int argc, char* argv[]) +{ + if(argc < 3) + { + fprintf(stderr, + "Usage: %s [mask] [overwrite]\n", + argv[0]); + return 2; + } + + // Resolve the target pid. + pid_t pid = (pid_t)strtol(argv[1], NULL, 10); + + // Resolve the dump path, honoring the NULL / EMPTY sentinels. + const char* path = argv[2]; + if(strcmp(path, "NULL") == 0) + { + path = NULL; + } + else if(strcmp(path, "EMPTY") == 0) + { + path = ""; + } + + // Resolve the dump mask. + int mask = PD_DUMP_MASK_DEFAULT; + if(argc >= 4 && strcmp(argv[3], "default") != 0) + { + mask = (int)strtol(argv[3], NULL, 0); + } + + // Resolve the overwrite flag (default: overwrite). + bool overwrite = true; + if(argc >= 5) + { + overwrite = (strtol(argv[4], NULL, 10) != 0); + } + + char* error = NULL; + int rc = pdWriteDump(pid, path, mask, overwrite, &error); + + if(rc != 0) + { + fprintf(stderr, + "pdWriteDump failed (rc=%d): %s\n", + rc, error ? error : "(no detail)"); + } + else + { + printf("pdWriteDump succeeded (pid=%d)\n", (int)pid); + } + + // Always exercise pdFreeError, even on success (error is NULL there). + pdFreeError(error); + + return rc != 0 ? 1 : 0; +} diff --git a/tests/integration/runLibTestAndValidate.sh b/tests/integration/runLibTestAndValidate.sh new file mode 100755 index 0000000..11304ab --- /dev/null +++ b/tests/integration/runLibTestAndValidate.sh @@ -0,0 +1,187 @@ +#!/bin/bash +# +# runLibTestAndValidate.sh +# +# Drives the ProcDump on-demand dump library API (pdWriteDump / pdFreeError) +# through the ProcDumpLibTestDriver binary and validates the outcome. This is +# the library-API counterpart of runProcDumpAndValidate.sh and is sourced by +# the lib_api_*.sh scenarios. +# +# Scenario-tunable variables (all optional, sensible defaults shown): +# +# LIBTEST_PID target - spawn ProcDumpTestApplication and dump it (default) +# invalid - pass -1 (negative test) +# zero - pass 0 (negative test) +# dead - pass the pid of a process that has already exited +# - pass a literal pid +# +# LIBTEST_PATH dump - $dumpDir/core (default) +# NULL - pass a NULL pointer (negative test) +# EMPTY - pass an empty string (negative test) +# baddir - a path under a non-existent directory (negative test) +# +# LIBTEST_MASK default (default) or a coredump_filter bitmask value +# LIBTEST_OVERWRITE 1 (default) or 0 +# PRECREATE_DUMP true to create the dump file before running (overwrite tests) +# +# EXPECTSUCCESS true (default) - expect pdWriteDump to return 0 +# false - expect pdWriteDump to return non-zero +# SHOULDDUMP defaults to EXPECTSUCCESS - whether a dump file must exist +# VALIDATE_SIZE true to compare dump size against a gcore reference +# VALIDATE_CONTENT true to validate dump content with GDB +# +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/helpers.sh" + +function runLibTestAndValidate { + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + DRIVERPATH="${DRIVERPATH:-$DIR/../../ProcDumpLibTestDriver}"; + TESTPROGPATH=$(readlink -m "$DIR/../../ProcDumpTestApplication"); + GDBSCRIPT="$DIR/validate_dump.gdb" + + # Defaults + LIBTEST_PID="${LIBTEST_PID:-target}" + LIBTEST_PATH="${LIBTEST_PATH:-dump}" + LIBTEST_MASK="${LIBTEST_MASK:-default}" + LIBTEST_OVERWRITE="${LIBTEST_OVERWRITE:-1}" + EXPECTSUCCESS="${EXPECTSUCCESS:-true}" + SHOULDDUMP="${SHOULDDUMP:-$EXPECTSUCCESS}" + VALIDATE_SIZE="${VALIDATE_SIZE:-false}" + VALIDATE_CONTENT="${VALIDATE_CONTENT:-false}" + PRECREATE_DUMP="${PRECREATE_DUMP:-false}" + + if [ ! -x "$DRIVERPATH" ]; then + echo "[libtest] ERROR: driver not found or not executable: $DRIVERPATH" + exit 1 + fi + + # Clean up dumps from a prior run + pkill -9 gdb > /dev/null 2>&1 + rm -rf /tmp/libdump_* + + dumpDir=$(mktemp -d -t libdump_XXXXXX) + cd "$dumpDir" + + # Spawn the target process when one is required. + targetPid="" + if [ "$LIBTEST_PID" = "target" ]; then + echo [`date +"%T.%3N"`] Starting "$TESTPROGPATH" burn + ("$TESTPROGPATH" burn) & + targetPid=$! + echo "Target PID: $targetPid" + sleep 3 + fi + + # Resolve the pid argument passed to the driver. + case "$LIBTEST_PID" in + target) pidArg=$targetPid ;; + invalid) pidArg=-1 ;; + zero) pidArg=0 ;; + dead) + # Start and immediately reap a short-lived process so its pid is stale. + (true) & + pidArg=$! + wait $pidArg 2>/dev/null + ;; + *) pidArg="$LIBTEST_PID" ;; + esac + + # Resolve the path argument passed to the driver. + case "$LIBTEST_PATH" in + dump) pathArg="$dumpDir/core" ;; + NULL) pathArg="NULL" ;; + EMPTY) pathArg="EMPTY" ;; + baddir) pathArg="$dumpDir/no_such_dir/core" ;; + *) pathArg="$LIBTEST_PATH" ;; + esac + + # The library appends "." to the supplied path. + expectedDump="" + if [ "$pathArg" != "NULL" ] && [ "$pathArg" != "EMPTY" ]; then + expectedDump="${pathArg}.${pidArg}" + fi + + # Optionally pre-create the dump file (overwrite tests). + if [ "$PRECREATE_DUMP" = "true" ] && [ -n "$expectedDump" ]; then + echo "[libtest] Pre-creating dump file: $expectedDump" + echo "stale" > "$expectedDump" + fi + + # Optionally produce a gcore reference dump for size comparison. + gcoreRefDump="" + if [ "$VALIDATE_SIZE" = "true" ] && [ -n "$targetPid" ] && ps -p "$targetPid" > /dev/null 2>&1; then + gcoreRefDir=$(mktemp -d -t gcoreref_XXXXXX) + gcoreRefDump="$gcoreRefDir/refcore.$targetPid" + echo [`date +"%T.%3N"`] Generating gcore reference dump for PID "$targetPid" + timeout 30 gdb -batch -ex "gcore $gcoreRefDump" -p "$targetPid" > /dev/null 2>&1 + [ -f "$gcoreRefDump" ] || gcoreRefDump="" + fi + + # Run the driver. + echo [`date +"%T.%3N"`] Running: "$DRIVERPATH" "$pidArg" "$pathArg" "$LIBTEST_MASK" "$LIBTEST_OVERWRITE" + "$DRIVERPATH" "$pidArg" "$pathArg" "$LIBTEST_MASK" "$LIBTEST_OVERWRITE" + rc=$? + echo "[libtest] driver exit code: $rc" + + # Stop the target process. + if [ -n "$targetPid" ] && ps -p "$targetPid" > /dev/null 2>&1; then + kill -9 "$targetPid" > /dev/null 2>&1 + fi + + result=0 + + # 1. Validate the return code against expectation. + if [ "$EXPECTSUCCESS" = "true" ]; then + if [ "$rc" -ne 0 ]; then + echo "[libtest] FAIL: expected success but driver returned $rc" + result=1 + else + echo "[libtest] PASS: driver returned success as expected" + fi + else + if [ "$rc" -eq 0 ]; then + echo "[libtest] FAIL: expected failure but driver returned success" + result=1 + else + echo "[libtest] PASS: driver returned failure as expected" + fi + fi + + # 2. Validate dump file presence/absence. + if [ "$SHOULDDUMP" = "true" ]; then + if [ -n "$expectedDump" ] && [ -f "$expectedDump" ]; then + echo "[libtest] PASS: dump file produced: $expectedDump" + + if [ "$VALIDATE_SIZE" = "true" ]; then + if [ -n "$gcoreRefDump" ]; then + if ! validatedumpsize "$expectedDump" "$gcoreRefDump" 20; then + echo "[libtest] FAIL: dump size validation failed" + result=1 + fi + else + echo "[libtest] SKIP: no gcore reference dump for size comparison" + fi + fi + + if [ "$VALIDATE_CONTENT" = "true" ] && [ -f "$GDBSCRIPT" ]; then + if ! validatedumpcontent "$expectedDump" "$TESTPROGPATH" "$GDBSCRIPT"; then + echo "[libtest] FAIL: dump content validation failed" + result=1 + fi + fi + else + echo "[libtest] FAIL: expected dump file not found: $expectedDump" + result=1 + fi + else + # No dump expected. For overwrite=false tests a pre-existing file may remain, + # but the library must not have produced a fresh, valid dump. + if [ "$PRECREATE_DUMP" != "true" ] && [ -n "$expectedDump" ] && [ -f "$expectedDump" ]; then + echo "[libtest] FAIL: unexpected dump file produced: $expectedDump" + result=1 + else + echo "[libtest] PASS: no unexpected dump produced" + fi + fi + + exit $result +} diff --git a/tests/integration/scenarios/lib_api_bad_directory.sh b/tests/integration/scenarios/lib_api_bad_directory.sh new file mode 100755 index 0000000..341998f --- /dev/null +++ b/tests/integration/scenarios/lib_api_bad_directory.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Library API: writing to a dump path under a non-existent directory fails. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="target" +LIBTEST_PATH="baddir" +EXPECTSUCCESS=false +SHOULDDUMP=false + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_basic_dump.sh b/tests/integration/scenarios/lib_api_basic_dump.sh new file mode 100755 index 0000000..83dc89f --- /dev/null +++ b/tests/integration/scenarios/lib_api_basic_dump.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Library API: basic on-demand dump of a live target process. +# Exercises pdWriteDump happy path and validates the produced dump. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="target" +LIBTEST_PATH="dump" +EXPECTSUCCESS=true +SHOULDDUMP=true +VALIDATE_SIZE=true +VALIDATE_CONTENT=true + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_custom_mask.sh b/tests/integration/scenarios/lib_api_custom_mask.sh new file mode 100755 index 0000000..a37fef6 --- /dev/null +++ b/tests/integration/scenarios/lib_api_custom_mask.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# +# Library API: an explicit coredump_filter bitmask is honored by pdWriteDump. +# 0x7f enables all standard mapping types. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="target" +LIBTEST_PATH="dump" +LIBTEST_MASK="0x7f" +EXPECTSUCCESS=true +SHOULDDUMP=true + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_dotnet_dump.sh b/tests/integration/scenarios/lib_api_dotnet_dump.sh new file mode 100755 index 0000000..5696cc0 --- /dev/null +++ b/tests/integration/scenarios/lib_api_dotnet_dump.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# +# Library API: basic on-demand dump of a live .NET process (TestWebApi) +# via pdWriteDump. Verifies the library can dump a managed/.NET target, +# not just native processes. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +DRIVERPATH=$(readlink -m "$DIR/../../../ProcDumpLibTestDriver"); +TESTWEBAPIPATH=$(readlink -m "$DIR/../TestWebApi"); +HELPERS=$(readlink -m "$DIR/../helpers.sh"); + +source $HELPERS + +if [ ! -x "$DRIVERPATH" ]; then + echo "[libtest] ERROR: driver not found or not executable: $DRIVERPATH" + exit 1 +fi + +# Clean up dumps from a prior run +rm -rf /tmp/libdumpnet_* +dumpDir=$(mktemp -d -t libdumpnet_XXXXXX) + +pushd . +cd $TESTWEBAPIPATH +rm -rf *TestWebApi_*Exception* +dotnet run --urls=http://localhost:5033 & + +# Wait until TestWebApi is ready to service requests +waitforurl http://localhost:5033/throwinvalidoperation +if [ $? -eq -1 ]; then + pkill -9 TestWebApi + popd + exit 1 +fi + +TESTCHILDPID=$(ps -o pid= -C "TestWebApi" | tr -d ' ') +echo "TestWebApi PID: $TESTCHILDPID" +if [ -z "$TESTCHILDPID" ]; then + echo "[libtest] FAIL: could not determine TestWebApi pid" + pkill -9 TestWebApi + popd + exit 1 +fi + +dumpPath="$dumpDir/core" +echo [`date +"%T.%3N"`] Running: "$DRIVERPATH" "$TESTCHILDPID" "$dumpPath" default 1 +sudo "$DRIVERPATH" "$TESTCHILDPID" "$dumpPath" default 1 +rc=$? +echo "[libtest] driver exit code: $rc" + +popd +pkill -9 TestWebApi + +result=0 + +# 1. The driver must report success. +if [ "$rc" -ne 0 ]; then + echo "[libtest] FAIL: expected success but driver returned $rc" + result=1 +else + echo "[libtest] PASS: driver returned success" +fi + +# 2. A non-trivial dump file must have been produced. For .NET targets the +# runtime's createdump writes to the literal path, while the native engine +# appends "."; accept either by locating the file in the dump dir. +producedDump=$(find "$dumpDir" -mindepth 1 -maxdepth 1 -type f -print -quit) +if [ -n "$producedDump" ] && [ -f "$producedDump" ]; then + dumpSize=$(stat -c%s "$producedDump") + echo "[libtest] dump produced: $producedDump (${dumpSize} bytes)" + if [ "$dumpSize" -gt 1024 ]; then + echo "[libtest] PASS: dump file is non-trivial" + else + echo "[libtest] FAIL: dump file is too small (${dumpSize} bytes)" + result=1 + fi +else + echo "[libtest] FAIL: no dump file found in $dumpDir" + result=1 +fi + +exit $result diff --git a/tests/integration/scenarios/lib_api_empty_path.sh b/tests/integration/scenarios/lib_api_empty_path.sh new file mode 100755 index 0000000..56f0faa --- /dev/null +++ b/tests/integration/scenarios/lib_api_empty_path.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Library API: an empty dumpPath is rejected with an error. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="target" +LIBTEST_PATH="EMPTY" +EXPECTSUCCESS=false +SHOULDDUMP=false + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_invalid_pid.sh b/tests/integration/scenarios/lib_api_invalid_pid.sh new file mode 100755 index 0000000..f4130ed --- /dev/null +++ b/tests/integration/scenarios/lib_api_invalid_pid.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Library API: a negative processId is rejected with an error. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="invalid" +LIBTEST_PATH="dump" +EXPECTSUCCESS=false +SHOULDDUMP=false + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_nonexistent_pid.sh b/tests/integration/scenarios/lib_api_nonexistent_pid.sh new file mode 100755 index 0000000..9aa2027 --- /dev/null +++ b/tests/integration/scenarios/lib_api_nonexistent_pid.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Library API: dumping a process id that is not running fails gracefully. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="dead" +LIBTEST_PATH="dump" +EXPECTSUCCESS=false +SHOULDDUMP=false + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_null_path.sh b/tests/integration/scenarios/lib_api_null_path.sh new file mode 100755 index 0000000..510f975 --- /dev/null +++ b/tests/integration/scenarios/lib_api_null_path.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Library API: a NULL dumpPath is rejected with an error. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="target" +LIBTEST_PATH="NULL" +EXPECTSUCCESS=false +SHOULDDUMP=false + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_overwrite_false.sh b/tests/integration/scenarios/lib_api_overwrite_false.sh new file mode 100755 index 0000000..bd19dae --- /dev/null +++ b/tests/integration/scenarios/lib_api_overwrite_false.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Library API: with bOverwrite=false pdWriteDump fails when the dump file +# already exists, leaving the existing file untouched. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="target" +LIBTEST_PATH="dump" +LIBTEST_OVERWRITE=0 +PRECREATE_DUMP=true +EXPECTSUCCESS=false +SHOULDDUMP=false + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_overwrite_true.sh b/tests/integration/scenarios/lib_api_overwrite_true.sh new file mode 100755 index 0000000..73a4cd6 --- /dev/null +++ b/tests/integration/scenarios/lib_api_overwrite_true.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# +# Library API: with bOverwrite=true an existing dump file is replaced. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="target" +LIBTEST_PATH="dump" +LIBTEST_OVERWRITE=1 +PRECREATE_DUMP=true +EXPECTSUCCESS=true +SHOULDDUMP=true + +runLibTestAndValidate diff --git a/tests/integration/scenarios/lib_api_zero_pid.sh b/tests/integration/scenarios/lib_api_zero_pid.sh new file mode 100755 index 0000000..eb94cca --- /dev/null +++ b/tests/integration/scenarios/lib_api_zero_pid.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Library API: a zero processId is rejected with an error. +# +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +runLibTestAndValidate=$(readlink -m "$DIR/../runLibTestAndValidate.sh"); +source $runLibTestAndValidate + +LIBTEST_PID="zero" +LIBTEST_PATH="dump" +EXPECTSUCCESS=false +SHOULDDUMP=false + +runLibTestAndValidate