Skip to content

feat: add --redact/--compress/--open for heap-dump, jstall bundle, and JRE-only fallback - #56

Open
parttimenerd wants to merge 71 commits into
SAP:masterfrom
parttimenerd:heap-dump-compress-redact
Open

parttimenerd wants to merge 71 commits into
SAP:masterfrom
parttimenerd:heap-dump-compress-redact

Conversation

@parttimenerd

@parttimenerd parttimenerd commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Summary

New heap-dump flags

  • --redact: zeros primitive arrays (byte[], char[], etc.) before saving (lean redaction mode). Uses the bundled hprof-redact binary. Supported on Linux (amd64/arm64), macOS (Apple Silicon), Windows (amd64/arm64).
  • --redact-complete: zeros all primitive arrays and individual primitive fields (complete redaction, maximum privacy). Mutually exclusive with --redact.
  • --compress: saves the dump as .hprof.gz instead of decompressing after transfer (requires JDK 17+ on container).
  • Transparent compressed transfer: on JDK 17+ containers the plugin always uses jmap gz=1 to reduce SSH transfer size, decompressing on the fly. --compress keeps the file compressed locally.
  • --open: after downloading, spins up a temporary single-serve local HTTP server and opens hprof-analyzer in the browser with the dump pre-loaded. Server shuts down automatically after the file is fetched once.
  • --open-url <URL>: override the hprof-analyzer base URL (e.g. a locally running instance). Implies --open.

Bundled jstall (JVM inspection)

  • Bundles jstall v0.8.1 for one-shot JVM inspection via cf java jstall APP_NAME (or the status/record-status shortcuts). Requires Java 17+ locally.
  • Key subcommands: status (thread analysis, metaspace, GC, compiler queue), record-status (repeated sampling), flame-graph, heap-info. All jstall subcommands available via --args.

JRE-only container support

  • heap-dump, thread-dump, vm-info, vm-version, jcmd, and all jstall-based commands now work on containers without JDK tools (jmap, jstack, jcmd) by falling back to the HotSpot attach socket via nc -U.
  • Requires netcat-openbsd or nmap-ncat on the container. Supports JDK 9–25 on Linux and macOS.

Windows fixes

  • cf install-plugin <URL> now works on Windows: Windows binary released as cf-cli-java-plugin-windows-amd64.exe (with .exe). CF CLI previously rejected the temp file without the extension.
  • cf java status and other jstall-based commands now work on Windows: JVM property jdk.lang.Process.allowAmbiguousCommands=false ensures embedded quotes in remote shell payloads are correctly escaped via ProcessBuilder.

UX improvements

  • SSH errors now include actionable diagnostics: connection reset/refused suggests retrying, "instance does not exist" explains the app is stopped/scaled down, permission errors indicate SSH is disabled for the space.
  • Better error when app name and subcommand are swapped (e.g. cf java my-app heap-dump): suggests the corrected command if the first argument matches a known app.

Maintenance

  • Bumped Go toolchain to 1.26.8 (fixes 6 stdlib CVEs: GO-2026-5856, 5972, 5039, 5037, 6089, 6090).
  • CI: bumped upload-artifact to v7, download-artifact to v8.
  • macOS plugin support now requires Apple Silicon; darwin/amd64 dropped.

Security notes (--open)

  • File served under a random 16-hex-char token URL — real filename never exposed
  • Server bound to 127.0.0.1 only (loopback, not reachable from network)
  • Uses io.Copy directly (not http.ServeFile) to prevent path traversal
  • All other paths return 404; CORS OPTIONS preflight handled without triggering shutdown
  • macOS: Application Firewall will prompt to accept connections — click Allow (loopback only, safe)
  • Firefox: may ask to allow the HTTPS page to access local services — click Allow

- Embeds hprof-redact binaries (linux/amd64, linux/arm64, darwin/arm64,
  windows/amd64) via //go:embed; extracted to ~/.cache/cf-java-plugin/
  on first use (SHA8-keyed, reused on subsequent runs).

- --compress: passes gz=1 to jmap (JDK 17+) to compress the dump on the
  remote container before transfer, saving bandwidth. jvmmon path falls
  back to post-creation gzip on the container. Output is .hprof.gz.

- --redact: pipes the downloaded dump through hprof-redact (lean mode:
  zeros primitive arrays only). Output is -redacted.hprof or
  -redacted.hprof.gz when --compress is also set.

- --redact-complete: like --redact but zeros all primitive values
  (instance scalar fields + CLASS_DUMP statics) via two-pass redaction.

- --compress and --redact can be combined: compress on remote to reduce
  transfer, redact locally (hprof-redact reads .hprof.gz natively).

- Adds FindHeapDumpGzFile to utils for locating *.hprof.gz on the
  container (jvmmon+compress path).

- Extracts osWindows and cmdHeapDump constants to satisfy goconst.
- hprof-analyzer release.yml: add aarch64-pc-windows-msvc target on
  windows-11-arm runner; compile-all includes GOOS=windows GOARCH=arm64

- redact.go: embed dist/hprof-redact-windows-arm64.exe (stub until
  v0.3.1 ships); add windows/arm64 case; guard against 0-byte stubs

- cfutils.go: add CopyOverCatGunzip (io.Pipe + compress/gzip for
  transparent streaming decompression) and ProbeRemoteFileGzip (checks
  gzip magic bytes 1f8b via SSH)

- cf_cli_java_plugin.go: heap-dump SSHCommand now uses shell-level
  gz probe (jmap -h | grep gz) so jmap auto-uses gz=1 on JDK 17+;
  Go post-command probes the remote file magic bytes to decide:
  - remote gz + no --compress → CopyOverCatGunzip, save as .hprof
  - remote gz + --compress → CopyOverCat, save as .hprof.gz
  - not gz + --compress → warn JDK 17+ required, save uncompressed
  Removes @JMAP_GZ Go expansion (replaced by shell probe); keeps
  @COMPRESS_FLAG for jvmmon path
- appInstanceIndexSet: simonleung8/flags IsSet() returns true for any
  registered flag whose non-zero default was set at init time, making
  it impossible to distinguish user-provided from default. Compare
  against known default (-1) instead.
- CheckRequiredTools wrapped in !options.DryRun guard so dry-run works
  without CF login or SSH access.
- var err declaration hoisted before GenerateFiles block (needed after
  CheckRequiredTools was scoped inside the if block).
…-dump

- Feature list in intro
- Examples: --redact, --redact-complete, --compress, combined usage
- New "Heap Dump Privacy" subsection: redaction modes table, what is
  preserved, file naming, supported platforms
- New "Compressed Transfer" subsection: JDK 17+ requirement, fallback
  behaviour, transparent gz transfer without --compress
- CHANGELOG [Unreleased]: all four new behaviours
- Use context.WithTimeout(context.Background(), 5s) for srv.Shutdown
  instead of the request context (which may already be canceled);
  suppress contextcheck lint with explanation since this is intentional
- Add empty title arg to Windows `cmd /c start` to avoid URL being
  interpreted as the window title
- Replace non-blocking done-channel check in test with a 2s timeout
  select to avoid a false-pass race condition
golangci-lint typecheck fails when the dist/ hprof-redact binaries are
missing (go:embed pattern not satisfied). Add a download step mirroring
build.py, placed after the existing jstall download and before lint.
The previous attempt tried to curl the binaries directly, but they are
packed inside tar.gz/zip archives. Add --deps-only flag to build.py to
run only the download+extraction step (no go build), and use it in the
PR validation workflow before golangci-lint.
Comment thread cf_cli_java_plugin.go Outdated
Comment thread open.go Outdated
Comment thread open.go Outdated
cf install-plugin rejects binaries without the .exe extension on Windows,
causing "temp/...exe doesn't exist" errors. build.py now outputs
cf-cli-java-plugin-windows-amd64.exe, README and release body URLs updated
accordingly, and the CI smoke-test asserts the extension before installing.
@parttimenerd

Copy link
Copy Markdown
Member Author

There is now Windows support (tested with amd64 Cygwin and PowerShell) for the jstall-related features. I tested these features manually.

- WorkloadApp: read PORT env var instead of hardcoding 8080
- jdk21/jdk25 manifests: bump memory to 512M and cap
  ReservedCodeCacheSize=64M so the memory calculator fits within limits
- Add run-on-all.sh: run any cf java command across all (or filtered)
  apps in parallel, group output by similarity, --keep-tmp support
Exit code 10 from jstall means "ran successfully but found a warning condition"
(e.g. JVM older than ~4 months). The plugin was surfacing this as an error:
  "jstall execution failed: exit status 10"
Treat it as success — the warning is already printed to stdout by jstall.

Also adds JSTALL_LOCAL=/path/to/jar make variable so developers can test
against a locally built jstall JAR without publishing to GitHub:
  JSTALL_LOCAL=../jstall/target/jstall.jar make install
… JDK 11

Two causes of the crash:
1. Memory: JDK 11 default ReservedCodeCacheSize=240M exhausted 256M container.
   Bump to 512M and cap ReservedCodeCacheSize=64M (same fix as jdk21/jdk25).
2. The sap_java_buildpack_jakarta injects a logging-level-change-agent JAR
   compiled for Java 17+ (class file version 61.0), which fails with
   UnsupportedClassVersionError on Java 11 (max class version 55.0).
   Exclude LoggingLevelChangeAgent from the frameworks list via JBP_CONFIG_COMPONENTS.
…-all.sh arg order

jdk11 and jdk17 were crashing at startup: the CF java-buildpack memory calculator
defaults ReservedCodeCacheSize=240M on JDK 11/17, exceeding the 256M limit.
Bump to 512M and cap to 64M (same fix applied earlier to jdk21/jdk25/sapmachine11).

Also fix run-on-all.sh: the CF CLI syntax is 'cf java SUBCOMMAND APP [options]',
but the script was calling 'cf java APP SUBCOMMAND'. Insert the app name after
CF_ARGS[0] (the subcommand) so e.g. './run-on-all.sh -- asprof-status' works.
…mp jstall to 0.8.0

Add AttachSocketNCHelper shell function (nc_jcmd) that communicates with
the JVM via HotSpot attach socket, enabling diagnostics on JRE-only
containers where jcmd/jstack are absent.

- thread-dump: try jstack → jvmmon → nc_jcmd Thread.print (with clear
  error if all three are absent)
- vm-info: try jcmd VM.info → nc_jcmd VM.info (removed RequiredTools hard-fail)
- vm-version: try jcmd VM.version → nc_jcmd VM.version

The nc_jcmd helper handles attach-socket creation on demand (SIGQUIT
handshake) and resolves macOS $TMPDIR trailing-slash via ${TMPDIR%/}.
Requires netcat-openbsd or nmap-ncat on the container.
heap-dump: fall back to GC.heap_dump via nc_jcmd when jmap/jvmmon absent.
  Verifies file exists after write; clear error if nc also unavailable.

jcmd: remove RequiredTools hard-fail; try jcmd binary first, fall back
  to nc_jcmd for arbitrary jcmd commands on JRE-only containers.
  @Args is split via "set -- @Args" so nc_jcmd receives command + args as
  separate positional parameters.

nc_jcmd: propagate non-zero return codes from attach protocol as errors
  instead of silently returning empty output. Add nc_available() helper
  to avoid repeating the nc -h probe inline.
HotSpot's attach protocol expects the entire command line (command name
+ space-separated args) as a single NUL-terminated field:
  printf '1\0jcmd\0GC.heap_dump /tmp/out.hprof\0\0\0'
It then splits that field on spaces to extract command and arguments.
The previous format sent command and each arg as separate NUL-delimited
fields, which the JVM ignored — causing "filename is mandatory" for
GC.heap_dump and silent no-ops for any multi-arg command.

Simplify nc_jcmd to: nc_jcmd PID CMDLINE... where "$*" after shift
builds the full command line string naturally.
@parttimenerd parttimenerd changed the title feat(heap-dump): add --redact, --compress, and --open flags feat: add --redact/--compress/--open for heap-dump, jstall bundle, and JRE-only fallback Oct 7, 2026
- fix: decompress gzip stream before passing to hprof-redact when remote
  jmap used gz=1 (JDK 17+); previously hprof-redact received raw gzip bytes
  and would corrupt or reject the file (fixes --redact on JDK 17+ containers)
- fix: single-quote app name in jstall --ssh fallback path to handle app
  names with spaces or shell metacharacters
- fix: remove WriteTimeout from one-shot heap dump HTTP server; a 30s
  deadline killed large (>3GB) dump transfers to the browser mid-stream
- warn: print a warning when --redact/--redact-complete/--compress/--open/
  --open-url are passed to non-heap-dump commands (previously silently ignored)
- refactor: replace inline nc -U checks in thread-dump/vm-info/vm-version
  shell snippets with nc_available() for consistency with heap-dump/jcmd
- jvmmon path: write setHeapDumpOnDemandPath.sh into @fspath (not the
  current directory) and remove it after use; previously the script was
  left behind on the container after every jvmmon heap dump
- redact: remove pre-write-access check (create+remove before hprof-redact
  runs); MkdirAll already covers the directory case and any write failure
  will surface naturally from hprof-redact itself without the TOCTOU window
…uffer

The 192 KB file fit entirely in the kernel TCP send buffer, so io.Copy
completed successfully even after the client disconnected — spuriously
triggering server shutdown and failing the test.

Also updates dist/jstall-minimal.jar with the vm-vitals two-section fix
(recent samples + extremes capped at max(10, topN)).
Update dist/jstall-minimal.jar to v0.8.2 (vm-vitals parser rewrite,
Trends + Observations sections, heap-comm/meta-comm duplicate-column fix).
Update CHANGELOG to document the new vm-vitals features.
Add two unit tests covering the app/command swap-detection logic.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants