-JATTACH_VERSION=1.5
+JATTACH_VERSION=2.0
ifneq ($(findstring Windows,$(OS)),)
CL=cl.exe
CFLAGS=/O2 /D_CRT_SECURE_NO_WARNINGS
JATTACH_EXE=jattach.exe
+ JATTACH_DLL=jattach.dll
else
+ CFLAGS ?= -O3
+ JATTACH_EXE=jattach
+
UNAME_S:=$(shell uname -s)
- ifneq ($(findstring FreeBSD,$(UNAME_S)),)
- CC=cc
- CFLAGS=-O2
- JATTACH_EXE=jattach
+ ifeq ($(UNAME_S),Darwin)
+ JATTACH_DLL=libjattach.dylib
else
+ JATTACH_DLL=libjattach.so
+ endif
+
+ ifeq ($(UNAME_S),Linux)
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
RPM_ROOT=$(ROOT_DIR)/build/rpm
SOURCES=$(RPM_ROOT)/SOURCES
SPEC_FILE=jattach.spec
- CC=gcc
- CFLAGS=-O2
- JATTACH_EXE=jattach
endif
endif
+
+.PHONY: all dll clean rpm-dirs rpm
+
all: build build/$(JATTACH_EXE)
+dll: build build/$(JATTACH_DLL)
+
build:
mkdir -p build
-build/jattach: src/jattach_posix.c
- $(CC) $(CFLAGS) -DJATTACH_VERSION=\"$(JATTACH_VERSION)\" -o $@ $^
+build/jattach: src/posix/*.c src/posix/*.h
+ $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -DJATTACH_VERSION=\"$(JATTACH_VERSION)\" -o $@ src/posix/*.c
+
+build/$(JATTACH_DLL): src/posix/*.c src/posix/*.h
+ $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -DJATTACH_VERSION=\"$(JATTACH_VERSION)\" -fPIC -shared -fvisibility=hidden -o $@ src/posix/*.c
-build/jattach.exe: src/jattach_windows.c
+build/jattach.exe: src/windows/jattach.c
$(CL) $(CFLAGS) /DJATTACH_VERSION=\"$(JATTACH_VERSION)\" /Fobuild/jattach.obj /Fe$@ $^ advapi32.lib /link /SUBSYSTEM:CONSOLE,5.02
clean:
- **printflag** : print VM flag
- **jcmd** : execute jcmd command
+### Download
+
+Binaries are available on the [Releases](https://github.com/apangin/jattach/releases) page.
+
+On some platforms, you can also [install](#installation) jattach with a package manager.
+
### Examples
-#### Load JVMTI agent
+#### Load native agent
$ jattach <pid> load <.so-path> { true | false } [ options ]
`options` are passed to the agent.
+#### Load Java agent
+
+Java agents are loaded by the special built-in native agent named `instrument`,
+which takes .jar path and its arguments as a single options string.
+
+ $ jattach <pid> load instrument false "javaagent.jar=arguments"
+
#### List available jcmd commands
$ jattach <pid> jcmd "help -all"
#### Alpine Linux
-On Alpine Linux, you can use the following command to install `jattach` package from the edge/testing repository:
+On Alpine Linux, you can use the following command to install `jattach` package from the edge/community repository:
- $ apk add --no-cache jattach --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/
+ $ apk add --no-cache jattach --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/
#### Archlinux
Name: jattach
-Version: 1.3
+Version: 2.0
Release: 1
Summary: JVM Dynamic Attach utility
/usr/bin/jattach
%changelog
+* Wed Aug 11 2021 Vadim Tsesko <incubos@yandex.com> - 2.0-1
+- Attach to OpenJ9 VMs
+- Pass agent error codes
+- Improved container support
+
+* Wed Jan 09 2018 Vadim Tsesko <incubos@yandex.com> - 1.5-1
+- Improved attach to containerized JVMs
+- chroot support
+
* Wed Nov 30 2016 Vadim Tsesko <incubos@yandex.com> - 0.1-1
- Initial version
+++ /dev/null
-/*
- * Copyright 2016 Andrei Pangin
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <sys/socket.h>
-#include <sys/un.h>
-#include <sys/syscall.h>
-#include <dirent.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <signal.h>
-#include <time.h>
-#include <unistd.h>
-
-#define MAX_PATH 1024
-#define TMP_PATH (MAX_PATH - 64)
-
-static char temp_path_storage[TMP_PATH] = {0};
-
-
-#ifdef __linux__
-
-const char* get_temp_path() {
- return temp_path_storage;
-}
-
-int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid) {
- // A process may have its own root path (when running in chroot environment)
- char path[64];
- snprintf(path, sizeof(path), "/proc/%d/root", pid);
-
- // Append /tmp to the resolved root symlink
- ssize_t path_size = readlink(path, temp_path_storage, sizeof(temp_path_storage) - 10);
- strcpy(temp_path_storage + (path_size > 1 ? path_size : 0), "/tmp");
-
- // Parse /proc/pid/status to find process credentials
- snprintf(path, sizeof(path), "/proc/%d/status", pid);
- FILE* status_file = fopen(path, "r");
- if (status_file == NULL) {
- return 0;
- }
-
- char* line = NULL;
- size_t size;
-
- while (getline(&line, &size, status_file) != -1) {
- if (strncmp(line, "Uid:", 4) == 0) {
- // Get the effective UID, which is the second value in the line
- *uid = (uid_t)atoi(strchr(line + 5, '\t'));
- } else if (strncmp(line, "Gid:", 4) == 0) {
- // Get the effective GID, which is the second value in the line
- *gid = (gid_t)atoi(strchr(line + 5, '\t'));
- } else if (strncmp(line, "NStgid:", 7) == 0) {
- // PID namespaces can be nested; the last one is the innermost one
- *nspid = atoi(strrchr(line, '\t'));
- }
- }
-
- free(line);
- fclose(status_file);
- return 1;
-}
-
-int enter_mount_ns(int pid) {
-#ifdef __NR_setns
- char path[128];
- snprintf(path, sizeof(path), "/proc/%d/ns/mnt", pid);
-
- struct stat oldns_stat, newns_stat;
- if (stat("/proc/self/ns/mnt", &oldns_stat) == 0 && stat(path, &newns_stat) == 0) {
- // Don't try to call setns() if we're in the same namespace already
- if (oldns_stat.st_ino != newns_stat.st_ino) {
- int newns = open(path, O_RDONLY);
- if (newns < 0) {
- return 0;
- }
-
- // Some ancient Linux distributions do not have setns() function
- int result = syscall(__NR_setns, newns, 0);
- close(newns);
- return result < 0 ? 0 : 1;
- }
- }
-#endif // __NR_setns
-
- return 1;
-}
-
-// The first line of /proc/pid/sched looks like
-// java (1234, #threads: 12)
-// where 1234 is the required host PID
-int sched_get_host_pid(const char* path) {
- static char* line = NULL;
- size_t size;
- int result = -1;
-
- FILE* sched_file = fopen(path, "r");
- if (sched_file != NULL) {
- if (getline(&line, &size, sched_file) != -1) {
- char* c = strrchr(line, '(');
- if (c != NULL) {
- result = atoi(c + 1);
- }
- }
- fclose(sched_file);
- }
-
- return result;
-}
-
-// Linux kernels < 4.1 do not export NStgid field in /proc/pid/status.
-// Fortunately, /proc/pid/sched in a container exposes a host PID,
-// so the idea is to scan all container PIDs to find which one matches the host PID.
-int alt_lookup_nspid(int pid) {
- int namespace_differs = 0;
- char path[300];
- snprintf(path, sizeof(path), "/proc/%d/ns/pid", pid);
-
- // Don't bother looking for container PID if we are already in the same PID namespace
- struct stat oldns_stat, newns_stat;
- if (stat("/proc/self/ns/pid", &oldns_stat) == 0 && stat(path, &newns_stat) == 0) {
- if (oldns_stat.st_ino == newns_stat.st_ino) {
- return pid;
- }
- namespace_differs = 1;
- }
-
- // Otherwise browse all PIDs in the namespace of the target process
- // trying to find which one corresponds to the host PID
- snprintf(path, sizeof(path), "/proc/%d/root/proc", pid);
- DIR* dir = opendir(path);
- if (dir != NULL) {
- struct dirent* entry;
- while ((entry = readdir(dir)) != NULL) {
- if (entry->d_name[0] >= '1' && entry->d_name[0] <= '9') {
- // Check if /proc/<container-pid>/sched points back to <host-pid>
- snprintf(path, sizeof(path), "/proc/%d/root/proc/%s/sched", pid, entry->d_name);
- if (sched_get_host_pid(path) == pid) {
- closedir(dir);
- return atoi(entry->d_name);
- }
- }
- }
- closedir(dir);
- }
-
- if (namespace_differs) {
- printf("WARNING: couldn't find container pid of the target process\n");
- }
-
- return pid;
-}
-
-#elif defined(__APPLE__)
-
-#include <sys/sysctl.h>
-
-// macOS has a secure per-user temporary directory
-const char* get_temp_path() {
- if (temp_path_storage[0] == 0) {
- int path_size = confstr(_CS_DARWIN_USER_TEMP_DIR, temp_path_storage, sizeof(temp_path_storage));
- if (path_size == 0 || path_size > sizeof(temp_path_storage)) {
- strcpy(temp_path_storage, "/tmp");
- }
- }
-
- return temp_path_storage;
-}
-
-int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid) {
- int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
- struct kinfo_proc info;
- size_t len = sizeof(info);
-
- if (sysctl(mib, 4, &info, &len, NULL, 0) < 0 || len <= 0) {
- return 0;
- }
-
- *uid = info.kp_eproc.e_ucred.cr_uid;
- *gid = info.kp_eproc.e_ucred.cr_gid;
- *nspid = pid;
- return 1;
-}
-
-// This is a Linux-specific API; nothing to do on macOS and FreeBSD
-int enter_mount_ns(int pid) {
- return 1;
-}
-
-// Not used on macOS and FreeBSD
-int alt_lookup_nspid(int pid) {
- return pid;
-}
-
-#else // __FreeBSD__
-
-#include <sys/sysctl.h>
-#include <sys/user.h>
-
-const char* get_temp_path() {
- return "/tmp";
-}
-
-int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid) {
- int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
- struct kinfo_proc info;
- size_t len = sizeof(info);
-
- if (sysctl(mib, 4, &info, &len, NULL, 0) < 0 || len <= 0) {
- return 0;
- }
-
- *uid = info.ki_uid;
- *gid = info.ki_groups[0];
- *nspid = pid;
- return 1;
-}
-
-// This is a Linux-specific API; nothing to do on macOS and FreeBSD
-int enter_mount_ns(int pid) {
- return 1;
-}
-
-// Not used on macOS and FreeBSD
-int alt_lookup_nspid(int pid) {
- return pid;
-}
-
-#endif
-
-
-// Check if remote JVM has already opened socket for Dynamic Attach
-static int check_socket(int pid) {
- char path[MAX_PATH];
- snprintf(path, sizeof(path), "%s/.java_pid%d", get_temp_path(), pid);
-
- struct stat stats;
- return stat(path, &stats) == 0 && S_ISSOCK(stats.st_mode);
-}
-
-// Check if a file is owned by current user
-static int check_file_owner(const char* path) {
- struct stat stats;
- if (stat(path, &stats) == 0 && stats.st_uid == geteuid()) {
- return 1;
- }
-
- // Some mounted filesystems may change the ownership of the file.
- // JVM will not trust such file, so it's better to remove it and try a different path
- unlink(path);
- return 0;
-}
-
-// Force remote JVM to start Attach listener.
-// HotSpot will start Attach listener in response to SIGQUIT if it sees .attach_pid file
-static int start_attach_mechanism(int pid, int nspid) {
- char path[MAX_PATH];
- snprintf(path, sizeof(path), "/proc/%d/cwd/.attach_pid%d", nspid, nspid);
-
- int fd = creat(path, 0660);
- if (fd == -1 || (close(fd) == 0 && !check_file_owner(path))) {
- // Failed to create attach trigger in current directory. Retry in /tmp
- snprintf(path, sizeof(path), "%s/.attach_pid%d", get_temp_path(), nspid);
- fd = creat(path, 0660);
- if (fd == -1) {
- return 0;
- }
- close(fd);
- }
-
- // We have to still use the host namespace pid here for the kill() call
- kill(pid, SIGQUIT);
-
- // Start with 20 ms sleep and increment delay each iteration
- struct timespec ts = {0, 20000000};
- int result;
- do {
- nanosleep(&ts, NULL);
- result = check_socket(nspid);
- } while (!result && (ts.tv_nsec += 20000000) < 300000000);
-
- unlink(path);
- return result;
-}
-
-// Connect to UNIX domain socket created by JVM for Dynamic Attach
-static int connect_socket(int pid) {
- int fd = socket(PF_UNIX, SOCK_STREAM, 0);
- if (fd == -1) {
- return -1;
- }
-
- struct sockaddr_un addr;
- addr.sun_family = AF_UNIX;
- int bytes = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/.java_pid%d", get_temp_path(), pid);
- if (bytes >= sizeof(addr.sun_path)) {
- addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
- }
-
- if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
- close(fd);
- return -1;
- }
- return fd;
-}
-
-// Send command with arguments to socket
-static int write_command(int fd, int argc, char** argv) {
- // Protocol version
- if (write(fd, "1", 2) <= 0) {
- return 0;
- }
-
- int i;
- for (i = 0; i < 4; i++) {
- const char* arg = i < argc ? argv[i] : "";
- if (write(fd, arg, strlen(arg) + 1) <= 0) {
- return 0;
- }
- }
- return 1;
-}
-
-// Mirror response from remote JVM to stdout
-static int read_response(int fd) {
- char buf[8192];
- ssize_t bytes = read(fd, buf, sizeof(buf) - 1);
- if (bytes <= 0) {
- perror("Error reading response");
- return 1;
- }
-
- // First line of response is the command result code
- buf[bytes] = 0;
- int result = atoi(buf);
-
- do {
- fwrite(buf, 1, bytes, stdout);
- bytes = read(fd, buf, sizeof(buf));
- } while (bytes > 0);
-
- return result;
-}
-
-int main(int argc, char** argv) {
- if (argc < 3) {
- printf("jattach " JATTACH_VERSION " built on " __DATE__ "\n"
- "Copyright 2018 Andrei Pangin\n"
- "\n"
- "Usage: jattach <pid> <cmd> [args ...]\n");
- return 1;
- }
-
- int pid = atoi(argv[1]);
- if (pid == 0) {
- perror("Invalid pid provided");
- return 1;
- }
-
- uid_t my_uid = geteuid();
- gid_t my_gid = getegid();
- uid_t target_uid = my_uid;
- gid_t target_gid = my_gid;
- int nspid = -1;
- if (!get_process_info(pid, &target_uid, &target_gid, &nspid)) {
- fprintf(stderr, "Process %d not found\n", pid);
- return 1;
- }
-
- if (nspid < 0) {
- nspid = alt_lookup_nspid(pid);
- }
-
- // Make sure our /tmp and target /tmp is the same
- if (!enter_mount_ns(pid)) {
- printf("WARNING: couldn't enter target process mnt namespace\n");
- }
-
- // Dynamic attach is allowed only for the clients with the same euid/egid.
- // If we are running under root, switch to the required euid/egid automatically.
- if ((my_gid != target_gid && setegid(target_gid) != 0) ||
- (my_uid != target_uid && seteuid(target_uid) != 0)) {
- perror("Failed to change credentials to match the target process");
- return 1;
- }
-
- // Make write() return EPIPE instead of silent process termination
- signal(SIGPIPE, SIG_IGN);
-
- if (!check_socket(nspid) && !start_attach_mechanism(pid, nspid)) {
- perror("Could not start attach mechanism");
- return 1;
- }
-
- int fd = connect_socket(nspid);
- if (fd == -1) {
- perror("Could not connect to socket");
- return 1;
- }
-
- printf("Connected to remote JVM\n");
- if (!write_command(fd, argc - 2, argv + 2)) {
- perror("Error writing to socket");
- close(fd);
- return 1;
- }
-
- printf("Response code = ");
- fflush(stdout);
-
- int result = read_response(fd);
- printf("\n");
- close(fd);
-
- return result;
-}
+++ /dev/null
-/*
- * Copyright 2016 Andrei Pangin
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <Windows.h>
-
-typedef HMODULE (WINAPI *GetModuleHandle_t)(LPCTSTR lpModuleName);
-typedef FARPROC (WINAPI *GetProcAddress_t)(HMODULE hModule, LPCSTR lpProcName);
-typedef int (__stdcall *JVM_EnqueueOperation_t)(char* cmd, char* arg0, char* arg1, char* arg2, char* pipename);
-
-typedef struct {
- GetModuleHandle_t GetModuleHandleA;
- GetProcAddress_t GetProcAddress;
- char strJvm[32];
- char strEnqueue[32];
- char pipeName[MAX_PATH];
- char args[4][MAX_PATH];
-} CallData;
-
-
-#pragma check_stack(off)
-
-// This code is executed in remote JVM process; be careful with memory it accesses
-DWORD WINAPI remote_thread_entry(LPVOID param) {
- CallData* data = (CallData*)param;
-
- HMODULE libJvm = data->GetModuleHandleA(data->strJvm);
- if (libJvm != NULL) {
- JVM_EnqueueOperation_t JVM_EnqueueOperation = (JVM_EnqueueOperation_t)data->GetProcAddress(libJvm, data->strEnqueue);
- if (JVM_EnqueueOperation != NULL) {
- return (DWORD)JVM_EnqueueOperation(data->args[0], data->args[1], data->args[2], data->args[3], data->pipeName);
- }
- }
-
- return 0xffff;
-}
-
-#pragma check_stack
-
-
-// Allocate executable memory in remote process
-static LPTHREAD_START_ROUTINE allocate_code(HANDLE hProcess) {
- SIZE_T codeSize = 1024;
- LPVOID code = VirtualAllocEx(hProcess, NULL, codeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
- if (code != NULL) {
- WriteProcessMemory(hProcess, code, remote_thread_entry, codeSize, NULL);
- }
- return (LPTHREAD_START_ROUTINE)code;
-}
-
-// Allocate memory for CallData in remote process
-static LPVOID allocate_data(HANDLE hProcess, char* pipeName, int argc, char** argv) {
- CallData data;
- data.GetModuleHandleA = GetModuleHandleA;
- data.GetProcAddress = GetProcAddress;
- strcpy(data.strJvm, "jvm");
- strcpy(data.strEnqueue, "JVM_EnqueueOperation");
- strcpy(data.pipeName, pipeName);
-
- int i;
- for (i = 0; i < 4; i++) {
- strcpy(data.args[i], i < argc ? argv[i] : "");
- }
-
- LPVOID remoteData = VirtualAllocEx(hProcess, NULL, sizeof(CallData), MEM_COMMIT, PAGE_READWRITE);
- if (remoteData != NULL) {
- WriteProcessMemory(hProcess, remoteData, &data, sizeof(data), NULL);
- }
- return remoteData;
-}
-
-static void print_error(const char* msg, DWORD code) {
- printf("%s (error code = %d)\n", msg, code);
-}
-
-// If the process is owned by another user, request SeDebugPrivilege to open it.
-// Debug privileges are typically granted to Administrators.
-static int enable_debug_privileges() {
- HANDLE hToken;
- if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES, FALSE, &hToken)) {
- if (!ImpersonateSelf(SecurityImpersonation) ||
- !OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES, FALSE, &hToken)) {
- return 0;
- }
- }
-
- LUID luid;
- if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {
- return 0;
- }
-
- TOKEN_PRIVILEGES tp;
- tp.PrivilegeCount = 1;
- tp.Privileges[0].Luid = luid;
- tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
-
- BOOL success = AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
- CloseHandle(hToken);
- return success ? 1 : 0;
-}
-
-// The idea of Dynamic Attach on Windows is to inject a thread into remote JVM
-// that calls JVM_EnqueueOperation() function exported by HotSpot DLL
-static int inject_thread(int pid, char* pipeName, int argc, char** argv) {
- HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid);
- if (hProcess == NULL && GetLastError() == ERROR_ACCESS_DENIED) {
- if (!enable_debug_privileges()) {
- print_error("Not enough privileges", GetLastError());
- return 0;
- }
- hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid);
- }
- if (hProcess == NULL) {
- print_error("Could not open process", GetLastError());
- return 0;
- }
-
- LPTHREAD_START_ROUTINE code = allocate_code(hProcess);
- LPVOID data = code != NULL ? allocate_data(hProcess, pipeName, argc, argv) : NULL;
- if (data == NULL) {
- print_error("Could not allocate memory in target process", GetLastError());
- CloseHandle(hProcess);
- return 0;
- }
-
- int success = 1;
- HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, code, data, 0, NULL);
- if (hThread == NULL) {
- print_error("Could not create remote thread", GetLastError());
- success = 0;
- } else {
- printf("Connected to remote process\n");
- WaitForSingleObject(hThread, INFINITE);
- DWORD exitCode;
- GetExitCodeThread(hThread, &exitCode);
- if (exitCode != 0) {
- print_error("Attach is not supported by the target process", exitCode);
- success = 0;
- }
- CloseHandle(hThread);
- }
-
- VirtualFreeEx(hProcess, code, 0, MEM_RELEASE);
- VirtualFreeEx(hProcess, data, 0, MEM_RELEASE);
- CloseHandle(hProcess);
-
- return success;
-}
-
-// JVM response is read from the pipe and mirrored to stdout
-static int read_response(HANDLE hPipe) {
- ConnectNamedPipe(hPipe, NULL);
-
- char buf[8192];
- DWORD bytesRead;
- if (!ReadFile(hPipe, buf, sizeof(buf) - 1, &bytesRead, NULL)) {
- print_error("Error reading response", GetLastError());
- return 1;
- }
-
- // First line of response is the command result code
- buf[bytesRead] = 0;
- int result = atoi(buf);
-
- do {
- fwrite(buf, 1, bytesRead, stdout);
- } while (ReadFile(hPipe, buf, sizeof(buf), &bytesRead, NULL));
-
- return result;
-}
-
-int main(int argc, char** argv) {
- if (argc < 3) {
- printf("jattach " JATTACH_VERSION " built on " __DATE__ "\n"
- "Copyright 2018 Andrei Pangin\n"
- "\n"
- "Usage: jattach <pid> <cmd> [args ...]\n");
- return 1;
- }
-
- int pid = atoi(argv[1]);
-
- char pipeName[MAX_PATH];
- sprintf(pipeName, "\\\\.\\pipe\\javatool%d", GetTickCount());
- HANDLE hPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
- 1, 4096, 8192, NMPWAIT_USE_DEFAULT_WAIT, NULL);
- if (hPipe == NULL) {
- print_error("Could not create pipe", GetLastError());
- return 1;
- }
-
- if (!inject_thread(pid, pipeName, argc - 2, argv + 2)) {
- CloseHandle(hPipe);
- return 1;
- }
-
- printf("Response code = ");
- fflush(stdout);
-
- int result = read_response(hPipe);
- printf("\n");
- CloseHandle(hPipe);
-
- return result;
-}
--- /dev/null
+/*
+ * Copyright 2021 Andrei Pangin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <unistd.h>
+#include "psutil.h"
+
+
+extern int is_openj9_process(int pid);
+extern int jattach_openj9(int pid, int nspid, int argc, char** argv);
+extern int jattach_hotspot(int pid, int nspid, int argc, char** argv);
+
+
+__attribute__((visibility("default")))
+int jattach(int pid, int argc, char** argv) {
+ uid_t my_uid = geteuid();
+ gid_t my_gid = getegid();
+ uid_t target_uid = my_uid;
+ gid_t target_gid = my_gid;
+ int nspid;
+ if (get_process_info(pid, &target_uid, &target_gid, &nspid) < 0) {
+ fprintf(stderr, "Process %d not found\n", pid);
+ return 1;
+ }
+
+ // Container support: switch to the target namespaces.
+ // Network and IPC namespaces are essential for OpenJ9 connection.
+ enter_ns(pid, "net");
+ enter_ns(pid, "ipc");
+ int mnt_changed = enter_ns(pid, "mnt");
+
+ // In HotSpot, dynamic attach is allowed only for the clients with the same euid/egid.
+ // If we are running under root, switch to the required euid/egid automatically.
+ if ((my_gid != target_gid && setegid(target_gid) != 0) ||
+ (my_uid != target_uid && seteuid(target_uid) != 0)) {
+ perror("Failed to change credentials to match the target process");
+ return 1;
+ }
+
+ get_tmp_path(mnt_changed > 0 ? nspid : pid);
+
+ // Make write() return EPIPE instead of abnormal process termination
+ signal(SIGPIPE, SIG_IGN);
+
+ if (is_openj9_process(nspid)) {
+ return jattach_openj9(pid, nspid, argc, argv);
+ } else {
+ return jattach_hotspot(pid, nspid, argc, argv);
+ }
+}
+
+int main(int argc, char** argv) {
+ if (argc < 3) {
+ printf("jattach " JATTACH_VERSION " built on " __DATE__ "\n"
+ "Copyright 2021 Andrei Pangin\n"
+ "\n"
+ "Usage: jattach <pid> <cmd> [args ...]\n"
+ "\n"
+ "Commands:\n"
+ " load threaddump dumpheap setflag properties\n"
+ " jcmd inspectheap datadump printflag agentProperties\n"
+ );
+ return 1;
+ }
+
+ int pid = atoi(argv[1]);
+ if (pid <= 0) {
+ fprintf(stderr, "%s is not a valid process ID\n", argv[1]);
+ return 1;
+ }
+
+ return jattach(pid, argc - 2, argv + 2);
+}
--- /dev/null
+/*
+ * Copyright 2021 Andrei Pangin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <time.h>
+#include <unistd.h>
+#include "psutil.h"
+
+
+// Check if remote JVM has already opened socket for Dynamic Attach
+static int check_socket(int pid) {
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "%s/.java_pid%d", tmp_path, pid);
+
+ struct stat stats;
+ return stat(path, &stats) == 0 && S_ISSOCK(stats.st_mode) ? 0 : -1;
+}
+
+// Check if a file is owned by current user
+static uid_t get_file_owner(const char* path) {
+ struct stat stats;
+ return stat(path, &stats) == 0 ? stats.st_uid : (uid_t)-1;
+}
+
+// Force remote JVM to start Attach listener.
+// HotSpot will start Attach listener in response to SIGQUIT if it sees .attach_pid file
+static int start_attach_mechanism(int pid, int nspid) {
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "/proc/%d/cwd/.attach_pid%d", nspid, nspid);
+
+ int fd = creat(path, 0660);
+ if (fd == -1 || (close(fd) == 0 && get_file_owner(path) != geteuid())) {
+ // Some mounted filesystems may change the ownership of the file.
+ // JVM will not trust such file, so it's better to remove it and try a different path
+ unlink(path);
+
+ // Failed to create attach trigger in current directory. Retry in /tmp
+ snprintf(path, sizeof(path), "%s/.attach_pid%d", tmp_path, nspid);
+ fd = creat(path, 0660);
+ if (fd == -1) {
+ return -1;
+ }
+ close(fd);
+ }
+
+ // We have to still use the host namespace pid here for the kill() call
+ kill(pid, SIGQUIT);
+
+ // Start with 20 ms sleep and increment delay each iteration. Total timeout is 6000 ms
+ struct timespec ts = {0, 20000000};
+ int result;
+ do {
+ nanosleep(&ts, NULL);
+ result = check_socket(nspid);
+ } while (result != 0 && (ts.tv_nsec += 20000000) < 500000000);
+
+ unlink(path);
+ return result;
+}
+
+// Connect to UNIX domain socket created by JVM for Dynamic Attach
+static int connect_socket(int pid) {
+ int fd = socket(PF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1) {
+ return -1;
+ }
+
+ struct sockaddr_un addr;
+ addr.sun_family = AF_UNIX;
+
+ int bytes = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/.java_pid%d", tmp_path, pid);
+ if (bytes >= sizeof(addr.sun_path)) {
+ addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
+ }
+
+ if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+// Send command with arguments to socket
+static int write_command(int fd, int argc, char** argv) {
+ // Protocol version
+ if (write(fd, "1", 2) <= 0) {
+ return -1;
+ }
+
+ int i;
+ for (i = 0; i < 4; i++) {
+ const char* arg = i < argc ? argv[i] : "";
+ if (write(fd, arg, strlen(arg) + 1) <= 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+// Mirror response from remote JVM to stdout
+static int read_response(int fd, int argc, char** argv) {
+ char buf[8192];
+ ssize_t bytes = read(fd, buf, sizeof(buf) - 1);
+ if (bytes == 0) {
+ fprintf(stderr, "Unexpected EOF reading response\n");
+ return 1;
+ } else if (bytes < 0) {
+ perror("Error reading response");
+ return 1;
+ }
+
+ // First line of response is the command result code
+ buf[bytes] = 0;
+ int result = atoi(buf);
+
+ // Special treatment of 'load' command
+ if (result == 0 && argc > 0 && strcmp(argv[0], "load") == 0) {
+ size_t total = bytes;
+ while (total < sizeof(buf) - 1 && (bytes = read(fd, buf + total, sizeof(buf) - 1 - total)) > 0) {
+ total += (size_t)bytes;
+ }
+ bytes = total;
+
+ // The second line is the result of 'load' command; since JDK 9 it starts from "return code: "
+ buf[bytes] = 0;
+ result = atoi(strncmp(buf + 2, "return code: ", 13) == 0 ? buf + 15 : buf + 2);
+ }
+
+ // Mirror JVM response to stdout
+ printf("JVM response code = ");
+ do {
+ fwrite(buf, 1, bytes, stdout);
+ bytes = read(fd, buf, sizeof(buf));
+ } while (bytes > 0);
+ printf("\n");
+
+ return result;
+}
+
+int jattach_hotspot(int pid, int nspid, int argc, char** argv) {
+ if (check_socket(nspid) != 0 && start_attach_mechanism(pid, nspid) != 0) {
+ perror("Could not start attach mechanism");
+ return 1;
+ }
+
+ int fd = connect_socket(nspid);
+ if (fd == -1) {
+ perror("Could not connect to socket");
+ return 1;
+ }
+
+ printf("Connected to remote JVM\n");
+
+ if (write_command(fd, argc, argv) != 0) {
+ perror("Error writing to socket");
+ close(fd);
+ return 1;
+ }
+
+ int result = read_response(fd, argc, argv);
+ close(fd);
+
+ return result;
+}
--- /dev/null
+/*
+ * Copyright 2021 Andrei Pangin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/file.h>
+#include <sys/ipc.h>
+#include <sys/sem.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <netinet/in.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <time.h>
+#include <unistd.h>
+#include "psutil.h"
+
+
+#define MAX_NOTIF_FILES 256
+static int notif_lock[MAX_NOTIF_FILES];
+
+
+// Translate HotSpot command to OpenJ9 equivalent
+static void translate_command(char* buf, size_t bufsize, int argc, char** argv) {
+ const char* cmd = argv[0];
+
+ if (strcmp(cmd, "load") == 0 && argc >= 2) {
+ if (argc > 2 && strcmp(argv[2], "true") == 0) {
+ snprintf(buf, bufsize, "ATTACH_LOADAGENTPATH(%s,%s)", argv[1], argc > 3 ? argv[3] : "");
+ } else {
+ snprintf(buf, bufsize, "ATTACH_LOADAGENT(%s,%s)", argv[1], argc > 3 ? argv[3] : "");
+ }
+
+ } else if (strcmp(cmd, "jcmd") == 0) {
+ snprintf(buf, bufsize, "ATTACH_DIAGNOSTICS:%s,%s", argc > 1 ? argv[1] : "help", argc > 2 ? argv[2] : "");
+
+ } else if (strcmp(cmd, "threaddump") == 0) {
+ snprintf(buf, bufsize, "ATTACH_DIAGNOSTICS:Thread.print,%s", argc > 1 ? argv[1] : "");
+
+ } else if (strcmp(cmd, "dumpheap") == 0) {
+ snprintf(buf, bufsize, "ATTACH_DIAGNOSTICS:Dump.heap,%s", argc > 1 ? argv[1] : "");
+
+ } else if (strcmp(cmd, "inspectheap") == 0) {
+ snprintf(buf, bufsize, "ATTACH_DIAGNOSTICS:GC.class_histogram,%s", argc > 1 ? argv[1] : "");
+
+ } else if (strcmp(cmd, "datadump") == 0) {
+ snprintf(buf, bufsize, "ATTACH_DIAGNOSTICS:Dump.java,%s", argc > 1 ? argv[1] : "");
+
+ } else if (strcmp(cmd, "properties") == 0) {
+ strcpy(buf, "ATTACH_GETSYSTEMPROPERTIES");
+
+ } else if (strcmp(cmd, "agentProperties") == 0) {
+ strcpy(buf, "ATTACH_GETAGENTPROPERTIES");
+
+ } else {
+ snprintf(buf, bufsize, "%s", cmd);
+ }
+
+ buf[bufsize - 1] = 0;
+}
+
+// Unescape a string and print it on stdout
+static void print_unescaped(char* str) {
+ char* p = strchr(str, '\n');
+ if (p != NULL) {
+ *p = 0;
+ }
+
+ while ((p = strchr(str, '\\')) != NULL) {
+ switch (p[1]) {
+ case 0:
+ break;
+ case 'f':
+ *p = '\f';
+ break;
+ case 'n':
+ *p = '\n';
+ break;
+ case 'r':
+ *p = '\r';
+ break;
+ case 't':
+ *p = '\t';
+ break;
+ default:
+ *p = p[1];
+ }
+ fwrite(str, 1, p - str + 1, stdout);
+ str = p + 2;
+ }
+
+ fwrite(str, 1, strlen(str), stdout);
+ printf("\n");
+}
+
+// Send command with arguments to socket
+static int write_command(int fd, const char* cmd) {
+ size_t len = strlen(cmd) + 1;
+ size_t off = 0;
+ while (off < len) {
+ ssize_t bytes = write(fd, cmd + off, len - off);
+ if (bytes <= 0) {
+ return -1;
+ }
+ off += bytes;
+ }
+ return 0;
+}
+
+// Mirror response from remote JVM to stdout
+static int read_response(int fd, const char* cmd) {
+ size_t size = 8192;
+ char* buf = malloc(size);
+
+ size_t off = 0;
+ while (buf != NULL) {
+ ssize_t bytes = read(fd, buf + off, size - off);
+ if (bytes == 0) {
+ fprintf(stderr, "Unexpected EOF reading response\n");
+ return 1;
+ } else if (bytes < 0) {
+ perror("Error reading response");
+ return 1;
+ }
+
+ off += bytes;
+ if (buf[off - 1] == 0) {
+ break;
+ }
+
+ if (off >= size) {
+ buf = realloc(buf, size *= 2);
+ }
+ }
+
+ if (buf == NULL) {
+ fprintf(stderr, "Failed to allocate memory for response\n");
+ return 1;
+ }
+
+ int result = 0;
+
+ if (strncmp(cmd, "ATTACH_LOADAGENT", 16) == 0) {
+ if (strncmp(buf, "ATTACH_ACK", 10) != 0) {
+ // AgentOnLoad error code comes right after AgentInitializationException
+ result = strncmp(buf, "ATTACH_ERR AgentInitializationException", 39) == 0 ? atoi(buf + 39) : -1;
+ }
+ } else if (strncmp(cmd, "ATTACH_DIAGNOSTICS:", 19) == 0) {
+ char* p = strstr(buf, "openj9_diagnostics.string_result=");
+ if (p != NULL) {
+ // The result of a diagnostic command is encoded in Java Properties format
+ print_unescaped(p + 33);
+ free(buf);
+ return result;
+ }
+ }
+
+ buf[off - 1] = '\n';
+ fwrite(buf, 1, off, stdout);
+
+ free(buf);
+ return result;
+}
+
+static void detach(int fd) {
+ if (write_command(fd, "ATTACH_DETACHED") != 0) {
+ return;
+ }
+
+ char buf[256];
+ ssize_t bytes;
+ do {
+ bytes = read(fd, buf, sizeof(buf));
+ } while (bytes > 0 && buf[bytes - 1] != 0);
+}
+
+static void close_with_errno(int fd) {
+ int saved_errno = errno;
+ close(fd);
+ errno = saved_errno;
+}
+
+static int acquire_lock(const char* subdir, const char* filename) {
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "%s/.com_ibm_tools_attach/%s/%s", tmp_path, subdir, filename);
+
+ int lock_fd = open(path, O_WRONLY | O_CREAT, 0666);
+ if (lock_fd < 0) {
+ return -1;
+ }
+
+ if (flock(lock_fd, LOCK_EX) < 0) {
+ close_with_errno(lock_fd);
+ return -1;
+ }
+
+ return lock_fd;
+}
+
+static void release_lock(int lock_fd) {
+ flock(lock_fd, LOCK_UN);
+ close(lock_fd);
+}
+
+static int create_attach_socket(int* port) {
+ // Try IPv6 socket first, then fall back to IPv4
+ int s = socket(AF_INET6, SOCK_STREAM, 0);
+ if (s != -1) {
+ struct sockaddr_in6 addr = {AF_INET6, 0};
+ socklen_t addrlen = sizeof(addr);
+ if (bind(s, (struct sockaddr*)&addr, addrlen) == 0 && listen(s, 0) == 0
+ && getsockname(s, (struct sockaddr*)&addr, &addrlen) == 0) {
+ *port = ntohs(addr.sin6_port);
+ return s;
+ }
+ } else if ((s = socket(AF_INET, SOCK_STREAM, 0)) != -1) {
+ struct sockaddr_in addr = {AF_INET, 0};
+ socklen_t addrlen = sizeof(addr);
+ if (bind(s, (struct sockaddr*)&addr, addrlen) == 0 && listen(s, 0) == 0
+ && getsockname(s, (struct sockaddr*)&addr, &addrlen) == 0) {
+ *port = ntohs(addr.sin_port);
+ return s;
+ }
+ }
+
+ close_with_errno(s);
+ return -1;
+}
+
+static void close_attach_socket(int s, int pid) {
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "%s/.com_ibm_tools_attach/%d/replyInfo", tmp_path, pid);
+ unlink(path);
+
+ close(s);
+}
+
+static unsigned long long random_key() {
+ unsigned long long key = time(NULL) * 0xc6a4a7935bd1e995ULL;
+
+ int fd = open("/dev/urandom", O_RDONLY);
+ if (fd >= 0) {
+ ssize_t r = read(fd, &key, sizeof(key));
+ (void)r;
+ close(fd);
+ }
+
+ return key;
+}
+
+static int write_reply_info(int pid, int port, unsigned long long key) {
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "%s/.com_ibm_tools_attach/%d/replyInfo", tmp_path, pid);
+
+ int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ if (fd < 0) {
+ return -1;
+ }
+
+ int chars = snprintf(path, sizeof(path), "%016llx\n%d\n", key, port);
+ write(fd, path, chars);
+ close(fd);
+
+ return 0;
+}
+
+static int notify_semaphore(int value, int notif_count) {
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "%s/.com_ibm_tools_attach/_notifier", tmp_path);
+
+ key_t sem_key = ftok(path, 0xa1);
+ int sem = semget(sem_key, 1, IPC_CREAT | 0666);
+ if (sem < 0) {
+ return -1;
+ }
+
+ struct sembuf op = {0, value, value < 0 ? IPC_NOWAIT : 0};
+ while (notif_count-- > 0) {
+ semop(sem, &op, 1);
+ }
+
+ return 0;
+}
+
+static int accept_client(int s, unsigned long long key) {
+ struct timeval tv = {5, 0};
+ setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+ int client = accept(s, NULL, NULL);
+ if (client < 0) {
+ perror("JVM did not respond");
+ return -1;
+ }
+
+ char buf[35];
+ size_t off = 0;
+ while (off < sizeof(buf)) {
+ ssize_t bytes = recv(client, buf + off, sizeof(buf) - off, 0);
+ if (bytes <= 0) {
+ fprintf(stderr, "The JVM connection was prematurely closed\n");
+ close(client);
+ return -1;
+ }
+ off += bytes;
+ }
+
+ char expected[35];
+ snprintf(expected, sizeof(expected), "ATTACH_CONNECTED %016llx ", key);
+ if (memcmp(buf, expected, sizeof(expected) - 1) != 0) {
+ fprintf(stderr, "Unexpected JVM response\n");
+ close(client);
+ return -1;
+ }
+
+ return client;
+}
+
+static int lock_notification_files() {
+ int count = 0;
+
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "%s/.com_ibm_tools_attach", tmp_path);
+
+ DIR* dir = opendir(path);
+ if (dir != NULL) {
+ struct dirent* entry;
+ while ((entry = readdir(dir)) != NULL && count < MAX_NOTIF_FILES) {
+ if (entry->d_name[0] >= '1' && entry->d_name[0] <= '9' &&
+ (entry->d_type == DT_DIR || entry->d_type == DT_UNKNOWN)) {
+ notif_lock[count++] = acquire_lock(entry->d_name, "attachNotificationSync");
+ }
+ }
+ closedir(dir);
+ }
+
+ return count;
+}
+
+static void unlock_notification_files(int count) {
+ int i;
+ for (i = 0; i < count; i++) {
+ if (notif_lock[i] >= 0) {
+ release_lock(notif_lock[i]);
+ }
+ }
+}
+
+int is_openj9_process(int pid) {
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "%s/.com_ibm_tools_attach/%d/attachInfo", tmp_path, pid);
+
+ struct stat stats;
+ return stat(path, &stats) == 0;
+}
+
+int jattach_openj9(int pid, int nspid, int argc, char** argv) {
+ int attach_lock = acquire_lock("", "_attachlock");
+ if (attach_lock < 0) {
+ perror("Could not acquire attach lock");
+ return 1;
+ }
+
+ int notif_count = 0;
+ int port;
+ int s = create_attach_socket(&port);
+ if (s < 0) {
+ perror("Failed to listen to attach socket");
+ goto error;
+ }
+
+ unsigned long long key = random_key();
+ if (write_reply_info(nspid, port, key) != 0) {
+ perror("Could not write replyInfo");
+ goto error;
+ }
+
+ notif_count = lock_notification_files();
+ if (notify_semaphore(1, notif_count) != 0) {
+ perror("Could not notify semaphore");
+ goto error;
+ }
+
+ int fd = accept_client(s, key);
+ if (fd < 0) {
+ // The error message has been already printed
+ goto error;
+ }
+
+ close_attach_socket(s, nspid);
+ unlock_notification_files(notif_count);
+ notify_semaphore(-1, notif_count);
+ release_lock(attach_lock);
+
+ printf("Connected to remote JVM\n");
+
+ char cmd[8192];
+ translate_command(cmd, sizeof(cmd), argc, argv);
+
+ if (write_command(fd, cmd) != 0) {
+ perror("Error writing to socket");
+ close(fd);
+ return 1;
+ }
+
+ int result = read_response(fd, cmd);
+ if (result != 1) {
+ detach(fd);
+ }
+ close(fd);
+
+ return result;
+
+error:
+ if (s >= 0) {
+ close_attach_socket(s, nspid);
+ }
+ if (notif_count > 0) {
+ unlock_notification_files(notif_count);
+ notify_semaphore(-1, notif_count);
+ }
+ release_lock(attach_lock);
+
+ return 1;
+}
--- /dev/null
+/*
+ * Copyright 2021 Andrei Pangin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include "psutil.h"
+
+
+// Less than MAX_PATH to leave some space for appending
+char tmp_path[MAX_PATH - 100];
+
+// Called just once to fill in tmp_path buffer
+void get_tmp_path(int pid) {
+ // Try user-provided alternative path first
+ const char* jattach_path = getenv("JATTACH_PATH");
+ if (jattach_path != NULL && strlen(jattach_path) < sizeof(tmp_path)) {
+ strcpy(tmp_path, jattach_path);
+ return;
+ }
+
+ if (get_tmp_path_r(pid, tmp_path, sizeof(tmp_path)) != 0) {
+ strcpy(tmp_path, "/tmp");
+ }
+}
+
+
+#ifdef __linux__
+
+// The first line of /proc/pid/sched looks like
+// java (1234, #threads: 12)
+// where 1234 is the host PID (before Linux 4.1)
+static int sched_get_host_pid(const char* path) {
+ static char* line = NULL;
+ size_t size;
+ int result = -1;
+
+ FILE* sched_file = fopen(path, "r");
+ if (sched_file != NULL) {
+ if (getline(&line, &size, sched_file) != -1) {
+ char* c = strrchr(line, '(');
+ if (c != NULL) {
+ result = atoi(c + 1);
+ }
+ }
+ fclose(sched_file);
+ }
+
+ return result;
+}
+
+// Linux kernels < 4.1 do not export NStgid field in /proc/pid/status.
+// Fortunately, /proc/pid/sched in a container exposes a host PID,
+// so the idea is to scan all container PIDs to find which one matches the host PID.
+static int alt_lookup_nspid(int pid) {
+ char path[300];
+ snprintf(path, sizeof(path), "/proc/%d/ns/pid", pid);
+
+ // Don't bother looking for container PID if we are already in the same PID namespace
+ struct stat oldns_stat, newns_stat;
+ if (stat("/proc/self/ns/pid", &oldns_stat) == 0 && stat(path, &newns_stat) == 0) {
+ if (oldns_stat.st_ino == newns_stat.st_ino) {
+ return pid;
+ }
+ }
+
+ // Otherwise browse all PIDs in the namespace of the target process
+ // trying to find which one corresponds to the host PID
+ snprintf(path, sizeof(path), "/proc/%d/root/proc", pid);
+ DIR* dir = opendir(path);
+ if (dir != NULL) {
+ struct dirent* entry;
+ while ((entry = readdir(dir)) != NULL) {
+ if (entry->d_name[0] >= '1' && entry->d_name[0] <= '9') {
+ // Check if /proc/<container-pid>/sched points back to <host-pid>
+ snprintf(path, sizeof(path), "/proc/%d/root/proc/%s/sched", pid, entry->d_name);
+ if (sched_get_host_pid(path) == pid) {
+ closedir(dir);
+ return atoi(entry->d_name);
+ }
+ }
+ }
+ closedir(dir);
+ }
+
+ // Could not find container pid; return host pid as the last resort
+ return pid;
+}
+
+int get_tmp_path_r(int pid, char* buf, size_t bufsize) {
+ if (snprintf(buf, bufsize, "/proc/%d/root/tmp", pid) >= bufsize) {
+ return -1;
+ }
+
+ // Check if the remote /tmp can be accessed via /proc/[pid]/root
+ struct stat stats;
+ return stat(buf, &stats);
+}
+
+int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid) {
+ // Parse /proc/pid/status to find process credentials
+ char path[64];
+ snprintf(path, sizeof(path), "/proc/%d/status", pid);
+ FILE* status_file = fopen(path, "r");
+ if (status_file == NULL) {
+ return -1;
+ }
+
+ char* line = NULL;
+ size_t size;
+ int nspid_found = 0;
+
+ while (getline(&line, &size, status_file) != -1) {
+ if (strncmp(line, "Uid:", 4) == 0) {
+ // Get the effective UID, which is the second value in the line
+ *uid = (uid_t)atoi(strchr(line + 5, '\t'));
+ } else if (strncmp(line, "Gid:", 4) == 0) {
+ // Get the effective GID, which is the second value in the line
+ *gid = (gid_t)atoi(strchr(line + 5, '\t'));
+ } else if (strncmp(line, "NStgid:", 7) == 0) {
+ // PID namespaces can be nested; the last one is the innermost one
+ *nspid = atoi(strrchr(line, '\t'));
+ nspid_found = 1;
+ }
+ }
+
+ free(line);
+ fclose(status_file);
+
+ if (!nspid_found) {
+ *nspid = alt_lookup_nspid(pid);
+ }
+
+ return 0;
+}
+
+int enter_ns(int pid, const char* type) {
+#ifdef __NR_setns
+ char path[64], selfpath[64];
+ snprintf(path, sizeof(path), "/proc/%d/ns/%s", pid, type);
+ snprintf(selfpath, sizeof(selfpath), "/proc/self/ns/%s", type);
+
+ struct stat oldns_stat, newns_stat;
+ if (stat(selfpath, &oldns_stat) == 0 && stat(path, &newns_stat) == 0) {
+ // Don't try to call setns() if we're in the same namespace already
+ if (oldns_stat.st_ino != newns_stat.st_ino) {
+ int newns = open(path, O_RDONLY);
+ if (newns < 0) {
+ return -1;
+ }
+
+ // Some ancient Linux distributions do not have setns() function
+ int result = syscall(__NR_setns, newns, 0);
+ close(newns);
+ return result < 0 ? -1 : 1;
+ }
+ }
+#endif // __NR_setns
+
+ return 0;
+}
+
+#elif defined(__APPLE__)
+
+#include <sys/sysctl.h>
+
+// macOS has a secure per-user temporary directory
+int get_tmp_path_r(int pid, char* buf, size_t bufsize) {
+ size_t path_size = confstr(_CS_DARWIN_USER_TEMP_DIR, buf, bufsize);
+ return path_size > 0 && path_size <= sizeof(tmp_path) ? 0 : -1;
+}
+
+int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid) {
+ int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
+ struct kinfo_proc info;
+ size_t len = sizeof(info);
+
+ if (sysctl(mib, 4, &info, &len, NULL, 0) < 0 || len <= 0) {
+ return -1;
+ }
+
+ *uid = info.kp_eproc.e_ucred.cr_uid;
+ *gid = info.kp_eproc.e_ucred.cr_gid;
+ *nspid = pid;
+ return 0;
+}
+
+// This is a Linux-specific API; nothing to do on macOS and FreeBSD
+int enter_ns(int pid, const char* type) {
+ return 0;
+}
+
+#else // __FreeBSD__
+
+#include <sys/sysctl.h>
+#include <sys/user.h>
+
+// Use default /tmp path on FreeBSD
+int get_tmp_path_r(int pid, char* buf, size_t bufsize) {
+ return -1;
+}
+
+int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid) {
+ int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
+ struct kinfo_proc info;
+ size_t len = sizeof(info);
+
+ if (sysctl(mib, 4, &info, &len, NULL, 0) < 0 || len <= 0) {
+ return -1;
+ }
+
+ *uid = info.ki_uid;
+ *gid = info.ki_groups[0];
+ *nspid = pid;
+ return 0;
+}
+
+// This is a Linux-specific API; nothing to do on macOS and FreeBSD
+int enter_ns(int pid, const char* type) {
+ return 0;
+}
+
+#endif
--- /dev/null
+/*
+ * Copyright 2021 Andrei Pangin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef _PSUTIL_H
+#define _PSUTIL_H
+
+#include <sys/types.h>
+
+
+#define MAX_PATH 1024
+extern char tmp_path[];
+
+// Gets /tmp path of the specified process, as it can be accessed from the host.
+// The obtained path is stored in the global tmp_path buffer.
+void get_tmp_path(int pid);
+
+// The reentrant version of get_tmp_path.
+// Stores the process-specific temporary path into the provided buffer.
+// Returns 0 on success, -1 on failure.
+int get_tmp_path_r(int pid, char* buf, size_t bufsize);
+
+// Gets the owner uid/gid of the target process, and also its pid inside the container.
+// Returns 0 on success, -1 on failure.
+int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid);
+
+// Tries to enter the namespace of the target process.
+// type of the namespace can be "mnt", "net", "pid", etc.
+// Returns 1, if the namespace has been successfully changed,
+// 0, if the target process is in the same namespace as the host,
+// -1, if the attempt failed.
+int enter_ns(int pid, const char* type);
+
+#endif // _PSUTIL_H
--- /dev/null
+/*
+ * Copyright 2016 Andrei Pangin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <Windows.h>
+
+typedef HMODULE (WINAPI *GetModuleHandle_t)(LPCTSTR lpModuleName);
+typedef FARPROC (WINAPI *GetProcAddress_t)(HMODULE hModule, LPCSTR lpProcName);
+typedef int (__stdcall *JVM_EnqueueOperation_t)(char* cmd, char* arg0, char* arg1, char* arg2, char* pipename);
+
+typedef struct {
+ GetModuleHandle_t GetModuleHandleA;
+ GetProcAddress_t GetProcAddress;
+ char strJvm[32];
+ char strEnqueue[32];
+ char pipeName[MAX_PATH];
+ char args[4][MAX_PATH];
+} CallData;
+
+
+#pragma check_stack(off)
+
+// This code is executed in remote JVM process; be careful with memory it accesses
+static DWORD WINAPI remote_thread_entry(LPVOID param) {
+ CallData* data = (CallData*)param;
+
+ HMODULE libJvm = data->GetModuleHandleA(data->strJvm);
+ if (libJvm == NULL) {
+ return 1001;
+ }
+
+ JVM_EnqueueOperation_t JVM_EnqueueOperation = (JVM_EnqueueOperation_t)data->GetProcAddress(libJvm, data->strEnqueue + 1);
+ if (JVM_EnqueueOperation == NULL) {
+ // Try alternative name: _JVM_EnqueueOperation@20
+ data->strEnqueue[21] = '@';
+ data->strEnqueue[22] = '2';
+ data->strEnqueue[23] = '0';
+ data->strEnqueue[24] = 0;
+
+ JVM_EnqueueOperation = (JVM_EnqueueOperation_t)data->GetProcAddress(libJvm, data->strEnqueue);
+ if (JVM_EnqueueOperation == NULL) {
+ return 1002;
+ }
+ }
+
+ return (DWORD)JVM_EnqueueOperation(data->args[0], data->args[1], data->args[2], data->args[3], data->pipeName);
+}
+
+static VOID WINAPI remote_thread_entry_end() {
+}
+
+#pragma check_stack
+
+
+// Allocate executable memory in remote process
+static LPTHREAD_START_ROUTINE allocate_code(HANDLE hProcess) {
+ SIZE_T codeSize = (SIZE_T)remote_thread_entry_end - (SIZE_T)remote_thread_entry;
+ LPVOID code = VirtualAllocEx(hProcess, NULL, codeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
+ if (code != NULL) {
+ WriteProcessMemory(hProcess, code, remote_thread_entry, codeSize, NULL);
+ }
+ return (LPTHREAD_START_ROUTINE)code;
+}
+
+// Allocate memory for CallData in remote process
+static LPVOID allocate_data(HANDLE hProcess, char* pipeName, int argc, char** argv) {
+ CallData data;
+ data.GetModuleHandleA = GetModuleHandleA;
+ data.GetProcAddress = GetProcAddress;
+ strcpy(data.strJvm, "jvm");
+ strcpy(data.strEnqueue, "_JVM_EnqueueOperation");
+ strcpy(data.pipeName, pipeName);
+
+ int i;
+ for (i = 0; i < 4; i++) {
+ strcpy(data.args[i], i < argc ? argv[i] : "");
+ }
+
+ LPVOID remoteData = VirtualAllocEx(hProcess, NULL, sizeof(CallData), MEM_COMMIT, PAGE_READWRITE);
+ if (remoteData != NULL) {
+ WriteProcessMemory(hProcess, remoteData, &data, sizeof(data), NULL);
+ }
+ return remoteData;
+}
+
+static void print_error(const char* msg, DWORD code) {
+ printf("%s (error code = %d)\n", msg, code);
+}
+
+// If the process is owned by another user, request SeDebugPrivilege to open it.
+// Debug privileges are typically granted to Administrators.
+static int enable_debug_privileges() {
+ HANDLE hToken;
+ if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES, FALSE, &hToken)) {
+ if (!ImpersonateSelf(SecurityImpersonation) ||
+ !OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES, FALSE, &hToken)) {
+ return 0;
+ }
+ }
+
+ LUID luid;
+ if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {
+ return 0;
+ }
+
+ TOKEN_PRIVILEGES tp;
+ tp.PrivilegeCount = 1;
+ tp.Privileges[0].Luid = luid;
+ tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
+
+ BOOL success = AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
+ CloseHandle(hToken);
+ return success ? 1 : 0;
+}
+
+// Fail if attaching 64-bit jattach to 32-bit JVM or vice versa
+static int check_bitness(HANDLE hProcess) {
+#ifdef _WIN64
+ BOOL targetWow64 = FALSE;
+ if (IsWow64Process(hProcess, &targetWow64) && targetWow64) {
+ printf("Cannot attach 64-bit process to 32-bit JVM\n");
+ return 0;
+ }
+#else
+ BOOL thisWow64 = FALSE;
+ BOOL targetWow64 = FALSE;
+ if (IsWow64Process(GetCurrentProcess(), &thisWow64) && IsWow64Process(hProcess, &targetWow64)) {
+ if (thisWow64 != targetWow64) {
+ printf("Cannot attach 32-bit process to 64-bit JVM\n");
+ return 0;
+ }
+ }
+#endif
+ return 1;
+}
+
+// The idea of Dynamic Attach on Windows is to inject a thread into remote JVM
+// that calls JVM_EnqueueOperation() function exported by HotSpot DLL
+static int inject_thread(int pid, char* pipeName, int argc, char** argv) {
+ HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid);
+ if (hProcess == NULL && GetLastError() == ERROR_ACCESS_DENIED) {
+ if (!enable_debug_privileges()) {
+ print_error("Not enough privileges", GetLastError());
+ return 0;
+ }
+ hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid);
+ }
+ if (hProcess == NULL) {
+ print_error("Could not open process", GetLastError());
+ return 0;
+ }
+
+ if (!check_bitness(hProcess)) {
+ CloseHandle(hProcess);
+ return 0;
+ }
+
+ LPTHREAD_START_ROUTINE code = allocate_code(hProcess);
+ LPVOID data = code != NULL ? allocate_data(hProcess, pipeName, argc, argv) : NULL;
+ if (data == NULL) {
+ print_error("Could not allocate memory in target process", GetLastError());
+ CloseHandle(hProcess);
+ return 0;
+ }
+
+ int success = 1;
+ HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, code, data, 0, NULL);
+ if (hThread == NULL) {
+ print_error("Could not create remote thread", GetLastError());
+ success = 0;
+ } else {
+ printf("Connected to remote process\n");
+ WaitForSingleObject(hThread, INFINITE);
+ DWORD exitCode;
+ GetExitCodeThread(hThread, &exitCode);
+ if (exitCode != 0) {
+ print_error("Attach is not supported by the target process", exitCode);
+ success = 0;
+ }
+ CloseHandle(hThread);
+ }
+
+ VirtualFreeEx(hProcess, code, 0, MEM_RELEASE);
+ VirtualFreeEx(hProcess, data, 0, MEM_RELEASE);
+ CloseHandle(hProcess);
+
+ return success;
+}
+
+// JVM response is read from the pipe and mirrored to stdout
+static int read_response(HANDLE hPipe) {
+ ConnectNamedPipe(hPipe, NULL);
+
+ char buf[8192];
+ DWORD bytesRead;
+ if (!ReadFile(hPipe, buf, sizeof(buf) - 1, &bytesRead, NULL)) {
+ print_error("Error reading response", GetLastError());
+ return 1;
+ }
+
+ // First line of response is the command result code
+ buf[bytesRead] = 0;
+ int result = atoi(buf);
+
+ do {
+ fwrite(buf, 1, bytesRead, stdout);
+ } while (ReadFile(hPipe, buf, sizeof(buf), &bytesRead, NULL));
+
+ return result;
+}
+
+int main(int argc, char** argv) {
+ if (argc < 3) {
+ printf("jattach " JATTACH_VERSION " built on " __DATE__ "\n"
+ "Copyright 2021 Andrei Pangin\n"
+ "\n"
+ "Usage: jattach <pid> <cmd> [args ...]\n"
+ "\n"
+ "Commands:\n"
+ " load threaddump dumpheap setflag properties\n"
+ " jcmd inspectheap datadump printflag agentProperties\n"
+ );
+ return 1;
+ }
+
+ int pid = atoi(argv[1]);
+
+ char pipeName[MAX_PATH];
+ sprintf(pipeName, "\\\\.\\pipe\\javatool%d", GetTickCount());
+ HANDLE hPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
+ 1, 4096, 8192, NMPWAIT_USE_DEFAULT_WAIT, NULL);
+ if (hPipe == NULL) {
+ print_error("Could not create pipe", GetLastError());
+ return 1;
+ }
+
+ if (!inject_thread(pid, pipeName, argc - 2, argv + 2)) {
+ CloseHandle(hPipe);
+ return 1;
+ }
+
+ printf("Response code = ");
+ fflush(stdout);
+
+ int result = read_response(hPipe);
+ printf("\n");
+ CloseHandle(hPipe);
+
+ return result;
+}