├── .java-version ├── src ├── test │ ├── resources │ │ └── files │ │ │ ├── HeapDumpSanitizerTest │ │ │ └── classifieds.txt │ │ │ ├── CaptureCommandProcessorTest │ │ │ ├── docker-exec-jcmd-gc-heap-dump.txt │ │ │ ├── docker-exec-jps.txt │ │ │ ├── docker-ps-none.txt │ │ │ └── docker-ps.txt │ │ │ ├── PrivilegeEscalatorTest │ │ │ ├── native-cgroup.txt │ │ │ └── docker-cgroup.txt │ │ │ ├── ApplicationTest │ │ │ └── help.txt │ │ │ └── SanitizeStreamFactoryTest │ │ │ └── sample.tar │ └── java │ │ └── com │ │ └── paypal │ │ └── heapdumptool │ │ ├── fixture │ │ ├── ConstructorTester.java │ │ ├── ResourceTool.java │ │ ├── MockitoTool.java │ │ ├── HeapDumper.java │ │ └── ByteArrayTool.java │ │ ├── hserr │ │ ├── SanitizeHserrCommandTest.java │ │ └── SanitizeHserrCommandProcessorTest.java │ │ ├── utils │ │ ├── DateTimeToolTest.java │ │ ├── ProcessToolTest.java │ │ ├── CallableToolTest.java │ │ └── ProgressMonitorTest.java │ │ ├── cli │ │ ├── CliCommandTest.java │ │ └── CliBootstrapTest.java │ │ ├── sanitizer │ │ ├── BasicTypeTest.java │ │ ├── SanitizeCommandTest.java │ │ ├── example │ │ │ └── SimpleClass.java │ │ ├── SanitizeCommandProcessorTest.java │ │ ├── SanitizeStreamFactoryTest.java │ │ ├── PipeTest.java │ │ ├── DataSizeTests.java │ │ └── HeapDumpSanitizerTest.java │ │ ├── capture │ │ ├── CaptureCommandTest.java │ │ ├── CaptureStreamFactoryTest.java │ │ ├── PrivilegeEscalatorTest.java │ │ └── CaptureCommandProcessorTest.java │ │ ├── ApplicationTestSupport.java │ │ └── ApplicationTest.java └── main │ ├── java │ └── com │ │ └── paypal │ │ └── heapdumptool │ │ ├── cli │ │ ├── CliCommandProcessor.java │ │ ├── CliCommand.java │ │ └── CliBootstrap.java │ │ ├── utils │ │ ├── CallableTool.java │ │ ├── DateTimeTool.java │ │ ├── ProcessTool.java │ │ ├── ProgressMonitor.java │ │ └── InternalLogger.java │ │ ├── sanitizer │ │ ├── Field.java │ │ ├── ClassObject.java │ │ ├── HeapRecord.java │ │ ├── BasicType.java │ │ ├── SanitizeCommand.java │ │ ├── DataUnit.java │ │ ├── SanitizeStreamFactory.java │ │ ├── SanitizeCommandProcessor.java │ │ ├── Pipe.java │ │ ├── SanitizeOrCaptureCommandBase.java │ │ └── DataSize.java │ │ ├── capture │ │ ├── CaptureStreamFactory.java │ │ ├── CaptureCommand.java │ │ ├── PrivilegeEscalator.java │ │ └── CaptureCommandProcessor.java │ │ ├── hserr │ │ ├── SanitizeHserrCommand.java │ │ └── SanitizeHserrCommandProcessor.java │ │ └── Application.java │ ├── docker │ └── docker-entrypoint.sh │ ├── c │ └── nsenter1.c │ └── resources │ └── privilege-escalate.sh.tmpl ├── CHANGES.md ├── .github └── dependabot.yml ├── .gitignore ├── Dockerfile ├── NOTICE ├── README.md └── LICENSE /.java-version: -------------------------------------------------------------------------------- 1 | 1.8 2 | -------------------------------------------------------------------------------- /src/test/resources/files/HeapDumpSanitizerTest/classifieds.txt: -------------------------------------------------------------------------------- 1 | his-classified-value -------------------------------------------------------------------------------- /src/test/resources/files/CaptureCommandProcessorTest/docker-exec-jcmd-gc-heap-dump.txt: -------------------------------------------------------------------------------- 1 | Heap dump file created -------------------------------------------------------------------------------- /src/test/resources/files/CaptureCommandProcessorTest/docker-exec-jps.txt: -------------------------------------------------------------------------------- 1 | 55 MyApplication 2 | 2839 Jps 3 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/cli/CliCommandProcessor.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.cli; 2 | 3 | public interface CliCommandProcessor { 4 | 5 | void process() throws Exception; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/files/CaptureCommandProcessorTest/docker-ps-none.txt: -------------------------------------------------------------------------------- 1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2 | -------------------------------------------------------------------------------- /src/main/docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | TMPDIR="${TMPDIR:-/tmp/}" 5 | 6 | # don't use JAVA_TOOL_OPTIONS to avoid JVM's "Picked up ..." message messing up piping to bash 7 | exec java -Djava.io.tmpdir=$TMPDIR ${JAVA_OPTS:-} -jar $APP_JAR $* 8 | -------------------------------------------------------------------------------- /src/test/resources/files/PrivilegeEscalatorTest/native-cgroup.txt: -------------------------------------------------------------------------------- 1 | 12:rdma:/ 2 | 11:freezer:/ 3 | 10:memory:/ 4 | 9:pids:/ 5 | 8:perf_event:/ 6 | 7:cpuset:/ 7 | 6:cpu,cpuacct:/ 8 | 5:blkio:/ 9 | 4:devices:/ 10 | 3:hugetlb:/ 11 | 2:net_cls,net_prio:/ 12 | 1:name=systemd:/init.scope 13 | 0::/init.scope 14 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.1.1 4 | - Upgrade to latest logback through spring-boot bom upgrade. 5 | - Also upgrade various other dependencies. 6 | 7 | ## 1.1.0 8 | - Add ability to sanitize hs_err Java fatal error logs 9 | 10 | ## 1.0.0 11 | - Public release of tool for capturing sanitized Java heap dumps 12 | -------------------------------------------------------------------------------- /src/test/resources/files/CaptureCommandProcessorTest/docker-ps.txt: -------------------------------------------------------------------------------- 1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2 | 06e633da3494 docker-registry.example.com/apps/my-app:latest "/docker-entrypoint.…" 2 days ago Up 17 hours 0.0.0.0:1234->1234/tcp my-app -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/cli/CliCommand.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.cli; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | public interface CliCommand extends Callable { 6 | 7 | @Override 8 | default Boolean call() throws Exception { 9 | return CliBootstrap.runCommand(this); 10 | } 11 | 12 | Class getProcessorClass(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/utils/CallableTool.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | public class CallableTool { 6 | 7 | public static T callQuietlyWithDefault(final T defaultValue, final Callable callable) { 8 | try { 9 | return callable.call(); 10 | } catch (final Exception ignore) { 11 | return defaultValue; 12 | } 13 | } 14 | 15 | private CallableTool() { 16 | throw new AssertionError(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/files/ApplicationTest/help.txt: -------------------------------------------------------------------------------- 1 | Usage: heap-dump-tool [-hV] [COMMAND] 2 | Tool primarily for capturing or sanitizing heap dumps 3 | -h, --help Show this help message and exit. 4 | -V, --version Print version information and exit. 5 | Commands: 6 | capture Capture sanitized heap dump of a containerized app 7 | sanitize Sanitize a heap dump by replacing byte and char array contents 8 | sanitize-hserr Sanitize fatal error log by censoring environment variable values 9 | help Display help information about the specified command. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | 3 | *.java.hsp 4 | *.sonarj 5 | *.sw* 6 | .DS_Store 7 | .settings 8 | .springBeans 9 | bin 10 | build.sh 11 | ivy-cache 12 | jxl.log 13 | jmx.log 14 | derby.log 15 | .gradle 16 | 17 | classes/ 18 | /build 19 | 20 | # Eclipse artifacts, including WTP generated manifests 21 | .classpath 22 | .project 23 | */src/main/java/META-INF/MANIFEST.MF 24 | .factorypath 25 | 26 | # IDEA artifacts and output dirs 27 | *.iml 28 | *.ipr 29 | *.iws 30 | .idea 31 | out 32 | test-output 33 | .gradletasknamecache 34 | 35 | /.apt_generated/ 36 | /.apt_generated_tests/ 37 | 38 | *.jar 39 | *.zip 40 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/Field.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString; 4 | import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE; 5 | 6 | public class Field { 7 | 8 | public final String name; 9 | public final BasicType type; 10 | 11 | public Field(final String name, final BasicType type) { 12 | this.name = name; 13 | this.type = type; 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | return reflectionToString(this, MULTI_LINE_STYLE); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/fixture/ConstructorTester.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.fixture; 2 | 3 | import java.lang.reflect.Constructor; 4 | 5 | import static org.assertj.core.api.Assertions.assertThatCode; 6 | 7 | public class ConstructorTester { 8 | 9 | public static void test(final Class clazz) throws Exception { 10 | final Constructor constructor = clazz.getDeclaredConstructor(); 11 | constructor.setAccessible(true); 12 | assertThatCode(constructor::newInstance).hasRootCauseInstanceOf(AssertionError.class); 13 | } 14 | 15 | private ConstructorTester() { 16 | throw new AssertionError(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/hserr/SanitizeHserrCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.hserr; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.meanbean.test.BeanVerifier; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class SanitizeHserrCommandTest { 9 | 10 | @Test 11 | public void testBean() { 12 | BeanVerifier.forClass(SanitizeHserrCommand.class) 13 | .verifyGettersAndSetters() 14 | .verifyToString(); 15 | 16 | assertThat(new SanitizeHserrCommand().getProcessorClass()) 17 | .isEqualTo(SanitizeHserrCommandProcessor.class); 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM amazoncorretto:8 AS builder 2 | 3 | # build nsenter1 in another stage 4 | FROM --platform=$BUILDPLATFORM ubuntu AS nsenter1 5 | RUN apt update \ 6 | && apt install gcc libc6-dev -y 7 | COPY src/main/c/nsenter1.c ./ 8 | RUN gcc -Wall -static nsenter1.c -o /usr/bin/nsenter1 9 | 10 | # go back to original state and copy nsenter 11 | FROM builder 12 | COPY --from=nsenter1 /usr/bin/nsenter1 /usr/bin/nsenter1 13 | 14 | WORKDIR /tmp/ 15 | 16 | ENV APP_ID=heap-dump-tool 17 | ENV APP_JAR=/opt/heap-dump-tool/$APP_ID.jar 18 | COPY src/main/docker/docker-entrypoint.sh / 19 | COPY target/$APP_ID.jar $APP_JAR 20 | 21 | RUN chmod ugo+x /docker-entrypoint.sh 22 | 23 | ENTRYPOINT ["/docker-entrypoint.sh"] 24 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/utils/DateTimeToolTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.time.Instant; 6 | 7 | import static com.paypal.heapdumptool.utils.DateTimeTool.getFriendlyDuration; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | public class DateTimeToolTest { 11 | 12 | @Test 13 | public void testFriendlyDuration() { 14 | final Instant start = Instant.now().minusSeconds(65); 15 | 16 | assertThat(getFriendlyDuration(start)) 17 | .satisfiesAnyOf(display -> assertThat(display).isEqualTo("1m5s"), 18 | display -> assertThat(display).isEqualTo("1m6s")); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/ClassObject.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString; 7 | import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE; 8 | 9 | public class ClassObject { 10 | 11 | public final long id; 12 | public final long superClassObjectId; 13 | public final List fields = new ArrayList<>(); 14 | 15 | public ClassObject(final long id, final long superClassObjectId) { 16 | this.id = id; 17 | this.superClassObjectId = superClassObjectId; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return reflectionToString(this, MULTI_LINE_STYLE); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/utils/DateTimeTool.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import java.time.Duration; 4 | import java.time.Instant; 5 | 6 | import static java.time.temporal.ChronoUnit.SECONDS; 7 | import static java.util.Locale.ENGLISH; 8 | 9 | public class DateTimeTool { 10 | 11 | public static String getFriendlyDuration(final Instant start) { 12 | final Instant startSeconds = start.truncatedTo(SECONDS); 13 | final Instant endSeconds = Instant.now().truncatedTo(SECONDS); 14 | 15 | final Duration duration = Duration.between(startSeconds, endSeconds); 16 | return duration.toString() 17 | .substring(2) 18 | .toLowerCase(ENGLISH); 19 | } 20 | 21 | private DateTimeTool() { 22 | throw new AssertionError(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/cli/CliCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.cli; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.mockito.MockedStatic; 5 | 6 | import static org.mockito.ArgumentMatchers.any; 7 | import static org.mockito.Mockito.doCallRealMethod; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.mockStatic; 10 | 11 | public class CliCommandTest { 12 | 13 | @Test 14 | public void testCall() throws Exception { 15 | final CliCommand command = mock(CliCommand.class); 16 | doCallRealMethod() 17 | .when(command) 18 | .call(); 19 | 20 | try (final MockedStatic mocked = mockStatic(CliBootstrap.class)) { 21 | command.call(); 22 | 23 | mocked.verify(() -> CliBootstrap.runCommand(any())); 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Heap Dump Tool 2 | Copyright 2020 PayPal Inc. 3 | 4 | This product includes software from Apache Commons Compress (Apache 2.0) 5 | https://github.com/apache/commons-compress 6 | 7 | This product includes software from Apache Commons IO (Apache 2.0) 8 | https://github.com/apache/commons-io 9 | 10 | This product includes software from Apache Commons Lang (Apache 2.0) 11 | https://github.com/apache/commons-lang 12 | 13 | This product includes software from Apache Commons Text (Apache 2.0) 14 | https://github.com/apache/commons-text 15 | 16 | This product includes software from Logback (LGPL) 17 | https://github.com/qos-ch/logback 18 | 19 | This product includes software from picocli (Apache 2.0) 20 | https://github.com/remkop/picocli 21 | 22 | This product includes software from slf4j project (MIT) 23 | https://github.com/qos-ch/slf4j 24 | 25 | This product includes software from spring-boot project (Apache 2.0) 26 | https://github.com/spring-projects/spring-boot 27 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/sanitizer/BasicTypeTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.EnumSource; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 9 | 10 | public class BasicTypeTest { 11 | 12 | @ParameterizedTest 13 | @EnumSource(BasicType.class) 14 | public void testFindValueSize(final BasicType basicType) { 15 | assertThat(BasicType.findValueSize(basicType.getU1Code(), 8)) 16 | .isGreaterThan(0); 17 | } 18 | 19 | @Test 20 | public void testUnknownU1Tag() { 21 | assertThatIllegalArgumentException() 22 | .isThrownBy(() -> BasicType.findValueSize(0, 0)) 23 | .withMessage("Unknown basic type code: 0"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/resources/files/PrivilegeEscalatorTest/docker-cgroup.txt: -------------------------------------------------------------------------------- 1 | 12:rdma:/ 2 | 11:freezer:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 3 | 10:memory:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 4 | 9:pids:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 5 | 8:perf_event:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 6 | 7:cpuset:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 7 | 6:cpu,cpuacct:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 8 | 5:blkio:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 9 | 4:devices:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 10 | 3:hugetlb:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 11 | 2:net_cls,net_prio:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 12 | 1:name=systemd:/docker/06e633da349426036456b60b5661b185b4908390b4a9b14b56a45e070158d5b1 13 | 0::/system.slice/containerd.service 14 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/fixture/ResourceTool.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.fixture; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | 5 | import java.io.IOException; 6 | 7 | import static java.nio.charset.StandardCharsets.UTF_8; 8 | 9 | public class ResourceTool { 10 | 11 | public static String contentOf(final Class testClass, final String resourceName) throws IOException { 12 | final String fqPath = getFqPath(testClass, resourceName); 13 | return IOUtils.resourceToString(fqPath, UTF_8); 14 | } 15 | 16 | public static byte[] bytesOf(final Class testClass, final String resourceName) throws IOException { 17 | final String fqPath = getFqPath(testClass, resourceName); 18 | return IOUtils.resourceToByteArray(fqPath); 19 | } 20 | 21 | private static String getFqPath(final Class testClass, final String resourceName) { 22 | return String.format("/files/%s/%s", testClass.getSimpleName(), resourceName); 23 | } 24 | 25 | private ResourceTool() { 26 | throw new AssertionError(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/cli/CliBootstrap.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.cli; 2 | 3 | import com.paypal.heapdumptool.Application; 4 | import org.apache.commons.lang3.exception.ExceptionUtils; 5 | 6 | import java.lang.reflect.InvocationTargetException; 7 | 8 | import static org.apache.commons.lang3.reflect.ConstructorUtils.invokeConstructor; 9 | 10 | public class CliBootstrap { 11 | 12 | public static boolean runCommand(final T command) throws Exception { 13 | 14 | Application.printVersion(); 15 | 16 | final Class clazz = command.getProcessorClass(); 17 | try { 18 | final CliCommandProcessor processor = invokeConstructor(clazz, command); 19 | 20 | processor.process(); 21 | } catch (final InvocationTargetException e) { 22 | if (e.getCause() != null) { 23 | ExceptionUtils.asRuntimeException(e.getCause()); 24 | } 25 | throw e; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | private CliBootstrap() { 32 | throw new AssertionError(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/utils/ProcessToolTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import com.paypal.heapdumptool.fixture.ConstructorTester; 4 | import com.paypal.heapdumptool.utils.ProcessTool.ProcessResult; 5 | import org.apache.commons.io.input.BrokenInputStream; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.io.UncheckedIOException; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatCode; 12 | 13 | public class ProcessToolTest { 14 | 15 | @Test 16 | public void testRun() throws Exception { 17 | final ProcessResult result = ProcessTool.run("ls", "-l"); 18 | assertThat(result.exitCode).isEqualTo(0); 19 | assertThat(result.stdout).isNotEmpty(); 20 | assertThat(result.stderr).isEmpty(); 21 | assertThat(result.stdoutLines()).isNotEmpty(); 22 | } 23 | 24 | @Test 25 | public void testBrokenInputStream() { 26 | assertThatCode(() -> ProcessTool.readStream(new BrokenInputStream())) 27 | .isInstanceOf(UncheckedIOException.class); 28 | } 29 | 30 | @Test 31 | public void testConstructor() throws Exception { 32 | ConstructorTester.test(ProcessTool.class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/fixture/MockitoTool.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.fixture; 2 | 3 | import org.mockito.MockedConstruction; 4 | import org.mockito.stubbing.Answer; 5 | 6 | import java.util.concurrent.Callable; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.mockito.Mockito.mock; 10 | 11 | public class MockitoTool { 12 | 13 | /** 14 | * For more conveniently mocking generic parametrized types without eliciting compiler warnings 15 | */ 16 | @SuppressWarnings("unchecked") 17 | public static T genericMock(final Class clazz) { 18 | return (T) mock(clazz); 19 | } 20 | 21 | public static Answer voidAnswer() { 22 | return voidAnswer(() -> null); 23 | } 24 | 25 | public static Answer voidAnswer(final Callable callable) { 26 | return invocation -> { 27 | callable.call(); 28 | return null; 29 | }; 30 | } 31 | 32 | public static T firstInstance(final MockedConstruction mocked) { 33 | assertThat(mocked.constructed()).hasSizeGreaterThan(1); 34 | return mocked.constructed().get(1); 35 | } 36 | 37 | private MockitoTool() { 38 | throw new AssertionError(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/HeapRecord.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import java.util.Arrays; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import java.util.function.Function; 7 | import java.util.stream.Collectors; 8 | 9 | public enum HeapRecord { 10 | 11 | STRING_IN_UTF8(0x01), 12 | LOAD_CLASS(0x02), 13 | UNLOAD_CLASS(0x03), 14 | STACK_FRAME(0x04), 15 | STACK_TRACE(0x05), 16 | ALLOC_SITES(0x06), 17 | HEAP_SUMMARY(0x07), 18 | START_THREAD(0x0A), 19 | END_THREAD(0x0B), 20 | HEAP_DUMP(0x0C), 21 | HEAP_DUMP_SEGMENT(0x1C), 22 | HEAP_DUMP_END(0x2C), 23 | CPU_SAMPLES(0x0D), 24 | CONTROL_SETTINGS(0x0E), 25 | ; 26 | 27 | private final int tag; 28 | 29 | private static final Map tagToHeapRecord = Arrays.stream(HeapRecord.values()) 30 | .collect(Collectors.toMap(HeapRecord::getTag, Function.identity())); 31 | 32 | HeapRecord(final int tag) { 33 | this.tag = tag; 34 | } 35 | 36 | public int getTag() { 37 | return tag; 38 | } 39 | 40 | public static HeapRecord findByTag(final int tag) { 41 | final HeapRecord heapRecord = tagToHeapRecord.get(tag); 42 | Objects.requireNonNull(heapRecord); 43 | return heapRecord; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/utils/CallableToolTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import com.paypal.heapdumptool.fixture.ConstructorTester; 4 | import org.junit.jupiter.api.DynamicTest; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.TestFactory; 7 | 8 | import static com.paypal.heapdumptool.utils.CallableTool.callQuietlyWithDefault; 9 | import static java.util.Objects.requireNonNull; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class CallableToolTest { 13 | 14 | @TestFactory 15 | public DynamicTest[] callQuietlyWithDefaultTests() { 16 | return new DynamicTest[] { 17 | 18 | DynamicTest.dynamicTest("Happy Path", () -> { 19 | 20 | final int result = callQuietlyWithDefault(5, () -> 1); 21 | assertThat(result).isEqualTo(1); 22 | }), 23 | 24 | DynamicTest.dynamicTest("Unhappy Path", () -> { 25 | 26 | final int result = callQuietlyWithDefault(5, () -> requireNonNull(null)); 27 | assertThat(result).isEqualTo(5); 28 | }), 29 | }; 30 | } 31 | 32 | @Test 33 | public void testConstructor() throws Exception { 34 | ConstructorTester.test(CallableTool.class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/fixture/HeapDumper.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.fixture; 2 | 3 | import javax.management.MBeanServer; 4 | 5 | import java.lang.management.ManagementFactory; 6 | import java.lang.reflect.Method; 7 | import java.nio.file.Path; 8 | 9 | public class HeapDumper { 10 | 11 | private static final String CLASS_NAME = "com.sun.management.HotSpotDiagnosticMXBean"; 12 | 13 | private static final String HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic"; 14 | 15 | public static void dumpHeap(final Path path) throws Exception { 16 | dumpHeap(path, false); 17 | } 18 | 19 | public static void dumpHeap(final Path path, final boolean live) throws Exception { 20 | final Class clazz = Class.forName(CLASS_NAME); 21 | final Object mxBean = getHotSpotMxBean(clazz); 22 | final Method method = clazz.getMethod("dumpHeap", String.class, boolean.class); 23 | method.invoke(mxBean, path.toString(), live); 24 | } 25 | 26 | private static T getHotSpotMxBean(final Class clazz) throws Exception { 27 | final MBeanServer server = ManagementFactory.getPlatformMBeanServer(); 28 | return ManagementFactory.newPlatformMXBeanProxy(server, HOTSPOT_BEAN_NAME, clazz); 29 | } 30 | 31 | private HeapDumper() { 32 | throw new AssertionError(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/capture/CaptureStreamFactory.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.capture; 2 | 3 | import com.paypal.heapdumptool.sanitizer.SanitizeCommand; 4 | import com.paypal.heapdumptool.sanitizer.SanitizeStreamFactory; 5 | import org.apache.commons.io.output.CloseShieldOutputStream; 6 | 7 | import java.io.Closeable; 8 | import java.io.IOException; 9 | import java.io.OutputStream; 10 | import java.util.concurrent.atomic.AtomicReference; 11 | 12 | import static org.apache.commons.io.IOUtils.closeQuietly; 13 | 14 | public class CaptureStreamFactory extends SanitizeStreamFactory implements Closeable { 15 | 16 | private final AtomicReference outputStreamRef; 17 | 18 | public CaptureStreamFactory(final SanitizeCommand command) { 19 | super(command); 20 | this.outputStreamRef = new AtomicReference<>(); 21 | } 22 | 23 | @Override 24 | public OutputStream newOutputStream() throws IOException { 25 | final OutputStream stream = super.newOutputStream(); 26 | outputStreamRef.compareAndSet(null, stream); 27 | return CloseShieldOutputStream.wrap(stream); 28 | } 29 | 30 | public OutputStream getNativeOutputStream() { 31 | return outputStreamRef.get(); 32 | } 33 | 34 | @Override 35 | public void close() { 36 | closeQuietly(getNativeOutputStream()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/capture/CaptureCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.capture; 2 | 3 | import com.paypal.heapdumptool.sanitizer.DataSize; 4 | import org.junit.jupiter.api.Test; 5 | import org.meanbean.test.BeanVerifier; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class CaptureCommandTest { 10 | 11 | @Test 12 | public void testBean() { 13 | BeanVerifier.forClass(CaptureCommand.class) 14 | .withSettings(settings -> settings.addOverridePropertyFactory(CaptureCommand::getBufferSize, () -> DataSize.ofMegabytes(5))) 15 | .withSettings(settings -> settings.addIgnoredPropertyName("excludeStringFields")) 16 | .verifyGettersAndSetters(); 17 | } 18 | 19 | @Test 20 | public void testSanitizationText() { 21 | assertThat(escapedSanitizationText("\\0")) 22 | .isEqualTo("\0"); 23 | assertThat(escapedSanitizationText("\0")) 24 | .isEqualTo("\0"); 25 | 26 | assertThat(escapedSanitizationText("foobar")) 27 | .isEqualTo("foobar"); 28 | } 29 | 30 | private String escapedSanitizationText(final String sanitizationText) { 31 | final CaptureCommand cmd = new CaptureCommand(); 32 | cmd.setSanitizationText(sanitizationText); 33 | return cmd.getSanitizationText(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/sanitizer/SanitizeCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.meanbean.test.BeanVerifier; 5 | 6 | import static com.paypal.heapdumptool.sanitizer.DataSize.ofMegabytes; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class SanitizeCommandTest { 10 | 11 | @Test 12 | public void testBean() { 13 | BeanVerifier.forClass(SanitizeCommand.class) 14 | .withSettings(settings -> settings.addOverridePropertyFactory(SanitizeCommand::getBufferSize, () -> ofMegabytes(5))) 15 | .withSettings(settings -> settings.addIgnoredPropertyName("excludeStringFields")) 16 | .verifyGettersAndSetters(); 17 | } 18 | 19 | @Test 20 | public void testSanitizationText() { 21 | assertThat(escapedSanitizationText("\\0")) 22 | .isEqualTo("\0"); 23 | assertThat(escapedSanitizationText("\0")) 24 | .isEqualTo("\0"); 25 | 26 | assertThat(escapedSanitizationText("foobar")) 27 | .isEqualTo("foobar"); 28 | } 29 | 30 | private String escapedSanitizationText(final String sanitizationText) { 31 | final SanitizeCommand cmd = new SanitizeCommand(); 32 | cmd.setSanitizationText(sanitizationText); 33 | return cmd.getSanitizationText(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/sanitizer/example/SimpleClass.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer.example; 2 | 3 | import java.lang.management.ManagementFactory; 4 | import java.time.Instant; 5 | import java.time.temporal.ChronoUnit; 6 | 7 | // for manual testing. 8 | // jcmd 186633 GC.heap_dump /tmp/heap.hprof 9 | // java -jar heap-dump-tool.jar sanitize /tmp/heap.hprof /tmp/sanitize.hprof --sanitize-byte-char-arrays-only=false 10 | // verify 0 primitive values 11 | // verify non-null object refs 12 | public class SimpleClass { 13 | 14 | private static final Long simpleStaticLong = System.currentTimeMillis(); 15 | private final Long simpleInstanceLong = simpleStaticLong + 1; 16 | private final int simpleInstanceInt = (int) (long) simpleInstanceLong; 17 | 18 | private final Inner inner = new Inner(); 19 | 20 | private static class Inner { 21 | private static final Long staticLong = System.currentTimeMillis(); 22 | private final Long instanceLong = staticLong + 1; 23 | private final int instanceInt = (int) (long) instanceLong; 24 | } 25 | 26 | public static void main(final String... args) throws InterruptedException { 27 | final SimpleClass simpleClass = new SimpleClass(); 28 | final String pid = ManagementFactory.getRuntimeMXBean().getName(); 29 | while (!Thread.currentThread().isInterrupted()) { 30 | System.out.println("Running at " + Instant.now().truncatedTo(ChronoUnit.SECONDS) + " " + pid); 31 | 32 | if (simpleClass.simpleInstanceLong > 0) { 33 | Thread.sleep(1_000); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/hserr/SanitizeHserrCommand.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.hserr; 2 | 3 | import com.paypal.heapdumptool.cli.CliCommand; 4 | import picocli.CommandLine.Command; 5 | import picocli.CommandLine.Parameters; 6 | 7 | import java.nio.file.Path; 8 | 9 | import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString; 10 | import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE; 11 | 12 | @Command(name = "sanitize-hserr", description = "Sanitize fatal error log by censoring environment variable values", abbreviateSynopsis = true) 13 | public class SanitizeHserrCommand implements CliCommand { 14 | 15 | // to allow field injection from picocli, these variables can't be final 16 | 17 | @Parameters(index = "0", description = "Input hs_err_pid* fatal error log. File or stdin") 18 | private Path inputFile; 19 | 20 | @Parameters(index = "1", description = "Output hs_err_pid* fatal error log. File, stdout, or stderr") 21 | private Path outputFile; 22 | 23 | @Override 24 | public Class getProcessorClass() { 25 | return SanitizeHserrCommandProcessor.class; 26 | } 27 | 28 | public Path getInputFile() { 29 | return inputFile; 30 | } 31 | 32 | public void setInputFile(final Path inputFile) { 33 | this.inputFile = inputFile; 34 | } 35 | 36 | public Path getOutputFile() { 37 | return outputFile; 38 | } 39 | 40 | public void setOutputFile(final Path outputFile) { 41 | this.outputFile = outputFile; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return reflectionToString(this, MULTI_LINE_STYLE); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/BasicType.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import java.util.Optional; 4 | import java.util.stream.Stream; 5 | 6 | public enum BasicType { 7 | OBJECT(2), 8 | BOOLEAN(4), 9 | CHAR(5), 10 | FLOAT(6), 11 | DOUBLE(7), 12 | BYTE(8), 13 | SHORT(9), 14 | INT(10), 15 | LONG(11); 16 | 17 | private final int u1Code; 18 | 19 | public static int findValueSize(final int u1Code, final int idSize) { 20 | final BasicType basicType = findByU1Code(u1Code).orElseThrow(() -> new IllegalArgumentException("Unknown basic type code: " + u1Code)); 21 | return basicType.getValueSize(idSize); 22 | } 23 | 24 | public static Optional findByU1Code(final int u1Code) { 25 | return Stream.of(BasicType.values()) 26 | .filter(basicType -> basicType.u1Code == u1Code) 27 | .findFirst(); 28 | } 29 | 30 | BasicType(final int u1Code) { 31 | this.u1Code = u1Code; 32 | } 33 | 34 | public int getU1Code() { 35 | return u1Code; 36 | } 37 | 38 | public int getValueSize(final int idSize) { 39 | switch (this) { 40 | case OBJECT: 41 | return idSize; 42 | case BOOLEAN: 43 | return 1; 44 | case CHAR: 45 | return 2; 46 | case FLOAT: 47 | return 4; 48 | case DOUBLE: 49 | return 8; 50 | case BYTE: 51 | return 1; 52 | case SHORT: 53 | return 2; 54 | case INT: 55 | return 4; 56 | case LONG: 57 | return 8; 58 | default: 59 | throw new IllegalArgumentException("Unknown basic type: " + this); 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/fixture/ByteArrayTool.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.fixture; 2 | 3 | import com.paypal.heapdumptool.sanitizer.DataSize; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Arrays; 8 | 9 | public class ByteArrayTool { 10 | 11 | public static int countOfSequence(final byte[] big, final byte[] small) { 12 | int count = 0; 13 | for (int i = 0; i < big.length; i++) { 14 | if (startsWith(big, i, small)) { 15 | count++; 16 | i = i + small.length - 2; 17 | } 18 | } 19 | return count; 20 | } 21 | 22 | public static boolean startsWith(final byte[] big, final int bigIndex, final byte[] small) { 23 | int count = 0; 24 | for (int i = bigIndex, j = 0; i < big.length && j < small.length; i++, j++) { 25 | if (big[i] == small[j]) { 26 | count++; 27 | } 28 | } 29 | return count == small.length; 30 | } 31 | 32 | public static byte[] lengthen(final byte[] input, final DataSize wantedDataSize) { 33 | return Arrays.copyOf(input, (int) wantedDataSize.toBytes()); 34 | } 35 | 36 | public static String lengthen(final String input, final DataSize wantedDataSize) { 37 | final byte[] currentBytes = input.getBytes(StandardCharsets.UTF_8); 38 | final byte[] newBytes = lengthen(currentBytes, wantedDataSize); 39 | return new String(newBytes, StandardCharsets.UTF_8); 40 | } 41 | 42 | public static byte[] nCopiesLongToBytes(final long value, final int count) { 43 | final ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * count); 44 | for (int i = 0; i < count; i++) { 45 | buffer.putLong(value); 46 | } 47 | return buffer.array(); 48 | } 49 | 50 | private ByteArrayTool() { 51 | throw new AssertionError(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/hserr/SanitizeHserrCommandProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.hserr; 2 | 3 | import com.paypal.heapdumptool.fixture.ResourceTool; 4 | import com.paypal.heapdumptool.sanitizer.SanitizeStreamFactory; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.nio.file.Paths; 12 | 13 | import static java.nio.charset.StandardCharsets.UTF_8; 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.mockito.Mockito.doReturn; 16 | import static org.mockito.Mockito.mock; 17 | 18 | public class SanitizeHserrCommandProcessorTest { 19 | 20 | private final SanitizeHserrCommand command = new SanitizeHserrCommand(); 21 | 22 | private final SanitizeStreamFactory streamFactory = mock(SanitizeStreamFactory.class); 23 | 24 | private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 25 | 26 | private SanitizeHserrCommandProcessor processor; 27 | 28 | @BeforeEach 29 | public void beforeEach() throws IOException { 30 | final String input = ResourceTool.contentOf(SanitizeHserrCommandProcessorTest.class, "hs_err_pid123.txt"); 31 | doReturn(new ByteArrayInputStream(input.getBytes(UTF_8))) 32 | .when(streamFactory) 33 | .newInputStream(); 34 | 35 | doReturn(outputStream).when(streamFactory).newOutputStream(); 36 | 37 | command.setInputFile(Paths.get("input")); 38 | command.setOutputFile(Paths.get("output")); 39 | processor = new SanitizeHserrCommandProcessor(command, streamFactory); 40 | } 41 | 42 | @Test 43 | public void testProcess() throws Exception { 44 | processor.process(); 45 | 46 | final String output = outputStream.toString(UTF_8.name()); 47 | assertThat(output) 48 | .contains("# SIGSEGV (0xb) at pc=0x00007fab2dfe7a6d, pid=32369, tid=32375") 49 | .doesNotContain("LANG=en_US.UTF-8") 50 | .contains("LANG=****"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/SanitizeCommand.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import com.paypal.heapdumptool.cli.CliCommand; 4 | import picocli.CommandLine.Command; 5 | import picocli.CommandLine.Option; 6 | import picocli.CommandLine.Parameters; 7 | 8 | import java.nio.file.Path; 9 | 10 | import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString; 11 | import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE; 12 | import static picocli.CommandLine.Help.Visibility.ALWAYS; 13 | 14 | @Command(name = "sanitize", description = "Sanitize a heap dump by replacing byte and char array contents", abbreviateSynopsis = true) 15 | public class SanitizeCommand extends SanitizeOrCaptureCommandBase implements CliCommand { 16 | 17 | // to allow field injection from picocli, these variables can't be final 18 | 19 | @Parameters(index = "0", description = "Input heap dump .hprof. File or stdin") 20 | private Path inputFile; 21 | 22 | @Parameters(index = "1", description = "Output heap dump .hprof. File, stdout, or stderr") 23 | private Path outputFile; 24 | 25 | @Option(names = {"-z", "--zip-output"}, description = "Write zipped output", showDefaultValue = ALWAYS) 26 | private boolean zipOutput; 27 | 28 | @Override 29 | public Class getProcessorClass() { 30 | return SanitizeCommandProcessor.class; 31 | } 32 | 33 | public Path getInputFile() { 34 | return inputFile; 35 | } 36 | 37 | public void setInputFile(final Path inputFile) { 38 | this.inputFile = inputFile; 39 | } 40 | 41 | public Path getOutputFile() { 42 | return outputFile; 43 | } 44 | 45 | public void setOutputFile(final Path outputFile) { 46 | this.outputFile = outputFile; 47 | } 48 | 49 | public boolean isZipOutput() { 50 | return zipOutput; 51 | } 52 | 53 | public void setZipOutput(final boolean zipOutput) { 54 | this.zipOutput = zipOutput; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return reflectionToString(this, MULTI_LINE_STYLE); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/capture/CaptureStreamFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.capture; 2 | 3 | import com.paypal.heapdumptool.sanitizer.SanitizeCommand; 4 | import org.apache.commons.io.IOUtils; 5 | import org.apache.commons.io.output.CloseShieldOutputStream; 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.io.TempDir; 9 | 10 | import java.io.Closeable; 11 | import java.io.IOException; 12 | import java.io.OutputStream; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.util.Collection; 16 | import java.util.concurrent.LinkedBlockingQueue; 17 | 18 | import static com.paypal.heapdumptool.sanitizer.DataSize.ofBytes; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | class CaptureStreamFactoryTest { 22 | 23 | @TempDir 24 | Path tempDir; 25 | 26 | private final Collection closeables = new LinkedBlockingQueue<>(); 27 | 28 | @AfterEach 29 | void afterEach() { 30 | closeables.forEach(IOUtils::closeQuietly); 31 | } 32 | 33 | @Test 34 | void testOutputStream() throws IOException { 35 | final CaptureStreamFactory streamFactory = newStreamFactory(); 36 | assertThat(streamFactory.newOutputStream()) 37 | .isInstanceOf(CloseShieldOutputStream.class); 38 | assertThat(streamFactory.getNativeOutputStream()) 39 | .isInstanceOf(nioOutputStreamType()); 40 | } 41 | 42 | private Class nioOutputStreamType() throws IOException { 43 | final OutputStream nioOutputStream = Files.newOutputStream(tempDir.resolve("baz")); 44 | closeables.add(nioOutputStream); 45 | return nioOutputStream.getClass(); 46 | } 47 | 48 | private CaptureStreamFactory newStreamFactory() { 49 | final SanitizeCommand command = new SanitizeCommand(); 50 | command.setInputFile(tempDir.resolve("foo")); 51 | command.setOutputFile(tempDir.resolve("bar")); 52 | command.setBufferSize(ofBytes(0)); 53 | 54 | final CaptureStreamFactory captureStreamFactory = new CaptureStreamFactory(command); 55 | closeables.add(captureStreamFactory); 56 | return captureStreamFactory; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/ApplicationTestSupport.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool; 2 | 3 | import com.paypal.heapdumptool.capture.PrivilegeEscalator; 4 | import org.apache.commons.lang3.mutable.MutableInt; 5 | import org.mockito.ArgumentCaptor; 6 | import org.mockito.Captor; 7 | import org.mockito.MockedStatic; 8 | import picocli.CommandLine; 9 | import picocli.CommandLine.ParseResult; 10 | 11 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation.PRIVILEGED_ALREADY; 12 | import static com.paypal.heapdumptool.fixture.MockitoTool.voidAnswer; 13 | import static org.mockito.ArgumentMatchers.any; 14 | import static org.mockito.ArgumentMatchers.anyInt; 15 | import static org.mockito.ArgumentMatchers.eq; 16 | import static org.mockito.Mockito.mockStatic; 17 | 18 | public class ApplicationTestSupport { 19 | 20 | public static int runApplication(final String... args) throws Exception { 21 | final ArgumentCaptor captor = ArgumentCaptor.forClass(int.class); 22 | 23 | try (final MockedStatic applicationMock = mockStatic(Application.class)) { 24 | 25 | applicationMock.when(() -> Application.systemExit(anyInt())) 26 | .thenAnswer(voidAnswer()); 27 | 28 | applicationMock.when(() -> Application.main(args)) 29 | .thenCallRealMethod(); 30 | 31 | applicationMock.when(Application::newCommandLine) 32 | .thenCallRealMethod(); 33 | 34 | Application.main(args); 35 | 36 | applicationMock.verify(() -> Application.systemExit(captor.capture())); 37 | } 38 | 39 | return captor.getValue(); 40 | } 41 | 42 | public static int runApplicationPrivileged(final String... args) throws Exception { 43 | 44 | final CommandLine commandLine = Application.newCommandLine(); 45 | 46 | try (final MockedStatic escalatorMock = mockStatic(PrivilegeEscalator.class)) { 47 | 48 | escalatorMock.when(() -> PrivilegeEscalator.escalatePrivilegesIfNeeded(eq(commandLine), any())) 49 | .thenReturn(PRIVILEGED_ALREADY); 50 | 51 | return runApplication(args); 52 | } 53 | } 54 | 55 | private ApplicationTestSupport() { 56 | throw new AssertionError(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/c/nsenter1.c: -------------------------------------------------------------------------------- 1 | // based on https://github.com/justincormack/nsenter1/blob/eeb60b727f98f78a56a81e048fe938e1297980a5/nsenter1.c 2 | /** 3 | Copyright (C) 2016 Justin Cormack 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | #define _GNU_SOURCE 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | extern char **environ; 27 | 28 | // Reassociate with the most important namespaces of pid 1 29 | 30 | int main(int argc, char **argv) { 31 | char *shell = "/bin/sh"; 32 | char *def[] = {shell, NULL}; 33 | char *cmd = shell; 34 | char **args = def; 35 | int fdm = open("/proc/1/ns/mnt", O_RDONLY); 36 | int fdu = open("/proc/1/ns/uts", O_RDONLY); 37 | int fdn = open("/proc/1/ns/net", O_RDONLY); 38 | int fdi = open("/proc/1/ns/ipc", O_RDONLY); 39 | int froot = open("/proc/1/root", O_RDONLY); 40 | 41 | if (fdm == -1 || fdu == -1 || fdn == -1 || fdi == -1 || froot == -1) { 42 | fprintf(stderr, "Failed to open /proc/1 files, are you root?\n"); 43 | exit(1); 44 | } 45 | 46 | if (setns(fdm, 0) == -1) { 47 | perror("setns:mnt"); 48 | exit(1); 49 | } 50 | if (setns(fdu, 0) == -1) { 51 | perror("setns:uts"); 52 | exit(1); 53 | } 54 | if (setns(fdn, 0) == -1) { 55 | perror("setns:net"); 56 | exit(1); 57 | } 58 | if (setns(fdi, 0) == -1) { 59 | perror("setns:ipc"); 60 | exit(1); 61 | } 62 | if (fchdir(froot) == -1) { 63 | perror("fchdir"); 64 | exit(1); 65 | } 66 | if (chroot(".") == -1) { 67 | perror("chroot"); 68 | exit(1); 69 | } 70 | if (argc > 1) { 71 | cmd = argv[1]; 72 | args = argv + 1; 73 | } 74 | if (execvpe(cmd, args, environ) == -1) { 75 | perror("execvpe"); 76 | exit(1); 77 | } 78 | exit(0); 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/utils/ProcessTool.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.UncheckedIOException; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.concurrent.Future; 11 | 12 | import static java.nio.charset.StandardCharsets.UTF_8; 13 | import static java.util.Collections.unmodifiableList; 14 | import static java.util.concurrent.CompletableFuture.supplyAsync; 15 | 16 | /** 17 | * Convenience tool for running os processes 18 | */ 19 | public class ProcessTool { 20 | 21 | // for mocking 22 | public static ProcessBuilder processBuilder(final String... cmd) { 23 | return new ProcessBuilder(cmd); 24 | } 25 | 26 | public static ProcessResult run(final String... cmd) throws Exception { 27 | final Process process = processBuilder(cmd).start(); 28 | try { 29 | process.getOutputStream().close(); 30 | 31 | final InputStream stderrStream = process.getErrorStream(); 32 | final InputStream stdoutStream = process.getInputStream(); 33 | 34 | final Future stderrFuture = supplyAsync(() -> readStream(stderrStream)); 35 | final Future stdoutFuture = supplyAsync(() -> readStream(stdoutStream)); 36 | 37 | final int exitCode = process.waitFor(); 38 | return new ProcessResult(exitCode, stdoutFuture.get(), stderrFuture.get()); 39 | } finally { 40 | process.destroy(); 41 | } 42 | } 43 | 44 | static String readStream(final InputStream stream) { 45 | try { 46 | return IOUtils.toString(stream, UTF_8); 47 | } catch (final IOException e) { 48 | throw new UncheckedIOException(e); 49 | } 50 | } 51 | 52 | private ProcessTool() { 53 | throw new AssertionError(); 54 | } 55 | 56 | public static class ProcessResult { 57 | 58 | public final int exitCode; 59 | public final String stdout; 60 | public final String stderr; 61 | 62 | public ProcessResult(final int exitCode, final String stdout, final String stderr) { 63 | this.exitCode = exitCode; 64 | this.stdout = stdout; 65 | this.stderr = stderr; 66 | } 67 | 68 | public List stdoutLines() { 69 | return unmodifiableList(Arrays.asList(stdout.split("\n"))); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/utils/ProgressMonitor.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import com.paypal.heapdumptool.sanitizer.DataSize; 4 | import org.apache.commons.io.FileUtils; 5 | import org.apache.commons.io.input.BoundedInputStream; 6 | import org.apache.commons.io.output.CountingOutputStream; 7 | import org.apache.commons.lang3.mutable.MutableLong; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.io.UncheckedIOException; 13 | import java.util.function.Consumer; 14 | 15 | @FunctionalInterface 16 | public interface ProgressMonitor extends Consumer { 17 | 18 | /** 19 | * Create a new {@link ProgressMonitor} that logs a message for each stepSize processed 20 | */ 21 | static ProgressMonitor numBytesProcessedMonitor(final DataSize stepSize, final InternalLogger logger) { 22 | final long stepSizeBytes = stepSize.toBytes(); 23 | final MutableLong steps = new MutableLong(); 24 | 25 | return numBytesProcessed -> { 26 | final long currentSteps = numBytesProcessed / stepSizeBytes; 27 | if (currentSteps != steps.longValue()) { 28 | steps.setValue(currentSteps); 29 | logger.info("Processed {}", FileUtils.byteCountToDisplaySize(numBytesProcessed)); 30 | } 31 | }; 32 | } 33 | 34 | /** 35 | * Create a OutputStream monitored by this 36 | */ 37 | default OutputStream monitoredOutputStream(final OutputStream output) { 38 | final ProgressMonitor monitor = this; 39 | return new CountingOutputStream(output) { 40 | 41 | @Override 42 | protected void beforeWrite(final int n) { 43 | super.beforeWrite(n); 44 | monitor.accept(getByteCount()); 45 | } 46 | }; 47 | } 48 | 49 | /** 50 | * Create a OutputStream monitored by this 51 | */ 52 | @SuppressWarnings("deprecation") 53 | default InputStream monitoredInputStream(final InputStream input) { 54 | final ProgressMonitor monitor = this; 55 | return new BoundedInputStream(input) { 56 | 57 | @Override 58 | public void afterRead(final int n) { 59 | try { 60 | super.afterRead(n); 61 | } catch (final IOException e) { 62 | throw new UncheckedIOException(e); 63 | } 64 | monitor.accept(getCount()); 65 | } 66 | }; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/sanitizer/SanitizeCommandProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.mockito.MockedConstruction; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Paths; 9 | import java.util.Collections; 10 | 11 | import static com.paypal.heapdumptool.sanitizer.DataSize.ofBytes; 12 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 13 | import static org.mockito.Mockito.atLeastOnce; 14 | import static org.mockito.Mockito.doNothing; 15 | import static org.mockito.Mockito.doReturn; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.mockConstruction; 18 | import static org.mockito.Mockito.verify; 19 | 20 | class SanitizeCommandProcessorTest { 21 | 22 | private final HeapDumpSanitizer sanitizer = mock(HeapDumpSanitizer.class); 23 | 24 | private final SanitizeStreamFactory streamFactory = mock(SanitizeStreamFactory.class); 25 | 26 | private final SanitizeCommand command = new SanitizeCommand(); 27 | 28 | @BeforeEach 29 | void beforeEach() throws IOException { 30 | doNothing().when(sanitizer).sanitize(); 31 | doReturn(null).when(streamFactory).newInputStream(); 32 | doReturn(null).when(streamFactory).newOutputStream(); 33 | 34 | command.setInputFile(Paths.get("input")); 35 | command.setOutputFile(Paths.get("output")); 36 | command.setExcludeStringFields(Collections.singletonList("none#none")); 37 | } 38 | 39 | @Test 40 | void testBufferSizeValidation() { 41 | command.setBufferSize(ofBytes(-1)); 42 | 43 | assertThatThrownBy(() -> new SanitizeCommandProcessor(command)) 44 | .isInstanceOf(IllegalArgumentException.class) 45 | .hasMessage("Invalid buffer size"); 46 | } 47 | 48 | @Test 49 | void testProcess() throws Exception { 50 | final SanitizeCommandProcessor processor = new SanitizeCommandProcessor(command, streamFactory); 51 | 52 | try (final MockedConstruction mocked = mockConstruction(HeapDumpSanitizer.class, this::prepare)) { 53 | processor.process(); 54 | for (final HeapDumpSanitizer sanitizer : mocked.constructed()) { 55 | verify(sanitizer, atLeastOnce()).sanitize(); 56 | } 57 | } 58 | } 59 | 60 | private void prepare(final HeapDumpSanitizer mock, final MockedConstruction.Context context) throws Throwable { 61 | doNothing().when(mock).sanitize(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/resources/privilege-escalate.sh.tmpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # If you see this comment while running the tool, you are likely running the tool incorrectly. It should be run like this: 4 | # $ docker run --rm __IMAGE_NAME__ __ARGS__ | bash 5 | # 6 | # The container does not have privileges or pid namespace is not attached. Escalating privileges by running the container 7 | # again with privileges. 8 | 9 | function runForAppleSilicon() { 10 | CMD='docker run -d --rm --name heap-dump-tool --entrypoint sleep' 11 | HDT_DIR=${HDT_DIR:-$TMPDIR/heap-dump-tool} 12 | echo "Extracting heap-dump-tool to $HDT_DIR ..." 13 | set +e 14 | docker stop --time 0 heap-dump-tool > /dev/null 2>&1 15 | set -e 16 | eval $CMD $FQ_IMAGE 99 > /dev/null 17 | 18 | sleep 1 19 | rm -rf $HDT_DIR 20 | docker cp -q heap-dump-tool:/opt/heap-dump-tool/heap-dump-tool.jar $TMPDIR/ 21 | 22 | docker stop --time 0 heap-dump-tool > /dev/null 23 | 24 | echo "java -jar $TMPDIR/heap-dump-tool.jar __ARGS__" 25 | echo 26 | exec java -jar $TMPDIR/heap-dump-tool.jar __ARGS__ 27 | } 28 | 29 | set -euo pipefail 30 | FQ_IMAGE="${__DOCKER_REGISTRY_ENV_NAME__:-__DEFAULT_REGISTRY__}/__IMAGE_NAME__" 31 | 32 | if [[ "${SKIP_DOCKER_PULL:-__SKIP_DOCKER_PULL__}" == "false" && "$FQ_IMAGE" != "local/__IMAGE_NAME__" ]]; then 33 | docker pull "$FQ_IMAGE" 34 | fi 35 | 36 | TMPDIR="${TMPDIR:-/tmp/}" 37 | JAVA_TOOL_OPTIONS=${JAVA_TOOL_OPTIONS:--XX:MaxRAMPercentage=50.0 -XX:-OmitStackTraceInFastThrow} 38 | DOCKER_OPTIONS="${DOCKER_OPTIONS:-}" 39 | 40 | HDT_DETECT_CPU=${HDT_DETECT_CPU:-TRUE} 41 | HDT_OS=${HDT_OS:-`uname -s`} 42 | HDT_CPU_TYPE=${HDT_CPU_TYPE:-`uname -m`} 43 | if [ "$HDT_DETECT_CPU" = "TRUE" ] && [ "$HDT_CPU_TYPE" = "arm64" ] && [ "$HDT_OS" = "Darwin" ]; then 44 | echo "Detected Apple Silicon" 45 | echo " \$HDT_DETECT_CPU=$HDT_DETECT_CPU" 46 | echo " \$HDT_OS=$HDT_OS" 47 | echo " \$HDT_CPU_TYPE=$HDT_CPU_TYPE" 48 | echo "" 49 | echo "Due to containerization limitations on Mac, heap-dump-tool running within a container cannot capture a sanitized" 50 | echo "heap dump of a Java process running in another container." 51 | echo "" 52 | echo "Running heap-dump-tool directly on Mac instead ..." 53 | runForAppleSilicon __ARGS__ 54 | exit 0 55 | fi 56 | 57 | # --privileged --pid=host -- let the tool run processes on host 58 | # --workdir `pwd` -v `pwd`:`pwd` -- mount host cwd as container cwd, where the heap dump will be saved 59 | # -e HOST_USER=`whoami` -- pass in host username so that the tool sets right owner on the output file 60 | docker run --rm \ 61 | --privileged --pid=host \ 62 | --workdir `pwd` -v `pwd`:`pwd` -v $TMPDIR:$TMPDIR \ 63 | -e JAVA_OPTS="$JAVA_TOOL_OPTIONS" \ 64 | -e TMPDIR=$TMPDIR \ 65 | -e HOST_USER=`whoami` \ 66 | ${DOCKER_OPTIONS} \ 67 | $FQ_IMAGE __ARGS__ 68 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/DataUnit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.paypal.heapdumptool.sanitizer; 18 | 19 | /** 20 | * A standard set of {@link DataSize} units. 21 | * 22 | *

The unit prefixes used in this class are 23 | * binary prefixes 24 | * indicating multiplication by powers of 2. The following table displays the 25 | * enum constants defined in this class and corresponding values. 26 | * 27 | * @author Stephane Nicoll 28 | * @author Sam Brannen 29 | * @since 5.1 30 | * @see DataSize 31 | */ 32 | public enum DataUnit { 33 | 34 | /** 35 | * Bytes, represented by suffix {@code B}. 36 | */ 37 | BYTES("B", DataSize.ofBytes(1)), 38 | 39 | /** 40 | * Kilobytes, represented by suffix {@code KB}. 41 | */ 42 | KILOBYTES("KB", DataSize.ofKilobytes(1)), 43 | 44 | /** 45 | * Megabytes, represented by suffix {@code MB}. 46 | */ 47 | MEGABYTES("MB", DataSize.ofMegabytes(1)), 48 | 49 | /** 50 | * Gigabytes, represented by suffix {@code GB}. 51 | */ 52 | GIGABYTES("GB", DataSize.ofGigabytes(1)), 53 | 54 | /** 55 | * Terabytes, represented by suffix {@code TB}. 56 | */ 57 | TERABYTES("TB", DataSize.ofTerabytes(1)); 58 | 59 | private final String suffix; 60 | 61 | private final DataSize size; 62 | 63 | DataUnit(final String suffix, final DataSize size) { 64 | this.suffix = suffix; 65 | this.size = size; 66 | } 67 | 68 | DataSize size() { 69 | return this.size; 70 | } 71 | 72 | /** 73 | * Return the {@link DataUnit} matching the specified {@code suffix}. 74 | * @param suffix one of the standard suffixes 75 | * @return the {@link DataUnit} matching the specified {@code suffix} 76 | * @throws IllegalArgumentException if the suffix does not match the suffix 77 | * of any of this enum's constants 78 | */ 79 | public static DataUnit fromSuffix(final String suffix) { 80 | for (final DataUnit candidate : values()) { 81 | if (candidate.suffix.equals(suffix)) { 82 | return candidate; 83 | } 84 | } 85 | throw new IllegalArgumentException("Unknown data unit suffix '" + suffix + "'"); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool; 2 | 3 | import com.paypal.heapdumptool.capture.PrivilegeEscalator; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.MockedConstruction; 7 | import org.mockito.MockedConstruction.Context; 8 | import org.mockito.MockedStatic; 9 | import org.springframework.boot.test.system.CapturedOutput; 10 | import org.springframework.boot.test.system.OutputCaptureExtension; 11 | import picocli.CommandLine; 12 | 13 | import java.util.Objects; 14 | 15 | import static com.paypal.heapdumptool.ApplicationTestSupport.runApplication; 16 | import static com.paypal.heapdumptool.ApplicationTestSupport.runApplicationPrivileged; 17 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation.REQUIRED_AND_PROMPTED; 18 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.escalatePrivilegesIfNeeded; 19 | import static com.paypal.heapdumptool.fixture.ResourceTool.contentOf; 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static org.mockito.ArgumentMatchers.any; 22 | import static org.mockito.ArgumentMatchers.eq; 23 | import static org.mockito.Mockito.doReturn; 24 | import static org.mockito.Mockito.mockConstruction; 25 | import static org.mockito.Mockito.mockStatic; 26 | 27 | @ExtendWith(OutputCaptureExtension.class) 28 | public class ApplicationTest { 29 | 30 | @Test 31 | public void testVersionProvider() throws Exception { 32 | final String[] version = new Application().getVersion(); 33 | assertThat(version[0]).contains("heap-dump-tool"); 34 | } 35 | 36 | @Test 37 | public void testMainHelp(final CapturedOutput output) throws Exception { 38 | final int exitCode = runApplicationPrivileged("help"); 39 | assertThat(exitCode).isEqualTo(0); 40 | 41 | final String expectedOutput = contentOf(getClass(), "help.txt"); 42 | assertThat(output.getOut()).isEqualTo(expectedOutput); 43 | } 44 | 45 | @Test 46 | public void testPrivilegeEscalated(final CapturedOutput output) throws Exception { 47 | final CommandLine commandLine = Application.newCommandLine(); 48 | try (final MockedStatic mocked = mockStatic(PrivilegeEscalator.class)) { 49 | mocked.when(() -> escalatePrivilegesIfNeeded(eq(commandLine), eq("capture"))) 50 | .thenReturn(REQUIRED_AND_PROMPTED); 51 | 52 | try (final MockedConstruction mockedCmd = mockConstruction(CommandLine.class, this::prepare)) { 53 | Objects.requireNonNull(mockedCmd); 54 | final int exitCode = runApplication("capture", "my-container"); 55 | assertThat(exitCode).isEqualTo(0); 56 | } 57 | 58 | assertThat(output.getOut()).isEmpty(); 59 | } 60 | } 61 | 62 | private void prepare(final CommandLine mock, final Context context) { 63 | doReturn(0).when(mock).execute(any()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/utils/ProgressMonitorTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.springframework.boot.test.system.CapturedOutput; 7 | import org.springframework.boot.test.system.OutputCaptureExtension; 8 | 9 | import java.io.ByteArrayInputStream; 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.OutputStream; 14 | 15 | import static com.paypal.heapdumptool.sanitizer.DataSize.ofBytes; 16 | import static java.nio.charset.StandardCharsets.UTF_8; 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.mockito.Mockito.*; 19 | 20 | @ExtendWith(OutputCaptureExtension.class) 21 | public class ProgressMonitorTest { 22 | 23 | private static final InternalLogger LOGGER = InternalLogger.getLogger(ProgressMonitorTest.class); 24 | 25 | @Test 26 | public void testNumBytesWrittenMonitor(final CapturedOutput output) { 27 | 28 | final ProgressMonitor numBytesWrittenMonitor = ProgressMonitor.numBytesProcessedMonitor(ofBytes(5), LOGGER); 29 | numBytesWrittenMonitor.accept(4L); 30 | 31 | assertThat(output) 32 | .isEmpty(); 33 | 34 | numBytesWrittenMonitor.accept(5L); 35 | assertThat(output) 36 | .hasLineCount(1) 37 | .contains("Processed 5 bytes"); 38 | 39 | numBytesWrittenMonitor.accept(6L); 40 | assertThat(output) 41 | .hasLineCount(1) 42 | .contains("Processed 5 bytes"); 43 | 44 | numBytesWrittenMonitor.accept(11L); 45 | assertThat(output) 46 | .hasLineCount(2) 47 | .contains("Processed 5 bytes") 48 | .contains("Processed 11 bytes"); 49 | } 50 | 51 | @Test 52 | public void testMonitoredInputStream() throws IOException { 53 | 54 | final ProgressMonitor monitor = mock(ProgressMonitor.class); 55 | 56 | final InputStream inputStream = new ByteArrayInputStream("hello".getBytes(UTF_8)); 57 | doCallRealMethod().when(monitor).monitoredInputStream(inputStream); 58 | 59 | final InputStream monitoredInputStream = monitor.monitoredInputStream(inputStream); 60 | IOUtils.toByteArray(monitoredInputStream); 61 | 62 | verify(monitor, times(2)).accept((long) "hello".length()); 63 | } 64 | 65 | @Test 66 | public void testMonitoredOutputStream() throws IOException { 67 | 68 | final ProgressMonitor monitor = mock(ProgressMonitor.class); 69 | 70 | final OutputStream outputStream = new ByteArrayOutputStream(); 71 | doCallRealMethod().when(monitor).monitoredOutputStream(outputStream); 72 | 73 | final OutputStream monitoredOutputStream = monitor.monitoredOutputStream(outputStream); 74 | IOUtils.write("world", monitoredOutputStream, UTF_8); 75 | 76 | verify(monitor).accept((long) "world".length()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/cli/CliBootstrapTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.cli; 2 | 3 | import com.paypal.heapdumptool.fixture.ConstructorTester; 4 | import org.apache.commons.lang3.reflect.ConstructorUtils; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.mockito.MockedStatic; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | 11 | import static com.paypal.heapdumptool.fixture.MockitoTool.genericMock; 12 | import static org.apache.commons.lang3.reflect.ConstructorUtils.invokeConstructor; 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.catchThrowable; 15 | import static org.mockito.Mockito.doReturn; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.mockStatic; 18 | import static org.mockito.Mockito.never; 19 | import static org.mockito.Mockito.verify; 20 | 21 | public class CliBootstrapTest { 22 | 23 | private final CliCommand command = mock(CliCommand.class); 24 | 25 | private final CliCommandProcessor processor = genericMock(CliCommandProcessor.class); 26 | 27 | @BeforeEach 28 | public void beforeEach() { 29 | doReturn(processor.getClass()) 30 | .when(command) 31 | .getProcessorClass(); 32 | } 33 | 34 | @Test 35 | public void testRunCommand() throws Exception { 36 | try (final MockedStatic mocked = mockStatic(ConstructorUtils.class)) { 37 | mocked.when(() -> invokeConstructor(processor.getClass(), command)) 38 | .thenReturn(processor); 39 | 40 | CliBootstrap.runCommand(command); 41 | 42 | verify(processor).process(); 43 | } 44 | } 45 | 46 | @Test 47 | public void testRunCommandInvocationTargetException() throws Exception { 48 | try (final MockedStatic mocked = mockStatic(ConstructorUtils.class)) { 49 | mocked.when(() -> invokeConstructor(processor.getClass(), command)) 50 | .thenThrow(new InvocationTargetException(new IllegalStateException())); 51 | 52 | verifyExceptionThrown(); 53 | } 54 | } 55 | 56 | @Test 57 | public void testRunCommandException() throws Exception { 58 | try (final MockedStatic mocked = mockStatic(ConstructorUtils.class)) { 59 | mocked.when(() -> invokeConstructor(processor.getClass(), command)) 60 | .thenThrow(new IllegalStateException()); 61 | 62 | final Throwable throwable = catchThrowable(() -> CliBootstrap.runCommand(command)); 63 | assertThat(throwable).isInstanceOf(IllegalStateException.class); 64 | 65 | verify(processor, never()).process(); 66 | } 67 | } 68 | 69 | private void verifyExceptionThrown() throws Exception { 70 | final Throwable throwable = catchThrowable(() -> CliBootstrap.runCommand(command)); 71 | assertThat(throwable).isInstanceOf(IllegalStateException.class); 72 | 73 | verify(processor, never()).process(); 74 | } 75 | 76 | 77 | @Test 78 | public void testConstructor() throws Exception { 79 | ConstructorTester.test(CliBootstrap.class); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/utils/InternalLogger.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.slf4j.spi.SLF4JServiceProvider; 6 | 7 | import java.io.PrintStream; 8 | import java.time.OffsetDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | import java.time.temporal.ChronoUnit; 11 | import java.util.ServiceLoader; 12 | 13 | // class to make slf4j impl work when heap-dump-tool is used as a library or when used as an app without slf4j impl 14 | public class InternalLogger { 15 | 16 | private static final int DEBUG_INT = 10; 17 | 18 | private static final int INFO_INT = 20; 19 | 20 | private static final boolean hasSlf4jImpl = hasSelf4jImpl(); 21 | 22 | private static final int level = Integer.getInteger("heap-dump-tool.logLevel", INFO_INT); 23 | 24 | private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss,SSS"); 25 | 26 | private final PrintStream out = System.out; 27 | 28 | private final String clazz; 29 | 30 | private Logger logger; 31 | 32 | public InternalLogger(final Class clazz) { 33 | this.clazz = clazz.getSimpleName(); 34 | } 35 | 36 | private static boolean hasSelf4jImpl() { 37 | final ClassLoader classLoader = Thread.currentThread().getContextClassLoader() != null 38 | ? Thread.currentThread().getContextClassLoader() 39 | : InternalLogger.class.getClassLoader(); 40 | final String explicitlySpecified = System.getProperty("slf4j.provider"); 41 | if (explicitlySpecified != null && !explicitlySpecified.isEmpty()) { 42 | return true; 43 | } 44 | final ServiceLoader loader = ServiceLoader.load(SLF4JServiceProvider.class, classLoader); 45 | return loader.iterator().hasNext(); 46 | } 47 | 48 | public static InternalLogger getLogger(final Class clazz) { 49 | return new InternalLogger(clazz); 50 | } 51 | 52 | private synchronized Logger getLogger() { 53 | if (logger == null) { 54 | logger = LoggerFactory.getLogger(clazz); 55 | } 56 | return logger; 57 | } 58 | 59 | public void info(final String format, final Object... arguments) { 60 | if (hasSlf4jImpl) { 61 | getLogger().info(format, arguments); 62 | } else if (level <= INFO_INT) { 63 | final String message = getMessage(format, arguments); 64 | out.printf("%s INFO %s - %s%n", getTimestamp(), clazz, message); 65 | } 66 | } 67 | 68 | public void debug(final String format, final Object... arguments) { 69 | if (hasSlf4jImpl) { 70 | getLogger().debug(format, arguments); 71 | } else if (level <= DEBUG_INT) { 72 | final String message = getMessage(format, arguments); 73 | out.printf("%s DEBUG %s - %s%n", getTimestamp(), clazz, message); 74 | } 75 | } 76 | 77 | private static String getMessage(final String format, final Object[] arguments) { 78 | final String newFormat = format.replace("{}", "%s"); 79 | return String.format(newFormat, arguments); 80 | } 81 | 82 | private static String getTimestamp() { 83 | return dateTimeFormatter.format(OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/SanitizeStreamFactory.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; 4 | import org.apache.commons.lang3.Strings; 5 | import org.apache.commons.lang3.Validate; 6 | 7 | import java.io.BufferedInputStream; 8 | import java.io.BufferedOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.util.zip.ZipEntry; 15 | import java.util.zip.ZipOutputStream; 16 | 17 | import static java.lang.Math.toIntExact; 18 | 19 | /** 20 | * Creates i/o streams for input/output files 21 | */ 22 | public class SanitizeStreamFactory { 23 | 24 | private final SanitizeCommand command; 25 | 26 | public SanitizeStreamFactory(final SanitizeCommand command) { 27 | this.command = validate(command); 28 | } 29 | 30 | public InputStream newInputStream() throws IOException { 31 | final Path inputFile = command.getInputFile(); 32 | final InputStream inputStream = getBufferSize() == 0 33 | ? newInputStream(inputFile) 34 | : new BufferedInputStream(newInputStream(inputFile), getBufferSize()); 35 | 36 | if (command.isTarInput()) { 37 | final TarArchiveInputStream tarStream = new TarArchiveInputStream(inputStream); 38 | Validate.notNull(tarStream.getNextEntry(), "no tar entries"); 39 | return tarStream; 40 | } 41 | return inputStream; 42 | } 43 | 44 | public OutputStream newOutputStream() throws IOException { 45 | final Path outputFile = command.getOutputFile(); 46 | final OutputStream output = getBufferSize() == 0 47 | ? Files.newOutputStream(outputFile) 48 | : new BufferedOutputStream(Files.newOutputStream(outputFile), getBufferSize()); 49 | 50 | if (command.isZipOutput()) { 51 | final ZipOutputStream zipStream = new ZipOutputStream(output); 52 | final String name = getOutputFileName(); 53 | final String entryName = Strings.CS.removeEnd(name, ".zip"); 54 | zipStream.putNextEntry(new ZipEntry(entryName)); 55 | return zipStream; 56 | } 57 | return output; 58 | } 59 | 60 | protected InputStream newInputStream(final Path inputFile) throws IOException { 61 | return isStdinInput() 62 | ? System.in 63 | : Files.newInputStream(inputFile); 64 | } 65 | 66 | public boolean isStdinInput() { 67 | final String name = command.getInputFile().getFileName().toString(); 68 | return Strings.CS.equalsAny(name, "-", "stdin", "0"); 69 | } 70 | 71 | private static SanitizeCommand validate(final SanitizeCommand command) { 72 | final Path outputFile = command.getOutputFile(); 73 | 74 | Validate.isTrue(!command.getInputFile().equals(outputFile), "input and output files cannot be the same"); 75 | return command; 76 | } 77 | 78 | private String getOutputFileName() { 79 | final Path outputFile = command.getOutputFile(); 80 | return outputFile.getFileName().toString(); 81 | } 82 | 83 | private int getBufferSize() { 84 | final DataSize bufferSize = command.getBufferSize(); 85 | return toIntExact(bufferSize.toBytes()); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/hserr/SanitizeHserrCommandProcessor.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.hserr; 2 | 3 | import com.paypal.heapdumptool.cli.CliCommandProcessor; 4 | import com.paypal.heapdumptool.sanitizer.SanitizeCommand; 5 | import com.paypal.heapdumptool.sanitizer.SanitizeStreamFactory; 6 | import com.paypal.heapdumptool.utils.InternalLogger; 7 | import org.apache.commons.io.IOUtils; 8 | import org.apache.commons.lang3.StringUtils; 9 | 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.nio.charset.StandardCharsets; 13 | import java.time.Instant; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import static com.paypal.heapdumptool.utils.DateTimeTool.getFriendlyDuration; 18 | 19 | public class SanitizeHserrCommandProcessor implements CliCommandProcessor { 20 | 21 | private static final InternalLogger LOGGER = InternalLogger.getLogger(SanitizeHserrCommandProcessor.class); 22 | 23 | private final SanitizeHserrCommand command; 24 | 25 | private final SanitizeStreamFactory streamFactory; 26 | 27 | public SanitizeHserrCommandProcessor(final SanitizeHserrCommand command) { 28 | this(command, new SanitizeStreamFactory(asSanitizeCommand(command))); 29 | } 30 | 31 | public SanitizeHserrCommandProcessor(final SanitizeHserrCommand command, final SanitizeStreamFactory streamFactory) { 32 | this.command = command; 33 | this.streamFactory = streamFactory; 34 | } 35 | 36 | @Override 37 | public void process() throws Exception { 38 | 39 | LOGGER.info("Starting hs_err sanitization"); 40 | LOGGER.info("Input File: {}", command.getInputFile()); 41 | LOGGER.info("Output File: {}", command.getOutputFile()); 42 | 43 | final Instant now = Instant.now(); 44 | try (final InputStream inputStream = streamFactory.newInputStream(); 45 | final OutputStream outputStream = streamFactory.newOutputStream()) { 46 | 47 | final List lines = IOUtils.readLines(inputStream, StandardCharsets.UTF_8); 48 | final List sanitizedLines = sanitize(lines); 49 | 50 | IOUtils.writeLines(sanitizedLines, System.lineSeparator(), outputStream, StandardCharsets.UTF_8); 51 | } 52 | LOGGER.info("Finished hs_err sanitization in {}", getFriendlyDuration(now)); 53 | } 54 | 55 | private List sanitize(final List lines) { 56 | boolean inEnvVarSection = false; 57 | final List sanitizedLines = new ArrayList<>(); 58 | for (final String line : lines) { 59 | String newLine = line; 60 | 61 | if (line.startsWith("Environment Variables:")) { 62 | inEnvVarSection = true; 63 | 64 | } else if (inEnvVarSection) { 65 | if (line.isEmpty()) { 66 | inEnvVarSection = false; 67 | } else { 68 | final String key = StringUtils.substringBefore(line, "="); 69 | newLine = key + "=****"; 70 | } 71 | } 72 | 73 | sanitizedLines.add(newLine); 74 | } 75 | return sanitizedLines; 76 | } 77 | 78 | private static SanitizeCommand asSanitizeCommand(final SanitizeHserrCommand command) { 79 | final SanitizeCommand sanitizeCommand = new SanitizeCommand(); 80 | sanitizeCommand.setInputFile(command.getInputFile()); 81 | sanitizeCommand.setOutputFile(command.getOutputFile()); 82 | return sanitizeCommand; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/Application.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool; 2 | 3 | import com.paypal.heapdumptool.capture.CaptureCommand; 4 | import com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation; 5 | import com.paypal.heapdumptool.hserr.SanitizeHserrCommand; 6 | import com.paypal.heapdumptool.sanitizer.DataSize; 7 | import com.paypal.heapdumptool.sanitizer.SanitizeCommand; 8 | import com.paypal.heapdumptool.utils.InternalLogger; 9 | import org.apache.commons.text.StringSubstitutor; 10 | import picocli.CommandLine; 11 | import picocli.CommandLine.Command; 12 | import picocli.CommandLine.HelpCommand; 13 | import picocli.CommandLine.IVersionProvider; 14 | 15 | import java.io.ByteArrayInputStream; 16 | import java.io.IOException; 17 | import java.util.Properties; 18 | 19 | import static com.paypal.heapdumptool.Application.APP_ID; 20 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation.REQUIRED_AND_PROMPTED; 21 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.escalatePrivilegesIfNeeded; 22 | import static org.apache.commons.io.IOUtils.resourceToByteArray; 23 | 24 | @Command(name = APP_ID, 25 | description = "Tool primarily for capturing or sanitizing heap dumps", 26 | mixinStandardHelpOptions = true, 27 | versionProvider = Application.class, 28 | subcommands = { 29 | CaptureCommand.class, 30 | SanitizeCommand.class, 31 | SanitizeHserrCommand.class, 32 | HelpCommand.class, 33 | } 34 | ) 35 | public class Application implements IVersionProvider { 36 | 37 | public static final String APP_ID = "heap-dump-tool"; 38 | 39 | private final String versionResource = System.getProperty("heap-dump-tool.version-resource", "/git-heap-dump-tool.properties"); 40 | 41 | // Stay with "String[] args". vararg "String... args" causes weird failure with mockito 42 | public static void main(final String[] args) throws Exception { 43 | final CommandLine commandLine = newCommandLine(); 44 | 45 | final Escalation escalation = escalatePrivilegesIfNeeded(commandLine, args); 46 | if (escalation == REQUIRED_AND_PROMPTED) { 47 | return; 48 | } 49 | 50 | final int exitCode = commandLine.execute(args); 51 | systemExit(exitCode); 52 | } 53 | 54 | static CommandLine newCommandLine() { 55 | final CommandLine commandLine = new CommandLine(new Application()); 56 | commandLine.setUsageHelpWidth(120); 57 | commandLine.registerConverter(DataSize.class, DataSize::parse); 58 | commandLine.setAbbreviatedOptionsAllowed(true); 59 | return commandLine; 60 | } 61 | 62 | // for mocking 63 | static void systemExit(final int exitCode) { 64 | System.exit(exitCode); 65 | } 66 | 67 | public static void printVersion() throws IOException { 68 | final String[] versionInfo = new Application().getVersion(); 69 | InternalLogger.getLogger(Application.class).info(versionInfo[0]); 70 | } 71 | 72 | @Override 73 | public String[] getVersion() throws IOException { 74 | final byte[] bytes = resourceToByteArray(versionResource); 75 | final Properties gitProperties = new Properties(); 76 | gitProperties.load(new ByteArrayInputStream(bytes)); 77 | gitProperties.put("appId", APP_ID); 78 | 79 | final String versionInfo = StringSubstitutor.replace( 80 | "${appId} (${git.build.version} ${git.commit.id.abbrev}, ${git.commit.time})", 81 | gitProperties); 82 | return new String[]{versionInfo}; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/SanitizeCommandProcessor.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import com.paypal.heapdumptool.cli.CliCommandProcessor; 4 | import com.paypal.heapdumptool.utils.InternalLogger; 5 | import org.apache.commons.io.output.NullOutputStream; 6 | import org.apache.commons.lang3.Validate; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.time.Instant; 12 | 13 | import static com.paypal.heapdumptool.utils.DateTimeTool.getFriendlyDuration; 14 | import static com.paypal.heapdumptool.utils.ProgressMonitor.numBytesProcessedMonitor; 15 | 16 | public class SanitizeCommandProcessor implements CliCommandProcessor { 17 | 18 | private static final InternalLogger LOGGER = InternalLogger.getLogger(SanitizeCommandProcessor.class); 19 | 20 | private final SanitizeCommand command; 21 | 22 | private final SanitizeStreamFactory streamFactory; 23 | 24 | // for mocking 25 | public static SanitizeCommandProcessor newInstance(final SanitizeCommand command, final SanitizeStreamFactory streamFactory) { 26 | return new SanitizeCommandProcessor(command, streamFactory); 27 | } 28 | 29 | public SanitizeCommandProcessor(final SanitizeCommand command) { 30 | this(command, new SanitizeStreamFactory(command)); 31 | } 32 | 33 | public SanitizeCommandProcessor(final SanitizeCommand command, final SanitizeStreamFactory streamFactory) { 34 | Validate.isTrue(command.getBufferSize().toBytes() >= 0, "Invalid buffer size"); 35 | 36 | this.command = command; 37 | this.streamFactory = streamFactory; 38 | } 39 | 40 | @Override 41 | public void process() throws Exception { 42 | if (streamFactory.isStdinInput() && !command.getExcludeStringFields().isEmpty()) { 43 | throw new IllegalArgumentException("stdin input and excludeStringFields cannot be both set to true simultaneously"); 44 | } 45 | Validate.notEmpty(command.getSanitizationText()); 46 | 47 | final Instant now = Instant.now(); 48 | 49 | final HeapDumpSanitizer sanitizer = applyPreprocessing(); 50 | LOGGER.info("Starting heap dump sanitization ..."); 51 | LOGGER.info("Input File: {}", command.getInputFile()); 52 | LOGGER.info("Output File: {}", command.getOutputFile()); 53 | 54 | try (final InputStream inputStream = streamFactory.newInputStream(); 55 | final OutputStream outputStream = streamFactory.newOutputStream()) { 56 | 57 | sanitize(sanitizer, inputStream, outputStream); 58 | } 59 | LOGGER.info("Finished heap dump sanitization in {}", getFriendlyDuration(now)); 60 | } 61 | 62 | private HeapDumpSanitizer applyPreprocessing() throws IOException { 63 | final HeapDumpSanitizer sanitizerPrototype = new HeapDumpSanitizer(); 64 | if (command.getExcludeStringFields().isEmpty() && !command.isForceMatchStringCoder()) { 65 | return sanitizerPrototype; 66 | } 67 | 68 | LOGGER.info("Pre-processing ..."); 69 | LOGGER.info(" String fields to exclude from sanitization: {}", String.join(",", command.getExcludeStringFields())); 70 | LOGGER.info(" Force match String.coder: {}", command.isForceMatchStringCoder()); 71 | LOGGER.info("Input File: {}", command.getInputFile()); 72 | 73 | try (final InputStream inputStream = streamFactory.newInputStream(); 74 | final OutputStream outputStream = NullOutputStream.INSTANCE) { 75 | 76 | sanitize(sanitizerPrototype, inputStream, outputStream); 77 | } 78 | return sanitizerPrototype; 79 | } 80 | 81 | private void sanitize(final HeapDumpSanitizer sanitizer, 82 | final InputStream inputStream, 83 | final OutputStream outputStream) throws IOException { 84 | sanitizer.setInputStream(inputStream); 85 | sanitizer.setOutputStream(outputStream); 86 | sanitizer.setProgressMonitor(numBytesProcessedMonitor(command.getBufferSize(), LOGGER)); 87 | sanitizer.setSanitizeCommand(command); 88 | 89 | sanitizer.sanitize(); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/capture/CaptureCommand.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.capture; 2 | 3 | import com.paypal.heapdumptool.cli.CliCommand; 4 | import com.paypal.heapdumptool.sanitizer.SanitizeOrCaptureCommandBase; 5 | import picocli.CommandLine.Command; 6 | import picocli.CommandLine.Option; 7 | import picocli.CommandLine.Parameters; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString; 15 | import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE; 16 | import static picocli.CommandLine.Help.Visibility.ALWAYS; 17 | 18 | @Command(name = "capture", 19 | description = { 20 | "Capture sanitized heap dump of a containerized app", 21 | "Plain thread dump is also captured" 22 | }, 23 | abbreviateSynopsis = true) 24 | public class CaptureCommand extends SanitizeOrCaptureCommandBase implements CliCommand { 25 | 26 | static final String DOCKER_REGISTRY_OPTION = "--docker-registry"; 27 | static final String SKIP_DOCKER_PULL = "--skip-docker-pull"; 28 | 29 | // to allow field injection from picocli, these variables can't be final 30 | 31 | @Option(names = {SKIP_DOCKER_PULL}, description = "skip pulling latest docker image") 32 | private boolean skipDockerPull; 33 | 34 | @Parameters(index = "0", description = "Container name") 35 | private String containerName; 36 | 37 | @Option(names = { "-p", "--pid" }, description = "Pid within the container, if there are multiple Java processes") 38 | private Long pid; 39 | 40 | @Option(names = { "--heap-cmd" }, description = "Command to capture heap dump", defaultValue = "jcmd PID GC.heap_dump FILE_PATH", showDefaultValue = ALWAYS) 41 | // e.g. set --heap-cmd "jcmd PID GC.heap_dump -all FILE_PATH" to pass -all flag to jcmd heap dump 42 | private List heapCmd = new ArrayList<>(Collections.singletonList("jcmd PID GC.heap_dump FILE_PATH")); 43 | 44 | @Option(names = { "--heap-options" }, description = "Options to heap dump command", defaultValue = "", showDefaultValue = ALWAYS) 45 | // e.g. set --heap-options -all to pass -all flag to jcmd heap dump 46 | private List heapOptions = new ArrayList<>(); 47 | 48 | @Option(names = { "--thread-cmd" }, description = "Command to capture thread dump", defaultValue = "jcmd PID Thread.print -l", showDefaultValue = ALWAYS) 49 | // e.g. set --thread-cmd "jcmd 1 Thread.dump_to_file -format=json -" to thread dump in json format 50 | private List threadCmd = new ArrayList<>(Collections.singletonList("jcmd PID Thread.print -l")); 51 | 52 | @Option(names = { "--thread-option" }, description = "Options to thread dump command", defaultValue = "", showDefaultValue = ALWAYS) 53 | private List threadOptions = new ArrayList<>(); 54 | 55 | @Override 56 | public Class getProcessorClass() { 57 | return CaptureCommandProcessor.class; 58 | } 59 | 60 | public String getContainerName() { 61 | return containerName; 62 | } 63 | 64 | public void setContainerName(final String containerName) { 65 | this.containerName = containerName; 66 | } 67 | 68 | public Long getPid() { 69 | return pid; 70 | } 71 | 72 | public void setPid(final Long pid) { 73 | this.pid = pid; 74 | } 75 | 76 | public List getHeapCmd() { 77 | return splitBySpace(heapCmd); 78 | } 79 | 80 | public void setHeapCmd(final List heapCmd) { 81 | this.heapCmd = heapCmd; 82 | } 83 | 84 | public List getHeapOptions() { 85 | return heapOptions; 86 | } 87 | 88 | public void setHeapOptions(final List heapOptions) { 89 | this.heapOptions = heapOptions; 90 | } 91 | 92 | public List getThreadCmd() { 93 | return splitBySpace(threadCmd); 94 | } 95 | 96 | public void setThreadCmd(final List threadCmd) { 97 | this.threadCmd = threadCmd; 98 | } 99 | 100 | private static List splitBySpace(final List in) { 101 | final List out = new ArrayList<>(); 102 | for (final String token : in) { 103 | out.addAll(Arrays.asList(token.split("\\s+"))); 104 | } 105 | return out; 106 | } 107 | 108 | public List getThreadOptions() { 109 | return threadOptions; 110 | } 111 | 112 | public void setThreadOptions(final List threadOptions) { 113 | this.threadOptions = threadOptions; 114 | } 115 | 116 | @Override 117 | public String toString() { 118 | return reflectionToString(this, MULTI_LINE_STYLE); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/Pipe.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import com.paypal.heapdumptool.utils.ProgressMonitor; 4 | import org.apache.commons.io.IOUtils; 5 | import org.apache.commons.io.input.BoundedInputStream; 6 | import org.apache.commons.lang3.Validate; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.DataInputStream; 10 | import java.io.DataOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.OutputStream; 14 | import java.nio.charset.StandardCharsets; 15 | 16 | /** 17 | * For piping or copying data from input to output streams. 18 | * Along the way, different data can be written by calling {@link #copyFrom(InputStream, long)} or {@link #writeU1(int)} methods. 19 | */ 20 | public class Pipe { 21 | 22 | private final DataInputStream input; 23 | private final DataOutputStream output; 24 | private Integer idSize; 25 | 26 | public Pipe(final InputStream input, final OutputStream output, final ProgressMonitor numBytesWrittenMonitor) { 27 | this.input = new DataInputStream(input); 28 | this.output = new DataOutputStream(numBytesWrittenMonitor.monitoredOutputStream(output)); 29 | } 30 | 31 | private Pipe(final DataInputStream input, final DataOutputStream output, final Integer idSize) { 32 | this.input = input; 33 | this.output = output; 34 | this.idSize = idSize; 35 | } 36 | 37 | /** 38 | * Creates a copy of this pipe where only up to give count of bytes can read from input stream 39 | */ 40 | @SuppressWarnings("deprecation") 41 | public Pipe newInputBoundedPipe(final long inputCount) { 42 | final DataInputStream boundedInput = new DataInputStream(new BoundedInputStream(input, inputCount)); 43 | return new Pipe(boundedInput, output, idSize); 44 | } 45 | 46 | public int getIdSize() { 47 | return idSize; 48 | } 49 | 50 | public void setIdSize(final int idSize) { 51 | Validate.isTrue(idSize == 4 || idSize == 8, "Unknown id size: %s", idSize); 52 | this.idSize = idSize; 53 | } 54 | 55 | public int readU1() throws IOException { 56 | return input.read(); 57 | } 58 | 59 | public byte[] read(final long numBytes) throws IOException { 60 | final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 61 | IOUtils.copyLarge(input, byteArrayOutputStream, 0, numBytes); 62 | return byteArrayOutputStream.toByteArray(); 63 | } 64 | 65 | public void writeU1(final int u1) throws IOException { 66 | output.write(u1); 67 | } 68 | 69 | public void write(final byte[] bytes) throws IOException { 70 | IOUtils.write(bytes, output); 71 | } 72 | 73 | public void copyFrom(final InputStream inputStream, final long count) throws IOException { 74 | IOUtils.copyLarge(inputStream, output, 0, count); 75 | } 76 | 77 | public int pipeU1() throws IOException { 78 | final int u1 = input.read(); 79 | output.write(u1); 80 | return u1; 81 | } 82 | 83 | public int pipeU1IfPossible() throws IOException { 84 | final int u1 = input.read(); 85 | if (u1 != -1) { 86 | output.write(u1); 87 | } 88 | return u1; 89 | } 90 | 91 | public int pipeU2() throws IOException { 92 | final int u2 = input.readShort(); 93 | output.writeShort(u2); 94 | return u2; 95 | } 96 | 97 | public long pipeU4() throws IOException { 98 | final int u4 = input.readInt(); 99 | output.writeInt(u4); 100 | return Integer.toUnsignedLong(u4); 101 | } 102 | 103 | public long pipeId() throws IOException { 104 | if (idSize == 4) { 105 | return pipeU4(); 106 | } else { 107 | final long value = input.readLong(); 108 | output.writeLong(value); 109 | Validate.isTrue(value >= 0, "Small unsigned long expected"); 110 | return value; 111 | } 112 | } 113 | 114 | public void pipe(final long count) throws IOException { 115 | IOUtils.copyLarge(input, output, 0, count); 116 | } 117 | 118 | public void skipInput(final long count) throws IOException { 119 | IOUtils.skipFully(input, count); 120 | } 121 | 122 | public String pipeNullTerminatedString() throws IOException { 123 | int byteValue = Integer.MAX_VALUE; 124 | final StringBuilder sb = new StringBuilder(); 125 | while (byteValue > 0) { 126 | byteValue = input.read(); 127 | if (byteValue >= 0) { 128 | output.write(byteValue); 129 | sb.append((char) byteValue); 130 | } 131 | } 132 | return sb.toString(); 133 | } 134 | 135 | public String pipeString(final long numBytes) throws IOException { 136 | final byte[] bytes = read(numBytes); 137 | write(bytes); 138 | return new String(bytes, StandardCharsets.UTF_8); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/sanitizer/SanitizeStreamFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import com.paypal.heapdumptool.fixture.ResourceTool; 4 | import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.io.TempDir; 7 | 8 | import java.io.BufferedInputStream; 9 | import java.io.BufferedOutputStream; 10 | import java.io.IOException; 11 | import java.io.PrintStream; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.util.zip.ZipOutputStream; 16 | 17 | import static com.paypal.heapdumptool.sanitizer.DataSize.ofBytes; 18 | import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 21 | 22 | public class SanitizeStreamFactoryTest { 23 | 24 | @TempDir 25 | Path tempDir; 26 | 27 | private SanitizeStreamFactory streamFactory; 28 | 29 | @Test 30 | public void testStdinInputStream() throws IOException { 31 | final SanitizeCommand cmd = newCommand(); 32 | cmd.setInputFile(Paths.get("-")); 33 | cmd.setBufferSize(ofBytes(0)); 34 | 35 | streamFactory = new SanitizeStreamFactory(cmd); 36 | assertThat(streamFactory.newInputStream()) 37 | .isEqualTo(System.in); 38 | } 39 | 40 | @Test 41 | public void testBufferedInputStream() throws IOException { 42 | final SanitizeCommand cmd = newCommand(); 43 | cmd.setInputFile(Paths.get("-")); 44 | 45 | streamFactory = new SanitizeStreamFactory(cmd); 46 | assertThat(streamFactory.newInputStream()) 47 | .isInstanceOf(BufferedInputStream.class) 48 | .isNotSameAs(System.in); 49 | } 50 | 51 | @Test 52 | public void testBufferedOutputStream() throws IOException { 53 | final SanitizeCommand cmd = newCommand(); 54 | cmd.setOutputFile(tempDir.resolve("testBufferedOutputStream")); 55 | 56 | streamFactory = new SanitizeStreamFactory(cmd); 57 | assertThat(streamFactory.newOutputStream()) 58 | .isInstanceOf(BufferedOutputStream.class); 59 | } 60 | 61 | @Test 62 | public void testFileInputStream() throws IOException { 63 | final Path inputFile = Files.createTempFile(tempDir, getClass().getSimpleName(), ".hprof"); 64 | final SanitizeCommand cmd = newCommand(); 65 | cmd.setInputFile(inputFile); 66 | cmd.setBufferSize(ofBytes(0)); 67 | 68 | streamFactory = new SanitizeStreamFactory(cmd); 69 | assertThat(streamFactory.newInputStream()) 70 | .isNotInstanceOf(PrintStream.class); 71 | } 72 | 73 | @Test 74 | public void testFileOutputStream() throws IOException { 75 | final Path file = Files.createTempFile(tempDir, getClass().getSimpleName(), ".hprof"); 76 | final SanitizeCommand cmd = newCommand(); 77 | cmd.setInputFile(Paths.get("foo")); 78 | cmd.setOutputFile(file); 79 | cmd.setBufferSize(ofBytes(0)); 80 | 81 | streamFactory = new SanitizeStreamFactory(cmd); 82 | assertThat(streamFactory.newOutputStream()) 83 | .isNotInstanceOf(PrintStream.class); 84 | } 85 | 86 | @Test 87 | public void testTarInputStream() throws IOException { 88 | final Path inputFile = Files.createTempFile(tempDir, getClass().getSimpleName(), ".hprof"); 89 | writeTar(inputFile); 90 | 91 | final SanitizeCommand cmd = newCommand(); 92 | cmd.setInputFile(inputFile); 93 | cmd.setBufferSize(ofBytes(0)); 94 | cmd.setTarInput(true); 95 | 96 | streamFactory = new SanitizeStreamFactory(cmd); 97 | assertThat(streamFactory.newInputStream()) 98 | .isInstanceOf(TarArchiveInputStream.class); 99 | } 100 | 101 | @Test 102 | public void testZipOutputStream() throws IOException { 103 | final Path outputFile = Files.createTempFile(tempDir, getClass().getSimpleName(), ".zip"); 104 | 105 | final SanitizeCommand cmd = newCommand(); 106 | cmd.setInputFile(Paths.get("foo")); 107 | cmd.setOutputFile(outputFile); 108 | cmd.setBufferSize(ofBytes(0)); 109 | cmd.setZipOutput(true); 110 | 111 | streamFactory = new SanitizeStreamFactory(cmd); 112 | assertThat(streamFactory.newOutputStream()) 113 | .isInstanceOf(ZipOutputStream.class); 114 | } 115 | 116 | @Test 117 | public void testSameInputOutput() { 118 | final SanitizeCommand cmd = newCommand(); 119 | cmd.setInputFile(Paths.get("foo")); 120 | cmd.setOutputFile(Paths.get("foo")); 121 | 122 | assertThatIllegalArgumentException() 123 | .isThrownBy(() -> new SanitizeStreamFactory(cmd)); 124 | } 125 | 126 | private void writeTar(final Path destPath) throws IOException { 127 | final byte[] srcBytes = ResourceTool.bytesOf(getClass(), "sample.tar"); 128 | Files.write(destPath, srcBytes, TRUNCATE_EXISTING); 129 | } 130 | 131 | private SanitizeCommand newCommand() { 132 | final SanitizeCommand cmd = new SanitizeCommand(); 133 | cmd.setInputFile(Paths.get("input.txt")); 134 | cmd.setOutputFile(Paths.get("output.txt")); 135 | return cmd; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/sanitizer/PipeTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.IOException; 9 | import java.util.concurrent.atomic.AtomicLong; 10 | 11 | import static java.nio.charset.StandardCharsets.UTF_8; 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 14 | 15 | public class PipeTest { 16 | 17 | private final String data = "hello world\0more-stuff-here"; 18 | 19 | private final ByteArrayInputStream inputBytes = byteStreamOf(data); 20 | 21 | private final ByteArrayOutputStream outputBytes = new ByteArrayOutputStream(); 22 | 23 | private final AtomicLong monitor = new AtomicLong(); 24 | 25 | private final Pipe pipe = new Pipe(inputBytes, outputBytes, monitor::set); 26 | 27 | @Test 28 | public void testIdSizeSetGet() { 29 | pipe.setIdSize(4); 30 | assertThat(pipe.getIdSize()) 31 | .isEqualTo(4); 32 | 33 | pipe.setIdSize(8); 34 | assertThat(pipe.getIdSize()) 35 | .isEqualTo(8); 36 | } 37 | 38 | @Test 39 | @DisplayName("testIdSizeNullDefault. check that NPE is thrown") 40 | public void testIdSizeNullDefault() { 41 | assertThatThrownBy(pipe::getIdSize) 42 | .isInstanceOf(NullPointerException.class); 43 | } 44 | 45 | @Test 46 | public void testIdSize4Or8() { 47 | assertThatThrownBy(() -> pipe.setIdSize(10)) 48 | .isInstanceOf(IllegalArgumentException.class) 49 | .hasMessage("Unknown id size: 10"); 50 | } 51 | 52 | @Test 53 | public void testReadU1() throws IOException { 54 | assertThat(pipe.readU1()) 55 | .isEqualTo('h'); 56 | 57 | pipe.skipInput(data.length() - 1); 58 | verifyEoF(); 59 | 60 | assertThat(outputBytes.toByteArray()) 61 | .hasSize(0); 62 | } 63 | 64 | @Test 65 | public void testWriteU1() throws IOException { 66 | pipe.writeU1('z'); 67 | assertThat(outputString()) 68 | .isEqualTo("z"); 69 | 70 | verifyInputStreamUnchanged(); 71 | } 72 | 73 | @Test 74 | public void testPipeByLength() throws IOException { 75 | pipe.pipe(data.length()); 76 | verifyEoF(); 77 | assertThat(outputString()) 78 | .isEqualTo(data); 79 | } 80 | 81 | @Test 82 | public void testPipeId4() throws IOException { 83 | pipe.setIdSize(4); 84 | pipe.pipeId(); 85 | 86 | assertThat(outputString()) 87 | .isEqualTo("hell") 88 | .hasSize(4); 89 | } 90 | 91 | @Test 92 | public void testPipeId8() throws IOException { 93 | pipe.setIdSize(8); 94 | pipe.pipeId(); 95 | 96 | assertThat(outputString()) 97 | .isEqualTo("hello wo") 98 | .hasSize(8); 99 | } 100 | 101 | @Test 102 | public void testCopyFrom() throws IOException { 103 | final String newData = "byte stream data"; 104 | pipe.copyFrom(byteStreamOf(newData), newData.length()); 105 | 106 | verifyInputStreamUnchanged(); 107 | 108 | assertThat(outputString()) 109 | .isEqualTo(newData); 110 | } 111 | 112 | @Test 113 | public void testPipeU1() throws IOException { 114 | final int u1 = pipe.pipeU1(); 115 | assertThat(u1) 116 | .isEqualTo('h'); 117 | 118 | assertThat(outputString()) 119 | .isEqualTo("h"); 120 | assertThat(inputBytes.read()) 121 | .isEqualTo('e'); 122 | } 123 | 124 | @Test 125 | public void testPipeU1IfPossible() throws IOException { 126 | final int u1 = pipe.pipeU1IfPossible(); 127 | assertThat(u1) 128 | .isEqualTo('h'); 129 | 130 | assertThat(outputString()) 131 | .isEqualTo("h"); 132 | assertThat(inputBytes.read()) 133 | .isEqualTo('e'); 134 | } 135 | 136 | @Test 137 | @DisplayName("pipe u1 on exhausted input") 138 | public void testPipeU1IfPossibleNot() throws IOException { 139 | pipe.pipe(100); 140 | final int u1 = pipe.pipeU1IfPossible(); 141 | assertThat(u1) 142 | .isEqualTo(-1); 143 | 144 | assertThat(outputString()) 145 | .isEqualTo(data); 146 | } 147 | 148 | @Test 149 | public void testPipeU2() throws IOException { 150 | pipe.pipeU2(); 151 | assertThat(inputBytes.read()) 152 | .isEqualTo('l'); 153 | assertThat(outputString()) 154 | .isEqualTo("he"); 155 | } 156 | 157 | @Test 158 | public void testPipeNullTerminatedString() throws IOException { 159 | assertThat(pipe.pipeNullTerminatedString()) 160 | .isEqualTo("hello world\0") 161 | .isEqualTo(outputString()); 162 | } 163 | 164 | @Test 165 | public void testNewInputBoundedPipe() throws IOException { 166 | pipe.pipeU1(); 167 | 168 | final Pipe boundedPipe = pipe.newInputBoundedPipe(4); 169 | assertThat(boundedPipe.pipeNullTerminatedString()) 170 | .isEqualTo("ello"); 171 | 172 | assertThat(outputString()) 173 | .isEqualTo("hello"); 174 | 175 | assertThat(pipe.pipeNullTerminatedString()) 176 | .isEqualTo(" world\0"); 177 | assertThat(outputString()) 178 | .isEqualTo("hello world\0"); 179 | } 180 | 181 | @Test 182 | public void testProgressMonitor() throws IOException { 183 | pipe.pipeU1(); 184 | assertThat(monitor) 185 | .hasValue(1); 186 | 187 | pipe.pipe(100); 188 | assertThat(monitor) 189 | .hasValue(data.length()); 190 | } 191 | 192 | private void verifyEoF() throws IOException { 193 | assertThat(pipe.readU1()) 194 | .isEqualTo(-1); 195 | } 196 | 197 | private void verifyInputStreamUnchanged() { 198 | assertThat(inputBytes.read()) 199 | .isEqualTo('h'); 200 | } 201 | 202 | private String outputString() throws IOException { 203 | return outputBytes.toString("UTF-8"); 204 | } 205 | 206 | private ByteArrayInputStream byteStreamOf(final String str) { 207 | return new ByteArrayInputStream(bytesOf(str)); 208 | } 209 | 210 | private byte[] bytesOf(final String str) { 211 | return str.getBytes(UTF_8); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/capture/PrivilegeEscalator.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.capture; 2 | 3 | import com.paypal.heapdumptool.utils.ProcessTool; 4 | import com.paypal.heapdumptool.utils.ProcessTool.ProcessResult; 5 | import org.apache.commons.lang3.RuntimeEnvironment; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.apache.commons.text.StringSubstitutor; 8 | import picocli.CommandLine; 9 | import picocli.CommandLine.ParseResult; 10 | 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Optional; 19 | import java.util.concurrent.Callable; 20 | import java.util.stream.Stream; 21 | 22 | import static com.paypal.heapdumptool.capture.CaptureCommand.DOCKER_REGISTRY_OPTION; 23 | import static com.paypal.heapdumptool.capture.CaptureCommand.SKIP_DOCKER_PULL; 24 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation.REQUIRED_AND_PROMPTED; 25 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation.NOT_REQUIRED; 26 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation.PRIVILEGED_ALREADY; 27 | import static com.paypal.heapdumptool.utils.CallableTool.callQuietlyWithDefault; 28 | import static java.nio.charset.StandardCharsets.UTF_8; 29 | import static java.util.stream.Collectors.joining; 30 | import static org.apache.commons.io.IOUtils.resourceToString; 31 | 32 | /** 33 | * Makes it possible to run docker inside docker. Prints a "docker run" command which the user can execute so that the container 34 | * has the right flags (--privileged and --pid) set. 35 | */ 36 | public class PrivilegeEscalator { 37 | 38 | public static final String DEFAULT_REGISTRY = System.getProperty("hdt.default-registry", "index.docker.io"); 39 | public static final String IMAGE_NAME = System.getProperty("hdt.image-name", "heapdumptool/heapdumptool"); 40 | private static final String DOCKER = "docker"; 41 | 42 | public static Escalation escalatePrivilegesIfNeeded(final CommandLine commandLine, final String... args) throws Exception { 43 | final ParseResult parseResult = parse(commandLine, args); 44 | final ParseResult subcommand = parseResult.subcommand(); 45 | if (subcommand != null && subcommand.commandSpec() != null) { 46 | final Object userObject = subcommand.commandSpec().userObject(); 47 | if (!(userObject instanceof CaptureCommand)) { 48 | return NOT_REQUIRED; 49 | } 50 | } 51 | 52 | if (!isInDockerContainer() || isLikelyPrivileged()) { 53 | return PRIVILEGED_ALREADY; 54 | } 55 | 56 | final String shellTemplate = resourceToString("/privilege-escalate.sh.tmpl", UTF_8); 57 | final Map templateParams = buildTemplateParams(args); 58 | 59 | final String shellCode = StringSubstitutor.replace(shellTemplate, templateParams, "__", "__"); 60 | println(shellCode); 61 | return REQUIRED_AND_PROMPTED; 62 | } 63 | 64 | public enum Escalation { 65 | REQUIRED_AND_PROMPTED, 66 | NOT_REQUIRED, 67 | PRIVILEGED_ALREADY 68 | } 69 | 70 | public static boolean isInDockerContainer() { 71 | return RuntimeEnvironment.inContainer(); 72 | } 73 | 74 | // yes, print directly to stdout (bypassing SysOutOverSLF4J or other logger decorations) 75 | static void println(final String str) { 76 | System.out.println(str); 77 | } 78 | 79 | private static boolean isLikelyPrivileged() { 80 | final Callable canRunDockerInsideDocker = () -> { 81 | final Path nsenter1 = Paths.get("nsenter1"); 82 | final ProcessResult result = ProcessTool.run(nsenter1.toString(), DOCKER); 83 | return result.exitCode == 0; 84 | }; 85 | return callQuietlyWithDefault(false, canRunDockerInsideDocker); 86 | } 87 | 88 | private static ParseResult parse(final CommandLine commandLine, final String... args) { 89 | final Optional forcedDockerRegistry = findForcedDockerRegistry(args); 90 | final List argsList = new ArrayList<>(Arrays.asList(args)); 91 | if (forcedDockerRegistry.isPresent()) { 92 | argsList.remove(DOCKER_REGISTRY_OPTION + "=" + forcedDockerRegistry.get()); 93 | argsList.remove(DOCKER_REGISTRY_OPTION); 94 | argsList.remove(forcedDockerRegistry.get()); 95 | } 96 | return commandLine.parseArgs(argsList.toArray(new String[0])); 97 | } 98 | 99 | private static Map buildTemplateParams(final String... args) { 100 | final String quotedArgs = Stream.of(args) 101 | .map(PrivilegeEscalator::quoteIfNeeded) 102 | .collect(joining(" ")); 103 | 104 | final Optional forcedDockerRegistry = findForcedDockerRegistry(args); 105 | final String defaultRegistry = forcedDockerRegistry.orElse(DEFAULT_REGISTRY); 106 | 107 | final String dockerRegistryEnvName = forcedDockerRegistry.isPresent() 108 | ? "FORCED_DOCKER_REGISTRY" 109 | : "DOCKER_REGISTRY"; 110 | 111 | final boolean skipDockerPull = Arrays.asList(args).contains(SKIP_DOCKER_PULL); 112 | 113 | final Map params = new HashMap<>(); 114 | params.put("IMAGE_NAME", IMAGE_NAME); 115 | params.put("ARGS", quotedArgs); 116 | params.put("DEFAULT_REGISTRY", defaultRegistry); 117 | params.put("DOCKER_REGISTRY_ENV_NAME", dockerRegistryEnvName); 118 | params.put("SKIP_DOCKER_PULL", Boolean.toString(skipDockerPull)); 119 | 120 | return params; 121 | } 122 | 123 | private static Optional findForcedDockerRegistry(final String... args) { 124 | String forcedDockerRegistry = null; 125 | for (int i = 0; i < args.length; i++) { 126 | final String arg = args[i]; 127 | 128 | if (arg.startsWith(DOCKER_REGISTRY_OPTION)) { 129 | if (arg.contains("=")) { 130 | forcedDockerRegistry = StringUtils.substringAfterLast(arg, "="); 131 | break; 132 | } 133 | 134 | if (i < args.length - 1) { 135 | forcedDockerRegistry = args[i + 1]; 136 | break; 137 | } 138 | 139 | throw new IllegalArgumentException("Cannot find argument value for " + DOCKER_REGISTRY_OPTION); 140 | } 141 | } 142 | return Optional.ofNullable(forcedDockerRegistry); 143 | } 144 | 145 | private static String quoteIfNeeded(final String str) { 146 | return str.contains(" ") 147 | ? "\"" + str + "\"" 148 | : str; 149 | } 150 | 151 | private PrivilegeEscalator() { 152 | throw new AssertionError(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/sanitizer/DataSizeTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.paypal.heapdumptool.sanitizer; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 23 | 24 | /** 25 | * Tests for {@link DataSize}. 26 | * 27 | * @author Stephane Nicoll 28 | */ 29 | public class DataSizeTests { 30 | 31 | @Test 32 | void ofBytesToBytes() { 33 | assertThat(DataSize.ofBytes(1024).toBytes()).isEqualTo(1024); 34 | } 35 | 36 | @Test 37 | void ofBytesToKilobytes() { 38 | assertThat(DataSize.ofBytes(1024).toKilobytes()).isEqualTo(1); 39 | } 40 | 41 | @Test 42 | void ofKilobytesToKilobytes() { 43 | assertThat(DataSize.ofKilobytes(1024).toKilobytes()).isEqualTo(1024); 44 | } 45 | 46 | @Test 47 | void ofKilobytesToMegabytes() { 48 | assertThat(DataSize.ofKilobytes(1024).toMegabytes()).isEqualTo(1); 49 | } 50 | 51 | @Test 52 | void ofMegabytesToMegabytes() { 53 | assertThat(DataSize.ofMegabytes(1024).toMegabytes()).isEqualTo(1024); 54 | } 55 | 56 | @Test 57 | void ofMegabytesToGigabytes() { 58 | assertThat(DataSize.ofMegabytes(2048).toGigabytes()).isEqualTo(2); 59 | } 60 | 61 | @Test 62 | void ofGigabytesToGigabytes() { 63 | assertThat(DataSize.ofGigabytes(4096).toGigabytes()).isEqualTo(4096); 64 | } 65 | 66 | @Test 67 | void ofGigabytesToTerabytes() { 68 | assertThat(DataSize.ofGigabytes(4096).toTerabytes()).isEqualTo(4); 69 | } 70 | 71 | @Test 72 | void ofTerabytesToGigabytes() { 73 | assertThat(DataSize.ofTerabytes(1).toGigabytes()).isEqualTo(1024); 74 | } 75 | 76 | @Test 77 | void ofWithBytesUnit() { 78 | assertThat(DataSize.of(10, DataUnit.BYTES)).isEqualTo(DataSize.ofBytes(10)); 79 | } 80 | 81 | @Test 82 | void ofWithKilobytesUnit() { 83 | assertThat(DataSize.of(20, DataUnit.KILOBYTES)).isEqualTo(DataSize.ofKilobytes(20)); 84 | } 85 | 86 | @Test 87 | void ofWithMegabytesUnit() { 88 | assertThat(DataSize.of(30, DataUnit.MEGABYTES)).isEqualTo(DataSize.ofMegabytes(30)); 89 | } 90 | 91 | @Test 92 | void ofWithGigabytesUnit() { 93 | assertThat(DataSize.of(40, DataUnit.GIGABYTES)).isEqualTo(DataSize.ofGigabytes(40)); 94 | } 95 | 96 | @Test 97 | void ofWithTerabytesUnit() { 98 | assertThat(DataSize.of(50, DataUnit.TERABYTES)).isEqualTo(DataSize.ofTerabytes(50)); 99 | } 100 | 101 | @Test 102 | void parseWithDefaultUnitUsesBytes() { 103 | assertThat(DataSize.parse("1024")).isEqualTo(DataSize.ofKilobytes(1)); 104 | } 105 | 106 | @Test 107 | void parseNegativeNumberWithDefaultUnitUsesBytes() { 108 | assertThat(DataSize.parse("-1")).isEqualTo(DataSize.ofBytes(-1)); 109 | } 110 | 111 | @Test 112 | void parseWithNullDefaultUnitUsesBytes() { 113 | assertThat(DataSize.parse("1024", null)).isEqualTo(DataSize.ofKilobytes(1)); 114 | } 115 | 116 | @Test 117 | void parseNegativeNumberWithNullDefaultUnitUsesBytes() { 118 | assertThat(DataSize.parse("-1024", null)).isEqualTo(DataSize.ofKilobytes(-1)); 119 | } 120 | 121 | @Test 122 | void parseWithCustomDefaultUnit() { 123 | assertThat(DataSize.parse("1", DataUnit.KILOBYTES)).isEqualTo(DataSize.ofKilobytes(1)); 124 | } 125 | 126 | @Test 127 | void parseNegativeNumberWithCustomDefaultUnit() { 128 | assertThat(DataSize.parse("-1", DataUnit.KILOBYTES)).isEqualTo(DataSize.ofKilobytes(-1)); 129 | } 130 | 131 | @Test 132 | void parseWithBytes() { 133 | assertThat(DataSize.parse("1024B")).isEqualTo(DataSize.ofKilobytes(1)); 134 | } 135 | 136 | @Test 137 | void parseWithNegativeBytes() { 138 | assertThat(DataSize.parse("-1024B")).isEqualTo(DataSize.ofKilobytes(-1)); 139 | } 140 | 141 | @Test 142 | void parseWithPositiveBytes() { 143 | assertThat(DataSize.parse("+1024B")).isEqualTo(DataSize.ofKilobytes(1)); 144 | } 145 | 146 | @Test 147 | void parseWithKilobytes() { 148 | assertThat(DataSize.parse("1KB")).isEqualTo(DataSize.ofBytes(1024)); 149 | } 150 | 151 | @Test 152 | void parseWithNegativeKilobytes() { 153 | assertThat(DataSize.parse("-1KB")).isEqualTo(DataSize.ofBytes(-1024)); 154 | } 155 | 156 | @Test 157 | void parseWithMegabytes() { 158 | assertThat(DataSize.parse("4MB")).isEqualTo(DataSize.ofMegabytes(4)); 159 | } 160 | 161 | @Test 162 | void parseWithNegativeMegabytes() { 163 | assertThat(DataSize.parse("-4MB")).isEqualTo(DataSize.ofMegabytes(-4)); 164 | } 165 | 166 | @Test 167 | void parseWithGigabytes() { 168 | assertThat(DataSize.parse("1GB")).isEqualTo(DataSize.ofMegabytes(1024)); 169 | } 170 | 171 | @Test 172 | void parseWithNegativeGigabytes() { 173 | assertThat(DataSize.parse("-1GB")).isEqualTo(DataSize.ofMegabytes(-1024)); 174 | } 175 | 176 | @Test 177 | void parseWithTerabytes() { 178 | assertThat(DataSize.parse("1TB")).isEqualTo(DataSize.ofTerabytes(1)); 179 | } 180 | 181 | @Test 182 | void parseWithNegativeTerabytes() { 183 | assertThat(DataSize.parse("-1TB")).isEqualTo(DataSize.ofTerabytes(-1)); 184 | } 185 | 186 | @Test 187 | void isNegativeWithPositive() { 188 | assertThat(DataSize.ofBytes(50).isNegative()).isFalse(); 189 | } 190 | 191 | @Test 192 | void isNegativeWithZero() { 193 | assertThat(DataSize.ofBytes(0).isNegative()).isFalse(); 194 | } 195 | 196 | @Test 197 | void isNegativeWithNegative() { 198 | assertThat(DataSize.ofBytes(-1).isNegative()).isTrue(); 199 | } 200 | 201 | @Test 202 | void toStringUsesBytes() { 203 | assertThat(DataSize.ofKilobytes(1).toString()).isEqualTo("1024B"); 204 | } 205 | 206 | @Test 207 | void toStringWithNegativeBytes() { 208 | assertThat(DataSize.ofKilobytes(-1).toString()).isEqualTo("-1024B"); 209 | } 210 | 211 | @Test 212 | void parseWithUnsupportedUnit() { 213 | assertThatIllegalArgumentException().isThrownBy(() -> DataSize.parse("3WB")) 214 | .withMessage("'3WB' is not a valid data size"); 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/capture/PrivilegeEscalatorTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.capture; 2 | 3 | import com.google.common.io.Closer; 4 | import com.paypal.heapdumptool.Application; 5 | import com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation; 6 | import com.paypal.heapdumptool.fixture.ConstructorTester; 7 | import com.paypal.heapdumptool.fixture.ResourceTool; 8 | import com.paypal.heapdumptool.sanitizer.DataSize; 9 | import org.apache.commons.lang3.RuntimeEnvironment; 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.junit.jupiter.api.io.TempDir; 15 | import org.mockito.MockedStatic; 16 | import org.springframework.boot.test.system.CapturedOutput; 17 | import org.springframework.boot.test.system.OutputCaptureExtension; 18 | import picocli.CommandLine; 19 | 20 | import java.io.IOException; 21 | import java.nio.file.Files; 22 | import java.nio.file.Path; 23 | import java.nio.file.Paths; 24 | 25 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.escalatePrivilegesIfNeeded; 26 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation.REQUIRED_AND_PROMPTED; 27 | import static com.paypal.heapdumptool.capture.PrivilegeEscalator.Escalation.PRIVILEGED_ALREADY; 28 | import static java.nio.charset.StandardCharsets.UTF_8; 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 31 | import static org.mockito.Mockito.mockStatic; 32 | 33 | @ExtendWith(OutputCaptureExtension.class) 34 | public class PrivilegeEscalatorTest { 35 | 36 | @TempDir 37 | Path tempDir; 38 | 39 | private final Closer closer = Closer.create(); 40 | 41 | @BeforeEach 42 | @AfterEach 43 | public void tearDown() throws IOException { 44 | closer.close(); 45 | } 46 | 47 | @Test 48 | public void testNotInDockerContainer() throws Exception { 49 | expectInDockerContainer(false); 50 | 51 | assertThat(escalate("capture", "b a r")) 52 | .isEqualTo(PRIVILEGED_ALREADY); 53 | } 54 | 55 | @Test 56 | public void testInDockerContainerPrivilegedAlready() throws Exception { 57 | expectInDockerContainer(true); 58 | 59 | final Path cgroupPath = Paths.get(copyCgroup("docker-cgroup.txt")); 60 | final Path replacementPath = Paths.get("/bin/echo"); 61 | final MockedStatic mocked = createStaticMock(Paths.class); 62 | expectCgroup(mocked, cgroupPath); 63 | expectNsenter1(mocked, replacementPath); 64 | 65 | assertThat(escalate("capture", "b a r")) 66 | .isEqualTo(PRIVILEGED_ALREADY); 67 | } 68 | 69 | @Test 70 | public void testInDockerContainerNotPrivilegedAlready(final CapturedOutput output) throws Exception { 71 | expectInDockerContainer(true); 72 | 73 | final Path cgroupPath = Paths.get(copyCgroup("docker-cgroup.txt")); 74 | final MockedStatic mocked = createStaticMock(Paths.class); 75 | expectCgroup(mocked, cgroupPath); 76 | 77 | assertThat(escalate("capture", "b a r")) 78 | .isEqualTo(REQUIRED_AND_PROMPTED); 79 | 80 | assertThat(output).contains("If you see this comment while running the tool"); 81 | } 82 | 83 | @Test 84 | public void testCustomDockerRegistryOneArg(final CapturedOutput output) throws Exception { 85 | expectInDockerContainer(true); 86 | 87 | final Path cgroupPath = Paths.get(copyCgroup("docker-cgroup.txt")); 88 | final MockedStatic mocked = createStaticMock(Paths.class); 89 | expectCgroup(mocked, cgroupPath); 90 | 91 | assertThat(escalate("--docker-registry=my-custom-registry.example.com")) 92 | .isEqualTo(REQUIRED_AND_PROMPTED); 93 | assertThat(output.getOut()) 94 | .contains("FQ_IMAGE=\"${FORCED_DOCKER_REGISTRY:-my-custom-registry.example.com}/heapdumptool/heapdumptool\"\n"); 95 | } 96 | 97 | @Test 98 | public void testCustomDockerRegistryTwoArg(final CapturedOutput output) throws Exception { 99 | expectInDockerContainer(true); 100 | 101 | final Path cgroupPath = Paths.get(copyCgroup("docker-cgroup.txt")); 102 | final MockedStatic mocked = createStaticMock(Paths.class); 103 | expectCgroup(mocked, cgroupPath); 104 | 105 | assertThat(escalate("--docker-registry", "my-custom-registry.example.com", "capture", "b a r")) 106 | .isEqualTo(REQUIRED_AND_PROMPTED); 107 | assertThat(output.getOut()) 108 | .contains("FQ_IMAGE=\"${FORCED_DOCKER_REGISTRY:-my-custom-registry.example.com}/heapdumptool/heapdumptool\"\n"); 109 | } 110 | 111 | @Test 112 | public void testCustomDockerRegistryInvalidArg() throws Exception { 113 | final boolean value = true; 114 | expectInDockerContainer(value); 115 | 116 | final Path cgroupPath = Paths.get(copyCgroup("docker-cgroup.txt")); 117 | final MockedStatic mocked = createStaticMock(Paths.class); 118 | expectCgroup(mocked, cgroupPath); 119 | 120 | assertThatExceptionOfType(IllegalArgumentException.class) 121 | .isThrownBy(() -> escalate("--docker-registry")) 122 | .withMessage("Cannot find argument value for --docker-registry"); 123 | } 124 | 125 | @Test 126 | public void testConstructor() throws Exception { 127 | ConstructorTester.test(PrivilegeEscalator.class); 128 | } 129 | 130 | private static Escalation escalate(final String... args) throws Exception { 131 | final CommandLine commandLine = newCommandLine(); 132 | return escalatePrivilegesIfNeeded(commandLine, args); 133 | } 134 | 135 | static CommandLine newCommandLine() { 136 | final CommandLine commandLine = new CommandLine(new Application()); 137 | commandLine.setUsageHelpWidth(120); 138 | commandLine.registerConverter(DataSize.class, DataSize::parse); 139 | commandLine.setAbbreviatedOptionsAllowed(true); 140 | return commandLine; 141 | } 142 | 143 | private void expectInDockerContainer(final boolean value) { 144 | final MockedStatic env = createStaticMock(RuntimeEnvironment.class); 145 | env.when(RuntimeEnvironment::inContainer).thenReturn(value); 146 | } 147 | 148 | private MockedStatic createStaticMock(final Class clazz) { 149 | final MockedStatic mocked = mockStatic(clazz); 150 | closer.register(mocked::close); 151 | return mocked; 152 | } 153 | 154 | private void expectNsenter1(final MockedStatic mocked, final Path replacement) { 155 | mocked.when(() -> Paths.get("nsenter1")) 156 | .thenReturn(replacement); 157 | } 158 | 159 | private void expectCgroup(final MockedStatic mocked, final Path replacement) { 160 | mocked.when(() -> Paths.get("/proc/1/cgroup")) 161 | .thenReturn(replacement); 162 | } 163 | 164 | private String copyCgroup(final String name) throws IOException { 165 | final Path file = copyResourceToFile(name); 166 | return file.toAbsolutePath().toString(); 167 | } 168 | 169 | private Path copyResourceToFile(final String name) throws IOException { 170 | final String content = resourceContent(name); 171 | 172 | final Path file = tempDir.resolve(name); 173 | Files.write(file, content.getBytes(UTF_8)); 174 | return file; 175 | } 176 | 177 | private String resourceContent(final String name) throws IOException { 178 | return ResourceTool.contentOf(getClass(), name); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/SanitizeOrCaptureCommandBase.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import com.paypal.heapdumptool.cli.CliCommand; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.apache.commons.text.StringEscapeUtils; 6 | import picocli.CommandLine.Option; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | import java.util.stream.Collectors; 16 | 17 | import static com.paypal.heapdumptool.sanitizer.DataSize.ofMegabytes; 18 | import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString; 19 | import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE; 20 | import static picocli.CommandLine.Help.Visibility.ALWAYS; 21 | 22 | public abstract class SanitizeOrCaptureCommandBase implements CliCommand { 23 | 24 | static final String DOCKER_REGISTRY_OPTION = "--docker-registry"; 25 | 26 | // to allow field injection from picocli, these variables can't be final 27 | 28 | @Option(names = {"-d", DOCKER_REGISTRY_OPTION}, description = "docker registry hostname for bootstrapping heap-dump-tool docker image") 29 | private String dockerRegistry; 30 | 31 | @Option(names = {"-a", "--tar-input"}, description = "Treat input as tar archive", arity = "1") 32 | private boolean tarInput; 33 | 34 | @Option(names = {"-e", "--exclude-string-fields"}, 35 | description = "String fields to exclude from sanitization. Value in com.example.MyClass#fieldName format", 36 | defaultValue = "java.lang.Thread#name,java.lang.ThreadGroup#name", 37 | showDefaultValue = ALWAYS) 38 | private List excludeStringFields; 39 | 40 | @Option(names = {"-f", "--force-string-coder-match"}, 41 | description = "Force strings coder values to match sanitizationText.coder value", 42 | defaultValue = "true", 43 | arity = "1", 44 | showDefaultValue = ALWAYS) 45 | // Suppose sanitizationText=*. If the coder value is not forced to match, the heap dump analyze tools like Eclipse 46 | // MAT might display escaped "\\u2A" (where 2A is ascii value) for Strings with coder==1. By forcing the coder value to 47 | // match, all strings would be displayed as "*" 48 | private boolean forceMatchStringCoder; 49 | 50 | @Option(names = {"-s", "--sanitize-byte-char-arrays-only"}, 51 | description = "Sanitize byte/char arrays only", 52 | defaultValue = "true", 53 | arity = "1", 54 | showDefaultValue = ALWAYS) 55 | private boolean sanitizeByteCharArraysOnly = true; 56 | 57 | @Option(names = {"-t", "--text"}, description = "Sanitization text to replace with", defaultValue = "\\0", showDefaultValue = ALWAYS) 58 | private String sanitizationText = "\\0"; 59 | 60 | @Option(names = {"-T", "--text-charset"}, 61 | description = "Sanitization text charset", 62 | defaultValue = "", 63 | showDefaultValue = ALWAYS) 64 | private String sanitizationTextCharset = ""; 65 | 66 | private StringFieldMap excludeStringFieldMap; 67 | 68 | @Option(names = {"-b", "--buffer-size"}, description = "Buffer size for reading and writing", defaultValue = "100MB", showDefaultValue = ALWAYS) 69 | private DataSize bufferSize = ofMegabytes(100); 70 | 71 | public void copyFrom(final SanitizeOrCaptureCommandBase other) { 72 | this.dockerRegistry = other.dockerRegistry; 73 | this.bufferSize = other.bufferSize; 74 | this.forceMatchStringCoder = other.forceMatchStringCoder; 75 | this.excludeStringFields = other.excludeStringFields; 76 | this.sanitizationText = other.sanitizationText; 77 | this.sanitizeByteCharArraysOnly = other.sanitizeByteCharArraysOnly; 78 | this.tarInput = other.tarInput; 79 | } 80 | 81 | public DataSize getBufferSize() { 82 | return bufferSize; 83 | } 84 | 85 | public void setBufferSize(final DataSize bufferSize) { 86 | this.bufferSize = bufferSize; 87 | } 88 | 89 | public boolean isSanitizeByteCharArraysOnly() { 90 | return sanitizeByteCharArraysOnly; 91 | } 92 | 93 | public void setSanitizeByteCharArraysOnly(final boolean sanitizeByteCharArraysOnly) { 94 | this.sanitizeByteCharArraysOnly = sanitizeByteCharArraysOnly; 95 | } 96 | 97 | public boolean isTarInput() { 98 | return tarInput; 99 | } 100 | 101 | public void setTarInput(final boolean tarInput) { 102 | this.tarInput = tarInput; 103 | } 104 | 105 | public String getSanitizationText() { 106 | return StringEscapeUtils.unescapeJava(sanitizationText); 107 | } 108 | 109 | public void setSanitizationText(final String sanitizationText) { 110 | // e.g. unescape user-supplied \\0 string (2 chars) to \0 string (1 char) 111 | this.sanitizationText = StringEscapeUtils.unescapeJava(sanitizationText); 112 | } 113 | 114 | public boolean isSanitizationTextCharsetAutoDetect() { 115 | return new SanitizeCommand().getSanitizationTextCharset().equals(getSanitizationTextCharset()); 116 | } 117 | 118 | public String getSanitizationTextCharset() { 119 | return sanitizationTextCharset; 120 | } 121 | 122 | public void setSanitizationTextCharset(final String sanitizationTextCharset) { 123 | this.sanitizationTextCharset = sanitizationTextCharset; 124 | } 125 | 126 | public boolean isForceMatchStringCoder() { 127 | return forceMatchStringCoder; 128 | } 129 | 130 | public void setForceMatchStringCoder(final boolean forceMatchStringCoder) { 131 | this.forceMatchStringCoder = forceMatchStringCoder; 132 | } 133 | 134 | public List getExcludeStringFields() { 135 | final List list = excludeStringFields == null ? Collections.emptyList() : excludeStringFields; 136 | return list.stream() 137 | .map(StringUtils::trimToNull) 138 | .filter(Objects::nonNull) 139 | .filter(field -> field.contains("#")) 140 | .map(field -> field.split(",")) 141 | .flatMap(Arrays::stream) 142 | .collect(Collectors.toList()); 143 | } 144 | 145 | public void setExcludeStringFields(final List list) { 146 | this.excludeStringFields = list; 147 | } 148 | 149 | private StringFieldMap getExcludeStringFieldMap() { 150 | if (excludeStringFieldMap != null) { 151 | return excludeStringFieldMap; 152 | } 153 | excludeStringFieldMap = new StringFieldMap(); 154 | getExcludeStringFields().forEach(excludeStringFieldMap::add); 155 | return excludeStringFieldMap; 156 | } 157 | 158 | public boolean isExactClassWithExcludeStringField(final String className) { 159 | return getExcludeStringFieldMap().map.containsKey(className); 160 | } 161 | 162 | public List getExcludeStringFields(final String className) { 163 | return getExcludeStringFieldMap().map.getOrDefault(className, Collections.emptyList()); 164 | } 165 | 166 | @Override 167 | public String toString() { 168 | return reflectionToString(this, MULTI_LINE_STYLE); 169 | } 170 | 171 | private static class StringFieldMap { 172 | private final Map> map = new HashMap<>(); 173 | 174 | public void add(final String field) { 175 | final String className = StringUtils.substringBefore(field, "#"); 176 | map.computeIfAbsent(className, key -> new ArrayList<>()); 177 | map.get(className).add(StringUtils.substringAfter(field, "#")); 178 | } 179 | 180 | @Override 181 | public String toString() { 182 | return reflectionToString(this, MULTI_LINE_STYLE); 183 | } 184 | 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/capture/CaptureCommandProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.capture; 2 | 3 | import com.paypal.heapdumptool.fixture.ResourceTool; 4 | import com.paypal.heapdumptool.sanitizer.SanitizeCommandProcessor; 5 | import com.paypal.heapdumptool.utils.ProcessTool; 6 | import com.paypal.heapdumptool.utils.ProcessTool.ProcessResult; 7 | import org.apache.commons.io.FileUtils; 8 | import org.apache.commons.io.input.NullInputStream; 9 | import org.apache.commons.io.output.NullOutputStream; 10 | import org.apache.commons.lang3.ArrayUtils; 11 | import org.junit.jupiter.api.AfterEach; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.mockito.MockedStatic; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.nio.file.Paths; 21 | import java.time.Instant; 22 | import java.util.stream.Stream; 23 | 24 | import static java.time.temporal.ChronoUnit.SECONDS; 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 27 | import static org.mockito.Answers.CALLS_REAL_METHODS; 28 | import static org.mockito.ArgumentMatchers.any; 29 | import static org.mockito.Mockito.doNothing; 30 | import static org.mockito.Mockito.doReturn; 31 | import static org.mockito.Mockito.mock; 32 | import static org.mockito.Mockito.mockStatic; 33 | import static org.mockito.Mockito.withSettings; 34 | 35 | public class CaptureCommandProcessorTest { 36 | 37 | private final Instant now = Instant.parse("2020-09-18T23:33:17.764866Z"); 38 | private final Path outputFile = Paths.get("my-app-2020-09-18T23-33-17.764866Z.hprof.zip"); 39 | 40 | private final MockedStatic processToolMock = mockStatic(ProcessTool.class); 41 | private final MockedStatic instantMock = mockStatic(Instant.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); 42 | private final MockedStatic sanitizerMock = mockStatic(SanitizeCommandProcessor.class); 43 | private final MockedStatic privilegeEscalatorMock = mockStatic(PrivilegeEscalator.class); 44 | 45 | @BeforeEach 46 | @AfterEach 47 | public void cleanUpTempFile() throws IOException { 48 | Files.deleteIfExists(outputFile); 49 | } 50 | 51 | @AfterEach 52 | public void afterEach() { 53 | Stream.of(processToolMock, instantMock, sanitizerMock, privilegeEscalatorMock) 54 | .forEach(MockedStatic::close); 55 | } 56 | 57 | @Test 58 | public void testProcessInContainer() throws Exception { 59 | freezeTime(); 60 | expectIsInDockerContainer(true); 61 | expectedProcessInvocations(true); 62 | expectSanitize(); 63 | 64 | final CaptureCommand command = new CaptureCommand(); 65 | command.setContainerName("my-app"); 66 | 67 | final CaptureCommandProcessor processor = new CaptureCommandProcessor(command); 68 | processor.process(); 69 | 70 | assertThat(outputFile).exists(); 71 | processToolMock.verify(() -> ProcessTool.run("nsenter1", "docker", "ps", "--filter", "name=my-app")); 72 | } 73 | 74 | @Test 75 | public void testProcessOnHost() throws Exception { 76 | expectIsInDockerContainer(false); 77 | 78 | processToolMock.when(() -> ProcessTool.run("docker", "ps", "--filter", "name=my-app")) 79 | .thenReturn(resultWith("docker-ps-none.txt")); 80 | 81 | final CaptureCommand command = new CaptureCommand(); 82 | command.setContainerName("my-app"); 83 | 84 | final CaptureCommandProcessor processor = new CaptureCommandProcessor(command); 85 | assertThatIllegalArgumentException() 86 | .isThrownBy(processor::process) 87 | .withMessageContaining("Cannot find container"); 88 | 89 | processToolMock.verify(() -> ProcessTool.run("docker", "ps", "--filter", "name=my-app")); 90 | } 91 | 92 | private void expectIsInDockerContainer(final boolean value) { 93 | privilegeEscalatorMock.when(PrivilegeEscalator::isInDockerContainer) 94 | .thenReturn(value); 95 | } 96 | 97 | private void expectSanitize() throws Exception { 98 | final SanitizeCommandProcessor processor = mock(SanitizeCommandProcessor.class); 99 | 100 | doNothing().when(processor).process(); 101 | 102 | sanitizerMock.when(() -> SanitizeCommandProcessor.newInstance(any(), any())) 103 | .thenAnswer(invocation -> { 104 | final CaptureStreamFactory streamFactory = invocation.getArgument(1, CaptureStreamFactory.class); 105 | streamFactory.newOutputStream(); // create now 106 | return processor; 107 | }); 108 | } 109 | 110 | private void freezeTime() { 111 | instantMock.when(Instant::now) 112 | .thenReturn(now); 113 | 114 | instantMock.when(() -> now.truncatedTo(SECONDS)) 115 | .thenReturn(now); 116 | } 117 | 118 | private void expectedProcessInvocations(final boolean inContainer) throws IOException { 119 | final CmdFunction cmd = args -> inContainer ? prefixWithNsenter1(args) : args; 120 | 121 | processToolMock.when(() -> ProcessTool.run(cmd.maybeWithNsenter1("docker", "ps", "--filter", "name=my-app"))) 122 | .thenReturn(resultWith("docker-ps.txt")); 123 | 124 | processToolMock.when(() -> ProcessTool.run(cmd.maybeWithNsenter1("docker", "exec", "my-app", "jps"))) 125 | .thenReturn(resultWith("docker-exec-jps.txt")); 126 | 127 | processToolMock.when(() -> ProcessTool.run(cmd.maybeWithNsenter1("docker", "exec", "my-app", "jcmd", "55", "Thread.print", "-l"))) 128 | .thenReturn(resultWith("docker-exec-jcmd-gc-heap-dump.txt")); 129 | 130 | final String tmpFile = "/tmp/my-app-2020-09-18T23-33-17.764866Z.hprof"; 131 | processToolMock.when(() -> ProcessTool.run(cmd.maybeWithNsenter1("docker", "exec", "my-app", "jcmd", "55", "GC.heap_dump", tmpFile))) 132 | .thenReturn(resultWith("docker-exec-jcmd-gc-heap-dump.txt")); 133 | 134 | final Path heapDumpFileOnHost = FileUtils.getTempDirectory() 135 | .toPath() 136 | .resolve(Paths.get(tmpFile).getFileName().toString()); 137 | 138 | final ProcessBuilder processBuilder = dockerCpProcess(); 139 | processToolMock.when(() -> ProcessTool.processBuilder(cmd.maybeWithNsenter1("docker", "cp", "my-app:" + tmpFile, heapDumpFileOnHost.toString()))) 140 | .thenReturn(processBuilder); 141 | } 142 | 143 | @FunctionalInterface 144 | private interface CmdFunction { 145 | String[] maybeWithNsenter1(String... args); 146 | } 147 | 148 | private String[] prefixWithNsenter1(final String[] args) { 149 | return ArrayUtils.addFirst(args, "nsenter1"); 150 | } 151 | 152 | private ProcessBuilder dockerCpProcess() throws IOException { 153 | final ProcessBuilder processBuilder = mock(ProcessBuilder.class); 154 | final Process process = mock(Process.class); 155 | 156 | final InputStream nullInputStream = new NullInputStream(); 157 | 158 | doReturn(process).when(processBuilder).start(); 159 | doReturn(nullInputStream).when(process).getInputStream(); 160 | doReturn(nullInputStream).when(process).getErrorStream(); 161 | doReturn(NullOutputStream.INSTANCE).when(process).getOutputStream(); 162 | 163 | return processBuilder; 164 | } 165 | 166 | private ProcessResult resultWith(final String stdoutResource) throws IOException { 167 | return new ProcessResult(0, resourceContent(stdoutResource), ""); 168 | } 169 | 170 | private String resourceContent(final String name) throws IOException { 171 | return ResourceTool.contentOf(getClass(), name); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/sanitizer/DataSize.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.paypal.heapdumptool.sanitizer; 18 | 19 | import org.apache.commons.lang3.StringUtils; 20 | import org.apache.commons.lang3.Validate; 21 | 22 | import java.util.regex.Matcher; 23 | import java.util.regex.Pattern; 24 | 25 | /** 26 | * A data size, such as '12MB'. 27 | * 28 | *

This class models data size in terms of bytes and is immutable and thread-safe. 29 | * 30 | *

The terms and units used in this class are based on 31 | * binary prefixes 32 | * indicating multiplication by powers of 2. Consult the following table and 33 | * the Javadoc for {@link DataUnit} for details. 34 | *

35 | * @author Stephane Nicoll 36 | * @author Sam Brannen 37 | * @since 5.1 38 | * @see DataUnit 39 | */ 40 | public final class DataSize implements Comparable { 41 | 42 | /** 43 | * The pattern for parsing. 44 | */ 45 | private static final Pattern PATTERN = Pattern.compile("^([+\\-]?\\d+)([a-zA-Z]{0,2})$"); 46 | 47 | /** 48 | * Bytes per Kilobyte. 49 | */ 50 | private static final long BYTES_PER_KB = 1024; 51 | 52 | /** 53 | * Bytes per Megabyte. 54 | */ 55 | private static final long BYTES_PER_MB = BYTES_PER_KB * 1024; 56 | 57 | /** 58 | * Bytes per Gigabyte. 59 | */ 60 | private static final long BYTES_PER_GB = BYTES_PER_MB * 1024; 61 | 62 | /** 63 | * Bytes per Terabyte. 64 | */ 65 | private static final long BYTES_PER_TB = BYTES_PER_GB * 1024; 66 | 67 | private final long bytes; 68 | 69 | private DataSize(final long bytes) { 70 | this.bytes = bytes; 71 | } 72 | 73 | /** 74 | * Obtain a {@link DataSize} representing the specified number of bytes. 75 | * @param bytes the number of bytes, positive or negative 76 | * @return a {@link DataSize} 77 | */ 78 | public static DataSize ofBytes(final long bytes) { 79 | return new DataSize(bytes); 80 | } 81 | 82 | /** 83 | * Obtain a {@link DataSize} representing the specified number of kilobytes. 84 | * @param kilobytes the number of kilobytes, positive or negative 85 | * @return a {@link DataSize} 86 | */ 87 | public static DataSize ofKilobytes(final long kilobytes) { 88 | return new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB)); 89 | } 90 | 91 | /** 92 | * Obtain a {@link DataSize} representing the specified number of megabytes. 93 | * @param megabytes the number of megabytes, positive or negative 94 | * @return a {@link DataSize} 95 | */ 96 | public static DataSize ofMegabytes(final long megabytes) { 97 | return new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB)); 98 | } 99 | 100 | /** 101 | * Obtain a {@link DataSize} representing the specified number of gigabytes. 102 | * @param gigabytes the number of gigabytes, positive or negative 103 | * @return a {@link DataSize} 104 | */ 105 | public static DataSize ofGigabytes(final long gigabytes) { 106 | return new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB)); 107 | } 108 | 109 | /** 110 | * Obtain a {@link DataSize} representing the specified number of terabytes. 111 | * @param terabytes the number of terabytes, positive or negative 112 | * @return a {@link DataSize} 113 | */ 114 | public static DataSize ofTerabytes(final long terabytes) { 115 | return new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB)); 116 | } 117 | 118 | /** 119 | * Obtain a {@link DataSize} representing an amount in the specified {@link DataUnit}. 120 | * @param amount the amount of the size, measured in terms of the unit, 121 | * positive or negative 122 | * @return a corresponding {@link DataSize} 123 | */ 124 | public static DataSize of(final long amount, final DataUnit unit) { 125 | Validate.notNull(unit, "Unit must not be null"); 126 | return new DataSize(Math.multiplyExact(amount, unit.size().toBytes())); 127 | } 128 | 129 | /** 130 | * Obtain a {@link DataSize} from a text string such as {@code 12MB} using 131 | * {@link DataUnit#BYTES} if no unit is specified. 132 | *

133 | * Examples: 134 | *

135 |      * "12KB" -- parses as "12 kilobytes"
136 |      * "5MB"  -- parses as "5 megabytes"
137 |      * "20"   -- parses as "20 bytes"
138 |      * 
139 | * @param text the text to parse 140 | * @return the parsed {@link DataSize} 141 | * @see #parse(CharSequence, DataUnit) 142 | */ 143 | public static DataSize parse(final CharSequence text) { 144 | return parse(text, null); 145 | } 146 | 147 | /** 148 | * Obtain a {@link DataSize} from a text string such as {@code 12MB} using 149 | * the specified default {@link DataUnit} if no unit is specified. 150 | *

151 | * The string starts with a number followed optionally by a unit matching one of the 152 | * supported {@linkplain DataUnit suffixes}. 153 | *

154 | * Examples: 155 | *

156 |      * "12KB" -- parses as "12 kilobytes"
157 |      * "5MB"  -- parses as "5 megabytes"
158 |      * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
159 |      * 
160 | * @param text the text to parse 161 | * @return the parsed {@link DataSize} 162 | */ 163 | public static DataSize parse(final CharSequence text, final DataUnit defaultUnit) { 164 | Validate.notNull(text, "Text must not be null"); 165 | try { 166 | final Matcher matcher = PATTERN.matcher(text); 167 | Validate.isTrue(matcher.matches(), "Does not match data size pattern"); 168 | final DataUnit unit = determineDataUnit(matcher.group(2), defaultUnit); 169 | final long amount = Long.parseLong(matcher.group(1)); 170 | return DataSize.of(amount, unit); 171 | } catch (final Exception ex) { 172 | throw new IllegalArgumentException("'" + text + "' is not a valid data size", ex); 173 | } 174 | } 175 | 176 | private static DataUnit determineDataUnit(final String suffix, final DataUnit defaultUnit) { 177 | final DataUnit defaultUnitToUse = (defaultUnit != null ? defaultUnit : DataUnit.BYTES); 178 | return (StringUtils.isNotEmpty(suffix) ? DataUnit.fromSuffix(suffix) : defaultUnitToUse); 179 | } 180 | 181 | /** 182 | * Checks if this size is negative, excluding zero. 183 | * @return true if this size has a size less than zero bytes 184 | */ 185 | public boolean isNegative() { 186 | return this.bytes < 0; 187 | } 188 | 189 | /** 190 | * Return the number of bytes in this instance. 191 | * @return the number of bytes 192 | */ 193 | public long toBytes() { 194 | return this.bytes; 195 | } 196 | 197 | /** 198 | * Return the number of kilobytes in this instance. 199 | * @return the number of kilobytes 200 | */ 201 | public long toKilobytes() { 202 | return this.bytes / BYTES_PER_KB; 203 | } 204 | 205 | /** 206 | * Return the number of megabytes in this instance. 207 | * @return the number of megabytes 208 | */ 209 | public long toMegabytes() { 210 | return this.bytes / BYTES_PER_MB; 211 | } 212 | 213 | /** 214 | * Return the number of gigabytes in this instance. 215 | * @return the number of gigabytes 216 | */ 217 | public long toGigabytes() { 218 | return this.bytes / BYTES_PER_GB; 219 | } 220 | 221 | /** 222 | * Return the number of terabytes in this instance. 223 | * @return the number of terabytes 224 | */ 225 | public long toTerabytes() { 226 | return this.bytes / BYTES_PER_TB; 227 | } 228 | 229 | @Override 230 | public int compareTo(final DataSize other) { 231 | return Long.compare(this.bytes, other.bytes); 232 | } 233 | 234 | @Override 235 | public String toString() { 236 | return String.format("%dB", this.bytes); 237 | } 238 | 239 | @Override 240 | public boolean equals(final Object other) { 241 | if (this == other) { 242 | return true; 243 | } 244 | if (other == null || getClass() != other.getClass()) { 245 | return false; 246 | } 247 | final DataSize otherSize = (DataSize) other; 248 | return (this.bytes == otherSize.bytes); 249 | } 250 | 251 | @Override 252 | public int hashCode() { 253 | return Long.hashCode(this.bytes); 254 | } 255 | 256 | } 257 | -------------------------------------------------------------------------------- /src/test/resources/files/SanitizeStreamFactoryTest/sample.tar: -------------------------------------------------------------------------------- 1 | hello.world0000664000175000017500000000001413622452564010512 0ustar mmhello-world 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heap Dump Tool 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.paypal/heap-dump-tool/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.paypal/heap-dump-tool) 4 | 5 | Heap Dump Tool can capture and, more importantly, sanitize sensitive data from Java heap dumps. Sanitization is accomplished 6 | by replacing field values in the heap dump file with zero values. Heap dump can then be more freely shared freely and analyzed. 7 | 8 | A typical scenario is when a heap dump needs to be sanitized before it can be given to another person or moved to a different 9 | environment. For example, an app running in production environment may contain sensitive data (passwords, credit card 10 | numbers, etc) which should not be viewable when the heap dump is copied to a development environment for analysis with a 11 | graphical program. 12 | 13 | **Conceptual illustration of sanitization**: 14 | 15 | 16 | 17 | --- 18 | 19 | 20 | 21 |   22 |   23 | 24 | **Concrete illustration of before-and-after sanitization**: 25 | 26 | 27 | 28 | --- 29 | 30 | 31 | 32 | 33 | ## TOC 34 | * [Examples](#examples) 35 | * [CLI Usage](#cli-usage) 36 | * [Library Usage](#library-usage) 37 | * [License](#license) 38 | 39 | ## Examples 40 | 41 | The tool can be run in several ways depending on tool's packaging and where the target to-be-captured app is running. 42 | 43 | #### [Jar] Capture sanitized heap dump manually 44 | 45 | Simplest way to capture sanitized heap dump of an app is to run: 46 | 47 | ``` 48 | # capture plain heap dump of Java process with given pid 49 | $ jcmd {pid} GC.heap_dump /path/to/plain-heap-dump.hprof 50 | 51 | # then sanitize the heap dump 52 | $ wget -O heap-dump-tool.jar https://repo1.maven.org/maven2/com/paypal/heap-dump-tool/1.3.4/heap-dump-tool-1.3.4-all.jar 53 | $ java -jar heap-dump-tool.jar sanitize /path/to/plain-dump.hprof /path/to/sanitized-dump.hprof 54 | ``` 55 | 56 |
57 | 58 | #### [Jar] Capture sanitized heap dump of a containerized app 59 | 60 | Suppose the tool is a packaged jar on the host, and the target app is running as the only Java process within a container. 61 | 62 | Then, to capture sanitized heap dump of a containerized app, run: 63 | 64 | ``` 65 | # list docker containers 66 | $ docker ps 67 | CONTAINER ID IMAGE [...] NAMES 68 | 06e633da3494 registry.example.com/my-app:latest [...] my-app 69 | 70 | # capture and sanitize 71 | $ wget -O heap-dump-tool.jar https://repo1.maven.org/maven2/com/paypal/heap-dump-tool/1.3.4/heap-dump-tool-1.3.4-all.jar 72 | $ java -jar heap-dump-tool.jar capture my-app 73 | ``` 74 | 75 | Note that a plain stack dump is also captured. 76 | 77 |
78 | 79 | #### [Docker] Capture sanitized heap dump of a containerized app 80 | 81 | Suppose the tool is a Docker image, and the target app is running as the only Java process within a container. 82 | 83 | Then, to capture sanitized heap dump of another containerized app, run: 84 | 85 | ``` 86 | # list docker containers 87 | $ docker ps 88 | CONTAINER ID IMAGE [...] NAMES 89 | 06e633da3494 registry.example.com/my-app:latest [...] my-app 90 | 91 | # capture and sanitize 92 | $ docker run heapdumptool/heapdumptool capture my-app | bash 93 | ``` 94 | 95 | If the container runs multiple Java processes, pid can be specified: 96 | ``` 97 | # list docker containers 98 | $ docker ps 99 | CONTAINER ID IMAGE [...] NAMES 100 | 06e633da3494 registry.example.com/my-app:latest [...] my-app 101 | 102 | # find pid 103 | $ jps 104 | $ ps aux 105 | 106 | # capture and sanitize 107 | $ docker run heapdumptool/heapdumptool capture my-app -p {pid} | bash 108 | ``` 109 | 110 |
111 | 112 | #### Sanitize hs_err* Java fatal error logs 113 | 114 | To sanitize environment variables in hs_err* files, you can run: 115 | 116 | ``` 117 | # with java -jar 118 | $ wget -O heap-dump-tool.jar https://repo1.maven.org/maven2/com/paypal/heap-dump-tool/1.3.4/heap-dump-tool-1.3.4-all.jar 119 | $ java -jar heap-dump-tool.jar sanitize-hserr input-hs_err.log outout-hs_err.log 120 | 121 | # Or, with docker 122 | $ docker run heapdumptool/heapdumptool sanitize-hserr input-hs_err.log outout-hs_err.log | bash 123 | ``` 124 | 125 | 126 | 127 | ## CLI Usage 128 | 129 | ``` 130 | java -jar heap-dump-tool.jar help 131 | Usage: heap-dump-tool [-hV] [COMMAND] 132 | Tool for capturing or sanitizing heap dumps 133 | -h, --help Show this help message and exit. 134 | -V, --version Print version information and exit. 135 | Commands: 136 | capture Capture sanitized heap dump of a containerized app 137 | sanitize Sanitize a heap dump by replacing byte and char array contents 138 | sanitize-hserr Sanitize fatal error log by censoring environment variable values 139 | help Displays help information about the specified command 140 | ``` 141 | 142 | Additional usage for sub-commands can be found by running `help {sub-command}`. For example: 143 | 144 | ``` 145 | $ java -jar heap-dump-tool.jar help capture 146 | Usage: heap-dump-tool sanitize [OPTIONS] 147 | Sanitize a heap dump by replacing byte and char array contents 148 | Input heap dump .hprof. File or stdin 149 | Output heap dump .hprof. File, stdout, or stderr 150 | -a, --tar-input Treat input as tar archive 151 | -b, --buffer-size= 152 | Buffer size for reading and writing 153 | Default: 100MB 154 | -d, --docker-registry= 155 | docker registry hostname for bootstrapping heap-dump-tool docker image 156 | -e, --exclude-string-fields= 157 | String fields to exclude from sanitization. Value in com.example.MyClass#fieldName format 158 | Default: java.lang.Thread#name,java.lang.ThreadGroup#name 159 | -f, --force-string-coder-match= 160 | Force strings coder values to match sanitizationText.coder value 161 | Default: true 162 | -s, --sanitize-byte-char-arrays-only= 163 | Sanitize byte/char arrays only 164 | Default: true 165 | -t, --text= 166 | Sanitization text to replace with 167 | Default: \0 168 | -z, --zip-output Write zipped output 169 | Default: false 170 | ``` 171 | 172 | ### Explanation of options 173 | 174 | * `-a, --tar-input Treat input as tar archive` 175 | * Meant for use with `-` or `stdin` as inputFile when piping heap dump from k8s `kubectl cp` command which produces tar 176 | archive. 177 | 178 | * `-b, --buffer-size=` 179 | * Higher buffer size should improve performance when reading and writing large heap dump files at the cost of higher 180 | memory usage. 181 | 182 | * `-d, --docker-registry=` 183 | * Meant for use with private docker-registry setups. 184 | 185 | * `-e, --exclude-string-fields=` 186 | * CSV list of string fields to exclude from sanitization. 187 | 188 | * `-f, --force-string-coder-match=` 189 | * In Java 9+, string instances may be encoded differently based on content. This setting forces encoding of sanitized 190 | strings in heap dump to match the encoding of the sanitization text provided via `-t` flag. If unset, some sanitized 191 | string fields may not be displayed correctly in analysis tools due to coder mismatch. 192 | 193 | * `-s, --sanitize-byte-char-arrays-only=` 194 | * When set to true, only byte and char arrays are sanitized. When false, all primitive array fields and all primitive 195 | non-array fields are sanitized. 196 | 197 | * `-t, --text=` 198 | * Sanitization text to replace with. Default is null character `\0`. 199 | 200 | * `-z, --zip-output Write zipped output` 201 | * When set, output heap dump is compressed in .hprof.zip format. 202 | 203 | ### CLI FAQ 204 | 205 | **Q: How can I sanitize non-array primitive fields?** 206 | Set `--sanitize-byte-char-arrays-only=false`. 207 | 208 | 209 | 210 | 211 | ## Library Usage 212 | 213 | To use the tool as a library and embed it within another app, you can declare it as dependency in your project. For maven: 214 | 215 | ```xml 216 | 217 | com.paypal 218 | heap-dump-tool 219 | 1.3.4 220 | 221 | ``` 222 | 223 | Then, use `HeapDumpSanitizer` class to sanitize heap dumps programmatically. Example: 224 | 225 | ```java 226 | public class Demo { 227 | public static void main(String[] args) throws Exception { 228 | SanitizeCommand command = new SanitizeCommand(); 229 | command.setInputFile(Path.of("/path/to/input.hprof")); 230 | command.setOutputFile(Path.of("/path/to/output.hprof")); 231 | 232 | SanitizeStreamFactory streamFactory = new SanitizeStreamFactory(command); 233 | 234 | try (InputStream inputStream = streamFactory.newInputStream(); 235 | OutputStream outputStream = streamFactory.newOutputStream()) { 236 | HeapDumpSanitizer sanitizer = new HeapDumpSanitizer(); 237 | sanitizer.setSanitizeCommand(command); 238 | sanitizer.setInputStream(inputStream); 239 | sanitizer.setOutputStream(outputStream); 240 | sanitizer.setProgressMonitor(bytes -> {}); 241 | sanitizer.sanitize(); 242 | } 243 | } 244 | } 245 | ``` 246 | 247 | ### Library FAQ 248 | 249 | **Q: I see `java.lang.NoClassDefFoundError: org/apache/commons/lang3/Strings`** error 250 | 251 | A: Another dependency in your project is likely pulling in an older version of commons-lang3 library. Try explicitly 252 | adding a newer version of commons-lang3 as dependency: 253 | ```xml 254 | 255 | org.apache.commons 256 | commons-lang3 257 | 3.18.0 258 | 259 | ``` 260 | 261 | 262 | 263 | ## Whitepaper 264 | 265 | See [whitepaper (pdf)](https://github.com/paypal/heap-dump-tool/blob/statics/whitepaper.pdf) 266 | 267 | ## License 268 | 269 | Heap Dump Tool is Open Source software released under the Apache 2.0 license. 270 | 271 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 PayPal 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | https://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/paypal/heapdumptool/capture/CaptureCommandProcessor.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.capture; 2 | 3 | import com.paypal.heapdumptool.cli.CliCommandProcessor; 4 | import com.paypal.heapdumptool.sanitizer.SanitizeCommand; 5 | import com.paypal.heapdumptool.sanitizer.SanitizeCommandProcessor; 6 | import com.paypal.heapdumptool.utils.InternalLogger; 7 | import com.paypal.heapdumptool.utils.ProcessTool; 8 | import com.paypal.heapdumptool.utils.ProcessTool.ProcessResult; 9 | import org.apache.commons.io.FileUtils; 10 | import org.apache.commons.io.IOUtils; 11 | import org.apache.commons.lang3.ArrayUtils; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.apache.commons.lang3.Validate; 14 | import org.apache.commons.lang3.function.Failable; 15 | import org.apache.commons.text.StringSubstitutor; 16 | 17 | import java.io.Closeable; 18 | import java.io.IOException; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.nio.file.Paths; 22 | import java.nio.file.attribute.PosixFilePermission; 23 | import java.time.Instant; 24 | import java.util.ArrayList; 25 | import java.util.Arrays; 26 | import java.util.HashMap; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Set; 30 | import java.util.stream.Collectors; 31 | import java.util.stream.Stream; 32 | import java.util.zip.ZipEntry; 33 | import java.util.zip.ZipOutputStream; 34 | 35 | import static com.paypal.heapdumptool.utils.DateTimeTool.getFriendlyDuration; 36 | import static com.paypal.heapdumptool.utils.ProcessTool.processBuilder; 37 | import static java.nio.charset.StandardCharsets.UTF_8; 38 | import static java.time.Instant.now; 39 | import static org.apache.commons.lang3.StringUtils.substringAfterLast; 40 | import static org.apache.commons.lang3.StringUtils.substringBefore; 41 | 42 | public class CaptureCommandProcessor implements CliCommandProcessor { 43 | 44 | private static final String DOCKER = "docker"; 45 | 46 | private static final InternalLogger LOGGER = InternalLogger.getLogger(CaptureCommandProcessor.class); 47 | 48 | private final CaptureCommand command; 49 | 50 | private final boolean isInContainer; 51 | 52 | public CaptureCommandProcessor(final CaptureCommand command) { 53 | this.command = command; 54 | this.isInContainer = PrivilegeEscalator.isInDockerContainer(); 55 | } 56 | 57 | @Override 58 | public void process() throws Exception { 59 | final Instant now = now(); 60 | LOGGER.info("Capturing sanitized heap dump. container={}", command.getContainerName()); 61 | 62 | validateContainerRunning(); 63 | 64 | final long pid = findPidInAppContainer(); 65 | 66 | final Path heapDumpFileInAppContainer = createPlainHeapDumpInAppContainer(pid); 67 | final String threadDump = captureThreadDump(pid); 68 | final Path heapDumpFileOnHost = FileUtils.getTempDirectory() 69 | .toPath() 70 | .resolve(heapDumpFileInAppContainer.getFileName().toString()); 71 | final Path output; 72 | try { 73 | copyFileOutOfAppContainer(heapDumpFileInAppContainer, heapDumpFileOnHost); 74 | output = sanitizeHeapDump(heapDumpFileOnHost, threadDump); 75 | } finally { 76 | Files.deleteIfExists(heapDumpFileOnHost); 77 | deletePlainHeapDumpInAppContainer(heapDumpFileOnHost); 78 | } 79 | 80 | LOGGER.info("Captured sanitized heap dump in {}. Output: {}", getFriendlyDuration(now), output); 81 | } 82 | 83 | private String captureThreadDump(final long pid) throws Exception { 84 | // jcmd PID Thread.print 85 | final List cmd = new ArrayList<>(command.getThreadCmd()); 86 | addIfNotEmpty(command.getThreadOptions(), cmd); 87 | final Object[] cmdArray = cmd.stream() 88 | .map(arg -> "PID".equals(arg) ? pid : arg) 89 | .toArray(); 90 | final ProcessResult result = execInAppContainer(cmdArray); 91 | return result.stdout; 92 | } 93 | 94 | private Path sanitizeHeapDump(final Path inputFile, final String threadDump) throws Exception { 95 | final String destFile = inputFile.getFileName().toAbsolutePath() // re-eval filename in current cwd 96 | + ".zip"; 97 | final Path destFilePath = Paths.get(destFile); 98 | 99 | final SanitizeCommand sanitizeCommand = new SanitizeCommand(); 100 | sanitizeCommand.copyFrom(this.command); 101 | sanitizeCommand.setInputFile(inputFile); 102 | sanitizeCommand.setOutputFile(destFilePath); 103 | sanitizeCommand.setZipOutput(true); 104 | 105 | try (final CaptureStreamFactory captureStreamFactory = new CaptureStreamFactory(sanitizeCommand)) { 106 | final SanitizeCommandProcessor processor = SanitizeCommandProcessor.newInstance(sanitizeCommand, captureStreamFactory); 107 | processor.process(); 108 | 109 | writeThreadDump(threadDump, inputFile, captureStreamFactory); 110 | } 111 | 112 | updateFilePermissions(destFilePath); 113 | return destFilePath; 114 | } 115 | 116 | private void writeThreadDump(final String threadDump, final Path filePath, final CaptureStreamFactory captureStreamFactory) throws Exception { 117 | final ZipOutputStream zipStream = (ZipOutputStream) captureStreamFactory.getNativeOutputStream(); 118 | 119 | final String fileName = filePath.getFileName() 120 | .toString() 121 | .replace(".hprof", ".threads.txt"); 122 | Validate.validState(fileName.endsWith(".threads.txt")); 123 | 124 | zipStream.putNextEntry(new ZipEntry(fileName)); 125 | IOUtils.write(threadDump, zipStream, UTF_8); 126 | } 127 | 128 | private void updateFilePermissions(final Path destFilePath) throws Exception { 129 | Files.setPosixFilePermissions(destFilePath, globalReadWritePermissions()); 130 | 131 | final String hostUser = System.getProperty("hdt.HOST_USER", System.getenv("HOST_USER")); 132 | if (hostUser != null) { 133 | invokePrivilegedProcess("chown", hostUser + ":" + hostUser, destFilePath.toString()); 134 | } 135 | } 136 | 137 | private Set globalReadWritePermissions() { 138 | return Stream.of(PosixFilePermission.values()) 139 | .filter(permission -> !permission.name().contains("EXECUTE")) 140 | .collect(Collectors.toSet()); 141 | } 142 | 143 | private void copyFileOutOfAppContainer( 144 | final Path heapDumpFileInContainer, 145 | final Path heapDumpFileOnHost) throws IOException, InterruptedException { 146 | final String src = command.getContainerName() + ":" + heapDumpFileInContainer; 147 | final String[] args = array(DOCKER, "cp", src, heapDumpFileOnHost.toString()); 148 | logProcessArgs(args); 149 | 150 | final String[] cmd = nsenterIfNeeded(args); 151 | final Process process = processBuilder(cmd).start(); 152 | closeQuietly(process.getOutputStream()); 153 | closeQuietly(process.getErrorStream()); 154 | final int exitCode = process.waitFor(); 155 | Validate.isTrue(exitCode == 0, "exitCode=" + exitCode); 156 | } 157 | 158 | private void deletePlainHeapDumpInAppContainer(final Path filePath) throws Exception { 159 | execInAppContainer("rm", filePath); 160 | } 161 | 162 | private long findPidInAppContainer() { 163 | if (command.getPid() != null) { 164 | return command.getPid(); 165 | } 166 | 167 | final ProcessResult result = Failable.call(() -> execInAppContainer("jps")); 168 | final Stream javaProcesses = result.stdoutLines() 169 | .stream() 170 | .map(String::trim) 171 | .filter(StringUtils::isNotEmpty) 172 | .filter(line -> !substringAfterLast(line, " ").equals("Jps")); 173 | 174 | final long[] pids = javaProcesses.map(line -> substringBefore(line, " ")) 175 | .mapToLong(Long::parseLong) 176 | .toArray(); 177 | 178 | Validate.validState(pids.length == 1, "Cannot find unique Java process. Passing in --pid=PID." + 179 | " container=%s found=%s", command.getContainerName(), Arrays.toString(pids)); 180 | return pids[0]; 181 | } 182 | 183 | private Path createPlainHeapDumpInAppContainer(final long pid) throws Exception { 184 | final Path filePath = newHeapDumpFilePath(); 185 | 186 | // jcmd PID GC.heap_dump FILE_PATH 187 | final List cmd = new ArrayList<>(command.getHeapCmd()); 188 | addIfNotEmpty(command.getHeapOptions(), cmd); 189 | final Object[] cmdArray = cmd.stream() 190 | .map(arg -> "PID".equals(arg) ? pid : arg) 191 | .map(arg -> "FILE_PATH".equals(arg) ? filePath : arg) 192 | .toArray(); 193 | final ProcessResult result = execInAppContainer(cmdArray); 194 | Validate.validState(result.stdout.contains("Heap dump file created"), 195 | "Cannot create heap dump. container=%s pid=%s" 196 | + "\nstdout=%s" 197 | + "\nstderr=%s", 198 | command.getContainerName(), 199 | pid, 200 | result.stdout, 201 | result.stderr); 202 | 203 | return filePath; 204 | } 205 | 206 | private static void addIfNotEmpty(final List src, final List dest) { 207 | src.stream() 208 | .filter(StringUtils::isNotEmpty) 209 | .forEach(dest::add); 210 | } 211 | 212 | private Path newHeapDumpFilePath() { 213 | final Map props = new HashMap<>(); 214 | props.put("containerName", command.getContainerName()); 215 | props.put("timestamp", Instant.now().toString().replace(":", "-")); 216 | 217 | final String fileName = StringSubstitutor.replace("${containerName}-${timestamp}.hprof", props); 218 | return Paths.get("/tmp/", fileName); 219 | } 220 | 221 | private void validateContainerRunning() throws Exception { 222 | final ProcessResult result = invokePrivilegedProcess(DOCKER, "ps", "--filter", "name=" + command.getContainerName()); 223 | result.stdoutLines() 224 | .stream() 225 | .skip(1) 226 | .map(String::trim) 227 | .filter(StringUtils::isNotEmpty) 228 | .filter(line -> { 229 | final String actualContainerName = substringAfterLast(line, " "); 230 | return actualContainerName.equals(command.getContainerName()); 231 | }) 232 | .findFirst() 233 | .orElseThrow(() -> new IllegalArgumentException("Cannot find container. name=" + command.getContainerName())); 234 | } 235 | 236 | private ProcessResult execInAppContainer(final Object... args) throws Exception { 237 | final String[] stringArgs = Stream.of(args) 238 | .map(String::valueOf) 239 | .toArray(String[]::new); 240 | 241 | final String[] cmd = concat(array(DOCKER, "exec", command.getContainerName()), 242 | stringArgs); 243 | return invokePrivilegedProcess(cmd); 244 | } 245 | 246 | private String[] nsenterIfNeeded(final String... args) { 247 | if (!isInContainer) { 248 | return args; 249 | } 250 | return concat("nsenter1", args); 251 | } 252 | 253 | private ProcessResult invokePrivilegedProcess(final String... args) throws Exception { 254 | logProcessArgs(args); 255 | final String[] cmd = nsenterIfNeeded(args); 256 | return ProcessTool.run(cmd); 257 | } 258 | 259 | private void logProcessArgs(final String... cmd) { 260 | LOGGER.info("Running: {}", String.join(" ", cmd)); 261 | } 262 | 263 | private static T[] concat(final T element, final T[] array) { 264 | return ArrayUtils.addFirst(array, element); 265 | } 266 | 267 | private static T[] concat(final T[] element, final T[] array) { 268 | return ArrayUtils.addAll(element, array); 269 | } 270 | 271 | private static String[] array(final String... elements) { 272 | return elements; 273 | } 274 | 275 | private static void closeQuietly(final Closeable closeable) { 276 | IOUtils.closeQuietly(closeable); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/test/java/com/paypal/heapdumptool/sanitizer/HeapDumpSanitizerTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.heapdumptool.sanitizer; 2 | 3 | import com.paypal.heapdumptool.fixture.HeapDumper; 4 | import com.paypal.heapdumptool.fixture.ResourceTool; 5 | import com.paypal.heapdumptool.sanitizer.example.ClassWithManyInstanceFields; 6 | import com.paypal.heapdumptool.sanitizer.example.ClassWithManyStaticFields; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.junit.jupiter.api.AfterEach; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.MethodOrderer.Random; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.TestInfo; 14 | import org.junit.jupiter.api.TestMethodOrder; 15 | import org.junit.jupiter.api.io.TempDir; 16 | import org.junit.jupiter.params.ParameterizedTest; 17 | import org.junit.jupiter.params.provider.CsvSource; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.nio.charset.Charset; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.concurrent.ThreadLocalRandom; 29 | import java.util.stream.Collectors; 30 | 31 | import static com.paypal.heapdumptool.ApplicationTestSupport.runApplicationPrivileged; 32 | import static com.paypal.heapdumptool.fixture.ByteArrayTool.countOfSequence; 33 | import static com.paypal.heapdumptool.fixture.ByteArrayTool.lengthen; 34 | import static com.paypal.heapdumptool.fixture.ByteArrayTool.nCopiesLongToBytes; 35 | import static java.nio.ByteOrder.BIG_ENDIAN; 36 | import static java.nio.charset.StandardCharsets.UTF_16BE; 37 | import static java.nio.charset.StandardCharsets.UTF_8; 38 | import static java.util.Arrays.asList; 39 | import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; 40 | import static org.apache.commons.lang3.ArrayUtils.EMPTY_BYTE_ARRAY; 41 | import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; 42 | import static org.apache.commons.lang3.JavaVersion.JAVA_1_8; 43 | import static org.apache.commons.lang3.SystemUtils.isJavaVersionAtMost; 44 | import static org.assertj.core.api.Assertions.assertThat; 45 | import static org.assertj.core.api.Assertions.assertThatCode; 46 | 47 | @TestMethodOrder(Random.class) 48 | class HeapDumpSanitizerTest { 49 | 50 | private static final Logger LOGGER = LoggerFactory.getLogger(HeapDumpSanitizerTest.class); 51 | 52 | @TempDir 53 | static Path tempDir; 54 | 55 | private final SecretArrays secretArrays = new SecretArrays(); 56 | 57 | @BeforeEach 58 | void beforeEach(final TestInfo info) { 59 | LOGGER.info("Test - {}:", info.getDisplayName()); 60 | } 61 | 62 | @BeforeEach 63 | @AfterEach 64 | void clearLoadedHeapDumpInfo() { 65 | System.gc(); 66 | } 67 | 68 | private void unused(final Object... o) { 69 | //no op 70 | } 71 | 72 | @Test 73 | @DisplayName("testSecretsAreInHeapDump. Verify that heap dump normally contains sensitive data") 74 | void testSecretsAreInHeapDump() throws Exception { 75 | // "his-secret-value" with each letter incremented by 1 76 | final String hisSecretValue = "ijt.tfdsfu.wbmvf"; 77 | final String herSecretValue = "ifs.tfdsfu.wbmvf"; 78 | final String itsSecretValue = "jut.tfdsfu.wbmvf"; 79 | 80 | // keep as byte array in mem 81 | byte[] actualHisSecretValue = adjustLettersToByteArray(hisSecretValue); 82 | 83 | // keep as char array in mem 84 | String actualHerSecretValue = new String(actualHisSecretValue, UTF_8).replace("his", "her"); 85 | 86 | // interned 87 | lengthenAndInternItsValue(actualHisSecretValue); 88 | 89 | actualHisSecretValue = lengthen(actualHisSecretValue, DataSize.ofMegabytes(1)); 90 | actualHerSecretValue = lengthen(actualHerSecretValue, DataSize.ofMegabytes(1)); 91 | unused(actualHisSecretValue, actualHerSecretValue); 92 | 93 | final byte[] heapDump = loadHeapDump(); 94 | 95 | final byte[] expectedHisSecretValueBytes = adjustLettersToByteArray(hisSecretValue); 96 | final byte[] expectedHerSecretValueBytes = adjustLettersToByteArray(herSecretValue); 97 | final byte[] expectedItsSecretValueBytes = adjustLettersToByteArray(itsSecretValue); 98 | 99 | assertThat(heapDump) 100 | .overridingErrorMessage("sequences do not match") // normal error message would be long and not helpful at all 101 | .containsSequence(expectedHisSecretValueBytes) 102 | .containsSequence(expectedHerSecretValueBytes) 103 | .containsSequence(expectedItsSecretValueBytes) 104 | .containsSequence(secretArrays.getByteArraySequence()) 105 | .containsSequence(secretArrays.getCharArraySequence()) 106 | .containsSequence(secretArrays.getShortArraySequence()) 107 | .containsSequence(secretArrays.getIntArraySequence()) 108 | .containsSequence(secretArrays.getLongArraySequence()) 109 | .containsSequence(secretArrays.getFloatArraySequence()) 110 | .containsSequence(secretArrays.getDoubleArraySequence()) 111 | .containsSequence(secretArrays.getBooleanArraySequence()); 112 | } 113 | 114 | @ParameterizedTest 115 | @CsvSource(value = {"--force-string-coder-match=true", "--force-string-coder-match=false"}) 116 | @DisplayName("testConfidentialsNotInHeapDump. Verify that sanitized heap dump does not contains sensitive data") 117 | void testConfidentialsNotInHeapDump(final String cliArg) throws Exception { 118 | // "his-classified-value" with each letter incremented by 1 119 | final String hisClassifiedValue = "ijt.dmbttjgjfe.wbmvf"; 120 | final String herClassifiedValue = "ifs.dmbttjgjfe.wbmvf"; 121 | final String itsClassifiedValue = "jut.dmbttjgjfe.wbmvf"; 122 | 123 | byte[] actualHisConfidentialValue = ResourceTool.bytesOf(getClass(), "classifieds.txt"); 124 | String actualHerConfidentialValue = new String(actualHisConfidentialValue, UTF_8).replace("his", "her"); 125 | lengthenAndInternItsValue(actualHisConfidentialValue); 126 | 127 | actualHisConfidentialValue = lengthen(actualHisConfidentialValue, DataSize.ofMegabytes(1)); 128 | actualHerConfidentialValue = lengthen(actualHerConfidentialValue, DataSize.ofMegabytes(1)); 129 | unused(actualHisConfidentialValue, actualHerConfidentialValue); 130 | 131 | final byte[] heapDump = loadSanitizedHeapDump(cliArg); 132 | 133 | final byte[] expectedHisClassifiedValueBytes = adjustLettersToByteArray(hisClassifiedValue); 134 | final byte[] expectedHerClassifiedValueBytes = adjustLettersToByteArray(herClassifiedValue); 135 | final byte[] expectedItsClassifiedValueBytes = adjustLettersToByteArray(itsClassifiedValue); 136 | 137 | verifyDoesNotContainsSequence(heapDump, expectedHisClassifiedValueBytes); 138 | verifyDoesNotContainsSequence(heapDump, expectedHerClassifiedValueBytes); 139 | verifyDoesNotContainsSequence(heapDump, expectedItsClassifiedValueBytes); 140 | 141 | verifyDoesNotContainsSequence(heapDump, secretArrays.getByteArraySequence()); 142 | verifyDoesNotContainsSequence(heapDump, secretArrays.getCharArraySequence()); 143 | 144 | // by default only byte and char arrays are sanitized 145 | assertThat(heapDump) 146 | .overridingErrorMessage("sequences do not match") // normal error message would be long and not helpful at all 147 | .containsSequence(secretArrays.getShortArraySequence()) 148 | .containsSequence(secretArrays.getIntArraySequence()) 149 | .containsSequence(secretArrays.getLongArraySequence()) 150 | .containsSequence(secretArrays.getFloatArraySequence()) 151 | .containsSequence(secretArrays.getDoubleArraySequence()) 152 | .containsSequence(secretArrays.getBooleanArraySequence()); 153 | } 154 | 155 | @Test 156 | @DisplayName("testSanitizeFieldsOfNonArrayPrimitiveType. Verify that fields of non-array primitive type can be sanitized") 157 | void testSanitizeFieldsOfNonArrayPrimitiveType() throws Exception { 158 | final Object instance = new ClassWithManyInstanceFields(); 159 | final Object staticFields = new ClassWithManyStaticFields(); 160 | assertThat(instance).isNotNull(); 161 | assertThat(staticFields).isNotNull(); 162 | 163 | byte[] sanitizedHeapDump = loadSanitizedHeapDump("--sanitize-byte-char-arrays-only=false"); 164 | verifyDoesNotContainsSequence(sanitizedHeapDump, nCopiesLongToBytes(deadcow(), 100)); 165 | assertThat(countOfSequence(sanitizedHeapDump, nCopiesLongToBytes(cafegirl(), 1))) 166 | .isLessThan(1000); 167 | 168 | sanitizedHeapDump = EMPTY_BYTE_ARRAY; 169 | unused(new Object[]{sanitizedHeapDump}); 170 | clearLoadedHeapDumpInfo(); 171 | 172 | { 173 | final byte[] clearHeapDump = loadSanitizedHeapDump("--sanitize-byte-char-arrays-only=true"); 174 | assertThat(clearHeapDump) 175 | .overridingErrorMessage("sequences do not match") // normal error message would be long and not helpful at all 176 | .containsSequence(nCopiesLongToBytes(deadcow(), 500)); 177 | 178 | assertThat(countOfSequence(clearHeapDump, nCopiesLongToBytes(cafegirl(), 1))) 179 | .isGreaterThan(500); 180 | } 181 | } 182 | 183 | @Test 184 | void testSanitizeArraysOnly() throws Exception { 185 | final byte[] heapDump = loadSanitizedHeapDump("--sanitize-byte-char-arrays-only=false"); 186 | verifyDoesNotContainsSequence(heapDump, secretArrays.getByteArraySequence()); 187 | verifyDoesNotContainsSequence(heapDump, secretArrays.getCharArraySequence()); 188 | verifyDoesNotContainsSequence(heapDump, secretArrays.getShortArraySequence()); 189 | verifyDoesNotContainsSequence(heapDump, secretArrays.getIntArraySequence()); 190 | verifyDoesNotContainsSequence(heapDump, secretArrays.getLongArraySequence()); 191 | verifyDoesNotContainsSequence(heapDump, secretArrays.getFloatArraySequence()); 192 | verifyDoesNotContainsSequence(heapDump, secretArrays.getDoubleArraySequence()); 193 | verifyDoesNotContainsSequence(heapDump, secretArrays.getBooleanArraySequence()); 194 | } 195 | 196 | @Test 197 | void testThreadNameExcludedFromSanitization() throws Exception { 198 | // "xN-classified-value" with each letter incremented by 1 199 | final String x2ClassifiedValue = "y3.dmbttjgjfe.wbmvf"; 200 | final String x5ClassifiedValue = "y6.dmbttjgjfe.wbmvf"; 201 | final ThreadGroup threadGroup = new ThreadGroup(adjustLetters(x5ClassifiedValue)); 202 | final Thread thread = new Thread(threadGroup, (Runnable) null); 203 | thread.setDaemon(true); 204 | thread.setName(adjustLetters(x2ClassifiedValue)); 205 | 206 | final Charset charset = isJavaVersionAtMost(JAVA_1_8) ? UTF_16BE : UTF_8; 207 | final byte[] sanitizedHeapDump = loadSanitizedHeapDump(); 208 | assertThat(sanitizedHeapDump) 209 | .withFailMessage("threadGroupName " + threadGroup.getName()) 210 | .containsSequence(butLast(threadGroup.getName()).getBytes(charset)) 211 | .withFailMessage("threadName " + thread.getName()) 212 | .containsSequence(butLast(thread.getName()).getBytes(charset)); 213 | } 214 | 215 | private String butLast(final String input) { 216 | return StringUtils.substring(input, 0, input.length() - 2); 217 | } 218 | 219 | @Test 220 | void testThreadNameIncludedInSanitization() throws Exception { 221 | // "xN-classified-value" with each letter incremented by 1 222 | final String x7ClassifiedValue = "y8.dmbttjgjfe.wbmvf"; 223 | final String x11ClassifiedValue = "y12.dmbttjgjfe.wbmvf"; 224 | final ThreadGroup threadGroup = new ThreadGroup(adjustLetters(x7ClassifiedValue)); 225 | final Thread thread = new Thread(threadGroup, (Runnable) null); 226 | thread.setDaemon(true); 227 | thread.setName(adjustLetters(x11ClassifiedValue)); 228 | 229 | final byte[] sanitizedHeapDump = loadSanitizedHeapDump("--exclude-string-fields=none#none"); 230 | verifyDoesNotContainsSequence(sanitizedHeapDump, threadGroup.getName().getBytes(UTF_8)); 231 | verifyDoesNotContainsSequence(sanitizedHeapDump, thread.getName().getBytes(UTF_8)); 232 | } 233 | 234 | // 0xDEADBEEF 235 | private long deadcow() { 236 | return 0xDEADBEEE + Integer.parseInt("1"); 237 | } 238 | 239 | // 0xCAFEBABE 240 | private long cafegirl() { 241 | return 0xCAFEBABD + Integer.parseInt("1"); 242 | } 243 | 244 | private void verifyDoesNotContainsSequence(final byte[] big, final byte[] small) { 245 | final String corrId = System.currentTimeMillis() + ""; 246 | assertThatCode(() -> { 247 | assertThat(big) 248 | .withFailMessage(corrId).containsSequence(small); 249 | }).withFailMessage("does in fact contains sequence") 250 | .hasMessageContaining(corrId); 251 | } 252 | 253 | private void lengthenAndInternItsValue(final byte[] value) { 254 | String itsValue = new String(value, UTF_8).replace("his", "its"); 255 | itsValue = lengthen(itsValue, DataSize.ofMegabytes(1)); 256 | unused(itsValue.intern()); 257 | } 258 | 259 | private byte[] adjustLettersToByteArray(final String str) { 260 | return adjustLetters(str).getBytes(UTF_8); 261 | } 262 | 263 | private String adjustLetters(final String str) { 264 | return str.chars() 265 | .map(chr -> chr - 1) 266 | .mapToObj(chr -> String.valueOf((char) chr)) 267 | .collect(Collectors.joining("")); 268 | } 269 | 270 | private Path triggerHeapDump() throws Exception { 271 | final Path heapDumpPath = newTempFilePath(); 272 | 273 | LOGGER.info("Heap dumping to {}", heapDumpPath); 274 | HeapDumper.dumpHeap(heapDumpPath); 275 | 276 | return heapDumpPath; 277 | } 278 | 279 | private byte[] loadHeapDump() throws Exception { 280 | return loadHeapDump(triggerHeapDump()); 281 | } 282 | 283 | private byte[] loadHeapDump(final Path heapDumpPath) throws IOException { 284 | final long size = Files.size(heapDumpPath); 285 | LOGGER.info("Loading heap dump. size={} name={}", byteCountToDisplaySize(size), heapDumpPath.getFileName()); 286 | return Files.readAllBytes(heapDumpPath); 287 | } 288 | 289 | private byte[] loadSanitizedHeapDump(final String... options) throws Exception { 290 | final Path heapDump = triggerHeapDump(); 291 | final Path sanitizedHeapDumpPath = newTempFilePath(); 292 | 293 | final List cmd = new ArrayList<>(); 294 | cmd.add("sanitize"); 295 | cmd.addAll(asList(options)); 296 | cmd.add(heapDump.toString()); 297 | cmd.add(sanitizedHeapDumpPath.toString()); 298 | 299 | runApplicationPrivileged(cmd.toArray(EMPTY_STRING_ARRAY)); 300 | return loadHeapDump(sanitizedHeapDumpPath); 301 | } 302 | 303 | private Path newTempFilePath() throws IOException { 304 | final Path path = Files.createTempFile(tempDir, getClass().getSimpleName(), ".hprof"); 305 | Files.delete(path); 306 | return path; 307 | } 308 | 309 | private static class SecretArrays { 310 | private static final int LENGTH = 512; 311 | 312 | private final byte[] byteArray = new byte[LENGTH]; 313 | private final char[] charArray = new char[LENGTH]; 314 | private final short[] shortArray = new short[LENGTH]; 315 | private final int[] intArray = new int[LENGTH]; 316 | private final long[] longArray = new long[LENGTH]; 317 | private final float[] floatArray = new float[LENGTH]; 318 | private final double[] doubleArray = new double[LENGTH]; 319 | private final boolean[] booleanArray = new boolean[LENGTH]; 320 | 321 | { 322 | final ThreadLocalRandom random = ThreadLocalRandom.current(); 323 | for (int i = 0; i < LENGTH; i++) { 324 | byteArray[i] = (byte) random.nextInt(); 325 | charArray[i] = (char) random.nextInt(); 326 | shortArray[i] = (short) random.nextInt(); 327 | intArray[i] = random.nextInt(); 328 | longArray[i] = random.nextLong(); 329 | floatArray[i] = random.nextFloat(); 330 | doubleArray[i] = random.nextDouble(); 331 | booleanArray[i] = random.nextBoolean(); 332 | } 333 | } 334 | 335 | public byte[] getByteArraySequence() { 336 | return byteArray; 337 | } 338 | 339 | public byte[] getCharArraySequence() { 340 | final ByteBuffer buffer = ByteBuffer.allocate(Character.BYTES * LENGTH); 341 | buffer.order(BIG_ENDIAN); 342 | for (int i = 0; i < LENGTH; i++) { 343 | buffer.putChar(i * Character.BYTES, charArray[i]); 344 | } 345 | return buffer.array(); 346 | } 347 | 348 | public byte[] getShortArraySequence() { 349 | final ByteBuffer buffer = ByteBuffer.allocate(Short.BYTES * LENGTH); 350 | buffer.order(BIG_ENDIAN); 351 | for (int i = 0; i < LENGTH; i++) { 352 | buffer.putShort(i * Short.BYTES, shortArray[i]); 353 | } 354 | return buffer.array(); 355 | } 356 | 357 | public byte[] getIntArraySequence() { 358 | final ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES * LENGTH); 359 | buffer.order(BIG_ENDIAN); 360 | for (int i = 0; i < LENGTH; i++) { 361 | buffer.putInt(i * Integer.BYTES, intArray[i]); 362 | } 363 | return buffer.array(); 364 | } 365 | 366 | public byte[] getLongArraySequence() { 367 | final ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * LENGTH); 368 | buffer.order(BIG_ENDIAN); 369 | for (int i = 0; i < LENGTH; i++) { 370 | buffer.putLong(i * Long.BYTES, longArray[i]); 371 | } 372 | return buffer.array(); 373 | } 374 | 375 | public byte[] getFloatArraySequence() { 376 | final ByteBuffer buffer = ByteBuffer.allocate(Float.BYTES * LENGTH); 377 | buffer.order(BIG_ENDIAN); 378 | for (int i = 0; i < LENGTH; i++) { 379 | buffer.putFloat(i * Float.BYTES, floatArray[i]); 380 | } 381 | return buffer.array(); 382 | } 383 | 384 | public byte[] getDoubleArraySequence() { 385 | final ByteBuffer buffer = ByteBuffer.allocate(Double.BYTES * LENGTH); 386 | buffer.order(BIG_ENDIAN); 387 | for (int i = 0; i < LENGTH; i++) { 388 | buffer.putDouble(i * Double.BYTES, doubleArray[i]); 389 | } 390 | return buffer.array(); 391 | } 392 | 393 | public byte[] getBooleanArraySequence() { 394 | final ByteBuffer buffer = ByteBuffer.allocate(LENGTH); 395 | buffer.order(BIG_ENDIAN); 396 | for (int i = 0; i < LENGTH; i++) { 397 | buffer.put(i, booleanArray[i] ? (byte) 1 : (byte) 0); 398 | } 399 | return buffer.array(); 400 | } 401 | } 402 | } 403 | --------------------------------------------------------------------------------