From 0a5496dcc76b610b341ee12d2164b3559fcdd7a8 Mon Sep 17 00:00:00 2001 From: Mario Hewardt Date: Thu, 25 Jun 2026 06:02:30 -0700 Subject: [PATCH 1/3] Initial library commit --- CMakeLists.txt | 50 +++++++++++- dist/DEBIAN.in/control-dev.in | 11 +++ dist/SPECS.in/spec-dev.in | 29 +++++++ include/CoreDumpWriter.h | 1 + lib/ProcDumpLib.cpp | 143 ++++++++++++++++++++++++++++++++++ lib/ProcDumpLib.h | 68 ++++++++++++++++ lib/sample.cpp | 43 ++++++++++ makePackages.sh | 55 +++++++++++++ src/CoreDumpWriter.cpp | 59 +++++++++++++- 9 files changed, 452 insertions(+), 7 deletions(-) create mode 100644 dist/DEBIAN.in/control-dev.in create mode 100644 dist/SPECS.in/spec-dev.in create mode 100644 lib/ProcDumpLib.cpp create mode 100644 lib/ProcDumpLib.h create mode 100644 lib/sample.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b1ae451c..cd341c66 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) @@ -180,6 +182,8 @@ configure_file(${procdump_INC}/ProcDumpVersion.h.in ${PROJECT_BINARY_DIR}/ProcDu if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") configure_file(dist/DEBIAN.in/control.in DEBIANcontrol) configure_file(dist/SPECS.in/spec.in SPECS.spec) + configure_file(dist/DEBIAN.in/control-dev.in DEBIANcontrol-dev) + configure_file(dist/SPECS.in/spec-dev.in SPECS.spec-dev) endif() # @@ -192,7 +196,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 +210,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 +222,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 +240,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 +282,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 +299,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() @@ -311,6 +343,18 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") COMMAND "${CMAKE_SOURCE_DIR}/makePackages.sh" "${CMAKE_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" "${PACKAGE_NAME}" "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" "0" "rpm" DEPENDS "${CMAKE_SOURCE_DIR}/dist" "${PROJECT_BINARY_DIR}/procdump" ) + + # Development packages: static library (libprocdump.a) + public header. + # Debian convention uses a "-dev" suffix; RPM uses "-devel". + add_custom_target(deb-dev + COMMAND "${CMAKE_SOURCE_DIR}/makePackages.sh" "${CMAKE_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" "${PACKAGE_NAME}-dev" "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" "0" "deb-dev" "${ARCHITECTURE}" + DEPENDS "${CMAKE_SOURCE_DIR}/dist" procdumplib + ) + + add_custom_target(rpm-dev + COMMAND "${CMAKE_SOURCE_DIR}/makePackages.sh" "${CMAKE_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" "${PACKAGE_NAME}-devel" "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" "0" "rpm-dev" + DEPENDS "${CMAKE_SOURCE_DIR}/dist" procdumplib + ) else() add_custom_target(brew COMMAND "${CMAKE_SOURCE_DIR}/makePackages.sh" "${CMAKE_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" "${PACKAGE_NAME}" "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" "0" "brew" diff --git a/dist/DEBIAN.in/control-dev.in b/dist/DEBIAN.in/control-dev.in new file mode 100644 index 00000000..99e3963c --- /dev/null +++ b/dist/DEBIAN.in/control-dev.in @@ -0,0 +1,11 @@ +Package: procdump-dev +Version: @PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@ +Architecture: @ARCHITECTURE@ +Maintainer: Sysinternals +Section: libdevel +Description: Development files for ProcDump (static library and headers) + This package contains the static library (libprocdump.a) and C header + (ProcDumpLib.h) used to link against ProcDump and generate process core + dumps on demand from your own programs. +Depends: zlib1g +License: MIT diff --git a/dist/SPECS.in/spec-dev.in b/dist/SPECS.in/spec-dev.in new file mode 100644 index 00000000..3e886685 --- /dev/null +++ b/dist/SPECS.in/spec-dev.in @@ -0,0 +1,29 @@ +Name: procdump-devel +Version: @PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@ +Release: @PROJECT_VERSION_TWEAK@%{?dist} +Summary: Development files for ProcDump (static library and headers) + +License: MIT +URL: https://github.com/Microsoft/ProcDump-for-Linux + +%description +This package contains the static library (libprocdump.a) and C header +(ProcDumpLib.h) used to link against ProcDump and generate process core +dumps on demand from your own programs. + +%install +rm -rf $RPM_BUILD_ROOT +mkdir -p $RPM_BUILD_ROOT/%{_libdir} +cp libprocdump.a $RPM_BUILD_ROOT/%{_libdir} +mkdir -p $RPM_BUILD_ROOT/%{_includedir}/procdump +cp ProcDumpLib.h $RPM_BUILD_ROOT/%{_includedir}/procdump + +%clean +rm -rf $RPM_BUILD_ROOT + +%files +%{_libdir}/libprocdump.a +%{_includedir}/procdump/ProcDumpLib.h + +%changelog +@CHANGE_LOG@ diff --git a/include/CoreDumpWriter.h b/include/CoreDumpWriter.h index f1d8eae9..e9919553 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 00000000..06e922d3 --- /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 00000000..e2ad5eb0 --- /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 00000000..58bcb7d6 --- /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/makePackages.sh b/makePackages.sh index cf827eee..126d9cff 100755 --- a/makePackages.sh +++ b/makePackages.sh @@ -74,6 +74,34 @@ if [ "$PACKAGE_TYPE" = "deb" ]; then exit 0 fi +if [ "$PACKAGE_TYPE" = "deb-dev" ]; then + DPKGDEB=`which dpkg-deb` + + if [ -d "${PROJECT_BINARY_DIR}/deb-dev" ]; then + rm -rf "${PROJECT_BINARY_DIR}/deb-dev" + fi + + # copy dev deb files (static library + public header) + mkdir -p "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/DEBIAN" + cp "${PROJECT_BINARY_DIR}/DEBIANcontrol-dev" "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/DEBIAN/control" + mkdir -p "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/usr/lib" + cp "${PROJECT_BINARY_DIR}/libprocdump.a" "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/usr/lib/" + mkdir -p "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/usr/include/procdump" + cp "${CMAKE_SOURCE_DIR}/lib/ProcDumpLib.h" "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/usr/include/procdump/" + + # make the deb + if [ "$DPKGDEB" != "" ]; then + cd "${PROJECT_BINARY_DIR}/deb-dev" + "$DPKGDEB" -Zxz --build --root-owner-group "${DEB_PACKAGE_NAME}" + RET=$? + else + echo "No dpkg-deb found" + RET=1 + fi + + exit $RET +fi + if [ "$PACKAGE_TYPE" = "rpm" ]; then RPMBUILD=`which rpmbuild` @@ -99,6 +127,33 @@ if [ "$PACKAGE_TYPE" = "rpm" ]; then fi fi +if [ "$PACKAGE_TYPE" = "rpm-dev" ]; then + RPMBUILD=`which rpmbuild` + + if [ -d "${PROJECT_BINARY_DIR}/rpm-dev" ]; then + rm -rf "${PROJECT_BINARY_DIR}/rpm-dev" + fi + + # copy dev rpm files (static library + public header) + mkdir -p "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}/SPECS" + cp -a "${PROJECT_BINARY_DIR}/SPECS.spec-dev" "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}/SPECS/${RPM_PACKAGE_NAME}.spec" + mkdir -p "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}/BUILD/" + cp "${PROJECT_BINARY_DIR}/libprocdump.a" "${CMAKE_SOURCE_DIR}/lib/ProcDumpLib.h" "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}/BUILD/" + + # make the rpm + if [ "$RPMBUILD" != "" ]; then + cd "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}" + "$RPMBUILD" --define "_topdir `pwd`" -v -bb "SPECS/${RPM_PACKAGE_NAME}.spec" + RET=$? + cp RPMS/$(uname -m)/*.rpm .. + else + echo "No rpmbuild found" + RET=1 + fi + + exit $RET +fi + if [ "$PACKAGE_TYPE" = "brew" ]; then # create brew package diff --git a/src/CoreDumpWriter.cpp b/src/CoreDumpWriter.cpp index acd752c1..2bbbaa12 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 { From 7b41daddc7d84d603e2b18ab0ccda24c4ad42a6a Mon Sep 17 00:00:00 2001 From: Mario Hewardt Date: Thu, 25 Jun 2026 09:29:03 -0700 Subject: [PATCH 2/3] Fix ARM dump generation bug and add tests for the dump lib --- CMakeLists.txt | 36 ++-- dist/DEBIAN.in/control-dev.in | 11 -- dist/SPECS.in/spec-dev.in | 29 --- makePackages.sh | 55 ------ src/corex/arch/aarch64.c | 21 ++ src/corex/arch/arch.h | 12 ++ src/corex/arch/x86_64.c | 8 + src/corex/note_builder.c | 17 ++ src/corex/ptrace_utils.c | 4 + src/corex/ptrace_utils.h | 2 + tests/integration/ProcDumpLibTestDriver.cpp | 101 ++++++++++ tests/integration/runLibTestAndValidate.sh | 187 ++++++++++++++++++ .../scenarios/lib_api_bad_directory.sh | 14 ++ .../scenarios/lib_api_basic_dump.sh | 17 ++ .../scenarios/lib_api_custom_mask.sh | 16 ++ .../scenarios/lib_api_dotnet_dump.sh | 82 ++++++++ .../scenarios/lib_api_empty_path.sh | 14 ++ .../scenarios/lib_api_invalid_pid.sh | 14 ++ .../scenarios/lib_api_nonexistent_pid.sh | 14 ++ .../scenarios/lib_api_null_path.sh | 14 ++ .../scenarios/lib_api_overwrite_false.sh | 17 ++ .../scenarios/lib_api_overwrite_true.sh | 16 ++ .../integration/scenarios/lib_api_zero_pid.sh | 14 ++ 23 files changed, 606 insertions(+), 109 deletions(-) delete mode 100644 dist/DEBIAN.in/control-dev.in delete mode 100644 dist/SPECS.in/spec-dev.in create mode 100644 tests/integration/ProcDumpLibTestDriver.cpp create mode 100755 tests/integration/runLibTestAndValidate.sh create mode 100755 tests/integration/scenarios/lib_api_bad_directory.sh create mode 100755 tests/integration/scenarios/lib_api_basic_dump.sh create mode 100755 tests/integration/scenarios/lib_api_custom_mask.sh create mode 100755 tests/integration/scenarios/lib_api_dotnet_dump.sh create mode 100755 tests/integration/scenarios/lib_api_empty_path.sh create mode 100755 tests/integration/scenarios/lib_api_invalid_pid.sh create mode 100755 tests/integration/scenarios/lib_api_nonexistent_pid.sh create mode 100755 tests/integration/scenarios/lib_api_null_path.sh create mode 100755 tests/integration/scenarios/lib_api_overwrite_false.sh create mode 100755 tests/integration/scenarios/lib_api_overwrite_true.sh create mode 100755 tests/integration/scenarios/lib_api_zero_pid.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index cd341c66..648ff63c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,8 +182,6 @@ configure_file(${procdump_INC}/ProcDumpVersion.h.in ${PROJECT_BINARY_DIR}/ProcDu if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") configure_file(dist/DEBIAN.in/control.in DEBIANcontrol) configure_file(dist/SPECS.in/spec.in SPECS.spec) - configure_file(dist/DEBIAN.in/control-dev.in DEBIANcontrol-dev) - configure_file(dist/SPECS.in/spec-dev.in SPECS.spec-dev) endif() # @@ -329,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) # @@ -343,18 +363,6 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") COMMAND "${CMAKE_SOURCE_DIR}/makePackages.sh" "${CMAKE_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" "${PACKAGE_NAME}" "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" "0" "rpm" DEPENDS "${CMAKE_SOURCE_DIR}/dist" "${PROJECT_BINARY_DIR}/procdump" ) - - # Development packages: static library (libprocdump.a) + public header. - # Debian convention uses a "-dev" suffix; RPM uses "-devel". - add_custom_target(deb-dev - COMMAND "${CMAKE_SOURCE_DIR}/makePackages.sh" "${CMAKE_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" "${PACKAGE_NAME}-dev" "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" "0" "deb-dev" "${ARCHITECTURE}" - DEPENDS "${CMAKE_SOURCE_DIR}/dist" procdumplib - ) - - add_custom_target(rpm-dev - COMMAND "${CMAKE_SOURCE_DIR}/makePackages.sh" "${CMAKE_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" "${PACKAGE_NAME}-devel" "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" "0" "rpm-dev" - DEPENDS "${CMAKE_SOURCE_DIR}/dist" procdumplib - ) else() add_custom_target(brew COMMAND "${CMAKE_SOURCE_DIR}/makePackages.sh" "${CMAKE_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" "${PACKAGE_NAME}" "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" "0" "brew" diff --git a/dist/DEBIAN.in/control-dev.in b/dist/DEBIAN.in/control-dev.in deleted file mode 100644 index 99e3963c..00000000 --- a/dist/DEBIAN.in/control-dev.in +++ /dev/null @@ -1,11 +0,0 @@ -Package: procdump-dev -Version: @PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@ -Architecture: @ARCHITECTURE@ -Maintainer: Sysinternals -Section: libdevel -Description: Development files for ProcDump (static library and headers) - This package contains the static library (libprocdump.a) and C header - (ProcDumpLib.h) used to link against ProcDump and generate process core - dumps on demand from your own programs. -Depends: zlib1g -License: MIT diff --git a/dist/SPECS.in/spec-dev.in b/dist/SPECS.in/spec-dev.in deleted file mode 100644 index 3e886685..00000000 --- a/dist/SPECS.in/spec-dev.in +++ /dev/null @@ -1,29 +0,0 @@ -Name: procdump-devel -Version: @PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@ -Release: @PROJECT_VERSION_TWEAK@%{?dist} -Summary: Development files for ProcDump (static library and headers) - -License: MIT -URL: https://github.com/Microsoft/ProcDump-for-Linux - -%description -This package contains the static library (libprocdump.a) and C header -(ProcDumpLib.h) used to link against ProcDump and generate process core -dumps on demand from your own programs. - -%install -rm -rf $RPM_BUILD_ROOT -mkdir -p $RPM_BUILD_ROOT/%{_libdir} -cp libprocdump.a $RPM_BUILD_ROOT/%{_libdir} -mkdir -p $RPM_BUILD_ROOT/%{_includedir}/procdump -cp ProcDumpLib.h $RPM_BUILD_ROOT/%{_includedir}/procdump - -%clean -rm -rf $RPM_BUILD_ROOT - -%files -%{_libdir}/libprocdump.a -%{_includedir}/procdump/ProcDumpLib.h - -%changelog -@CHANGE_LOG@ diff --git a/makePackages.sh b/makePackages.sh index 126d9cff..cf827eee 100755 --- a/makePackages.sh +++ b/makePackages.sh @@ -74,34 +74,6 @@ if [ "$PACKAGE_TYPE" = "deb" ]; then exit 0 fi -if [ "$PACKAGE_TYPE" = "deb-dev" ]; then - DPKGDEB=`which dpkg-deb` - - if [ -d "${PROJECT_BINARY_DIR}/deb-dev" ]; then - rm -rf "${PROJECT_BINARY_DIR}/deb-dev" - fi - - # copy dev deb files (static library + public header) - mkdir -p "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/DEBIAN" - cp "${PROJECT_BINARY_DIR}/DEBIANcontrol-dev" "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/DEBIAN/control" - mkdir -p "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/usr/lib" - cp "${PROJECT_BINARY_DIR}/libprocdump.a" "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/usr/lib/" - mkdir -p "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/usr/include/procdump" - cp "${CMAKE_SOURCE_DIR}/lib/ProcDumpLib.h" "${PROJECT_BINARY_DIR}/deb-dev/${DEB_PACKAGE_NAME}/usr/include/procdump/" - - # make the deb - if [ "$DPKGDEB" != "" ]; then - cd "${PROJECT_BINARY_DIR}/deb-dev" - "$DPKGDEB" -Zxz --build --root-owner-group "${DEB_PACKAGE_NAME}" - RET=$? - else - echo "No dpkg-deb found" - RET=1 - fi - - exit $RET -fi - if [ "$PACKAGE_TYPE" = "rpm" ]; then RPMBUILD=`which rpmbuild` @@ -127,33 +99,6 @@ if [ "$PACKAGE_TYPE" = "rpm" ]; then fi fi -if [ "$PACKAGE_TYPE" = "rpm-dev" ]; then - RPMBUILD=`which rpmbuild` - - if [ -d "${PROJECT_BINARY_DIR}/rpm-dev" ]; then - rm -rf "${PROJECT_BINARY_DIR}/rpm-dev" - fi - - # copy dev rpm files (static library + public header) - mkdir -p "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}/SPECS" - cp -a "${PROJECT_BINARY_DIR}/SPECS.spec-dev" "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}/SPECS/${RPM_PACKAGE_NAME}.spec" - mkdir -p "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}/BUILD/" - cp "${PROJECT_BINARY_DIR}/libprocdump.a" "${CMAKE_SOURCE_DIR}/lib/ProcDumpLib.h" "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}/BUILD/" - - # make the rpm - if [ "$RPMBUILD" != "" ]; then - cd "${PROJECT_BINARY_DIR}/rpm-dev/${RPM_PACKAGE_NAME}" - "$RPMBUILD" --define "_topdir `pwd`" -v -bb "SPECS/${RPM_PACKAGE_NAME}.spec" - RET=$? - cp RPMS/$(uname -m)/*.rpm .. - else - echo "No rpmbuild found" - RET=1 - fi - - exit $RET -fi - if [ "$PACKAGE_TYPE" = "brew" ]; then # create brew package diff --git a/src/corex/arch/aarch64.c b/src/corex/arch/aarch64.c index 89d6b9f5..828f9f00 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 e1cb8700..db8e30aa 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 b5f38de3..5d0d1ae6 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 0dfff07c..ed9f7e8c 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 364b334b..849b7f8b 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 39f11aa3..e3fcc416 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 00000000..8e04d59a --- /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 00000000..11304abc --- /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 00000000..341998f3 --- /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 00000000..83dc89ff --- /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 00000000..a37fef6e --- /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 00000000..5696cc0d --- /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 00000000..56f0faa9 --- /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 00000000..f4130edd --- /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 00000000..9aa20275 --- /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 00000000..510f9751 --- /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 00000000..bd19dae3 --- /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 00000000..73a4cd6f --- /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 00000000..eb94ccab --- /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 From 17d460c51d14f26422840bbb681c6b1d7a13607b Mon Sep 17 00:00:00 2001 From: Mario Hewardt Date: Thu, 2 Jul 2026 08:59:45 -0700 Subject: [PATCH 3/3] Update title and description in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 897a9234..40b3f169 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")