From 91eb05a0b9cfabc7670a4f0f1e2b8083307a2fca Mon Sep 17 00:00:00 2001 From: Sven Hoexter Date: Mon, 16 Aug 2021 10:48:46 +0200 Subject: [PATCH 1/3] New upstream version 2.0 --- Makefile | 33 +- README.md | 19 +- jattach.spec | 11 +- src/jattach_posix.c | 433 ------------------ src/posix/jattach.c | 88 ++++ src/posix/jattach_hotspot.c | 184 ++++++++ src/posix/jattach_openj9.c | 439 +++++++++++++++++++ src/posix/psutil.c | 241 ++++++++++ src/posix/psutil.h | 46 ++ src/{jattach_windows.c => windows/jattach.c} | 65 ++- 10 files changed, 1101 insertions(+), 458 deletions(-) delete mode 100644 src/jattach_posix.c create mode 100644 src/posix/jattach.c create mode 100644 src/posix/jattach_hotspot.c create mode 100644 src/posix/jattach_openj9.c create mode 100644 src/posix/psutil.c create mode 100644 src/posix/psutil.h rename src/{jattach_windows.c => windows/jattach.c} (76%) diff --git a/Makefile b/Makefile index 8fd6d4d..4b04acf 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,46 @@ -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: diff --git a/README.md b/README.md index 7532b56..711ffe6 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,14 @@ https://docs.oracle.com/javase/8/docs/jdk/api/attach/spec/ - **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 load <.so-path> { true | false } [ options ] @@ -31,6 +37,13 @@ Where `true` means that the path is absolute, `false` -- the path is relative. `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 load instrument false "javaagent.jar=arguments" + #### List available jcmd commands $ jattach jcmd "help -all" @@ -44,9 +57,9 @@ On FreeBSD, you can use the following command to install `jattach` package: #### 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 diff --git a/jattach.spec b/jattach.spec index 92dff2e..529cf0e 100644 --- a/jattach.spec +++ b/jattach.spec @@ -1,5 +1,5 @@ Name: jattach -Version: 1.3 +Version: 2.0 Release: 1 Summary: JVM Dynamic Attach utility @@ -35,5 +35,14 @@ install -p -m 555 %{_sourcedir}/bin/jattach ${BIN} /usr/bin/jattach %changelog +* Wed Aug 11 2021 Vadim Tsesko - 2.0-1 +- Attach to OpenJ9 VMs +- Pass agent error codes +- Improved container support + +* Wed Jan 09 2018 Vadim Tsesko - 1.5-1 +- Improved attach to containerized JVMs +- chroot support + * Wed Nov 30 2016 Vadim Tsesko - 0.1-1 - Initial version diff --git a/src/jattach_posix.c b/src/jattach_posix.c deleted file mode 100644 index cdf7821..0000000 --- a/src/jattach_posix.c +++ /dev/null @@ -1,433 +0,0 @@ -/* - * 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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//sched points back to - 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 - -// 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 -#include - -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 [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; -} diff --git a/src/posix/jattach.c b/src/posix/jattach.c new file mode 100644 index 0000000..804d13d --- /dev/null +++ b/src/posix/jattach.c @@ -0,0 +1,88 @@ +/* + * 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 +#include +#include +#include +#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 [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); +} diff --git a/src/posix/jattach_hotspot.c b/src/posix/jattach_hotspot.c new file mode 100644 index 0000000..e23e460 --- /dev/null +++ b/src/posix/jattach_hotspot.c @@ -0,0 +1,184 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/src/posix/jattach_openj9.c b/src/posix/jattach_openj9.c new file mode 100644 index 0000000..f34f4cc --- /dev/null +++ b/src/posix/jattach_openj9.c @@ -0,0 +1,439 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/src/posix/psutil.c b/src/posix/psutil.c new file mode 100644 index 0000000..847a060 --- /dev/null +++ b/src/posix/psutil.c @@ -0,0 +1,241 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#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//sched points back to + 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 + +// 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 +#include + +// 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 diff --git a/src/posix/psutil.h b/src/posix/psutil.h new file mode 100644 index 0000000..fa1c416 --- /dev/null +++ b/src/posix/psutil.h @@ -0,0 +1,46 @@ +/* + * 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 + + +#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 diff --git a/src/jattach_windows.c b/src/windows/jattach.c similarity index 76% rename from src/jattach_windows.c rename to src/windows/jattach.c index 8f8d25f..b43e2f8 100644 --- a/src/jattach_windows.c +++ b/src/windows/jattach.c @@ -35,18 +35,32 @@ typedef struct { #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) { +static 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); + 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 0xffff; + 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 @@ -54,7 +68,7 @@ DWORD WINAPI remote_thread_entry(LPVOID param) { // Allocate executable memory in remote process static LPTHREAD_START_ROUTINE allocate_code(HANDLE hProcess) { - SIZE_T codeSize = 1024; + 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); @@ -68,7 +82,7 @@ static LPVOID allocate_data(HANDLE hProcess, char* pipeName, int argc, char** ar data.GetModuleHandleA = GetModuleHandleA; data.GetProcAddress = GetProcAddress; strcpy(data.strJvm, "jvm"); - strcpy(data.strEnqueue, "JVM_EnqueueOperation"); + strcpy(data.strEnqueue, "_JVM_EnqueueOperation"); strcpy(data.pipeName, pipeName); int i; @@ -113,6 +127,27 @@ static int enable_debug_privileges() { 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) { @@ -129,6 +164,11 @@ static int inject_thread(int pid, char* pipeName, int argc, char** argv) { 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) { @@ -186,9 +226,14 @@ static int read_response(HANDLE hPipe) { int main(int argc, char** argv) { if (argc < 3) { printf("jattach " JATTACH_VERSION " built on " __DATE__ "\n" - "Copyright 2018 Andrei Pangin\n" + "Copyright 2021 Andrei Pangin\n" + "\n" + "Usage: jattach [args ...]\n" "\n" - "Usage: jattach [args ...]\n"); + "Commands:\n" + " load threaddump dumpheap setflag properties\n" + " jcmd inspectheap datadump printflag agentProperties\n" + ); return 1; } -- 2.39.2 From 98ce3928a99273b631c525a81df022e6f8ec46a7 Mon Sep 17 00:00:00 2001 From: Sven Hoexter Date: Mon, 25 Jul 2022 20:38:38 +0200 Subject: [PATCH 2/3] New upstream version 2.1 --- Makefile | 7 +++-- README.md | 10 ++++++- jattach.spec | 6 +++- src/posix/jattach.c | 4 +++ src/posix/jattach_openj9.c | 7 ++++- src/posix/psutil.c | 13 +++++---- src/windows/jattach.c | 59 ++++++++++++++++++++++++++------------ 7 files changed, 77 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index 4b04acf..7ff43df 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -JATTACH_VERSION=2.0 +JATTACH_VERSION=2.1 ifneq ($(findstring Windows,$(OS)),) CL=cl.exe @@ -6,13 +6,14 @@ ifneq ($(findstring Windows,$(OS)),) JATTACH_EXE=jattach.exe JATTACH_DLL=jattach.dll else - CFLAGS ?= -O3 JATTACH_EXE=jattach UNAME_S:=$(shell uname -s) ifeq ($(UNAME_S),Darwin) + CFLAGS ?= -O3 -arch x86_64 -arch arm64 -mmacos-version-min=10.12 JATTACH_DLL=libjattach.dylib else + CFLAGS ?= -O3 JATTACH_DLL=libjattach.so endif @@ -38,7 +39,7 @@ 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 + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -fPIC -shared -fvisibility=hidden -o $@ src/posix/*.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 diff --git a/README.md b/README.md index 711ffe6..3699451 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ### JVM Dynamic Attach utility -The utility to send commands to remote JVM via Dynamic Attach mechanism. +The utility to send commands to a JVM process via Dynamic Attach mechanism. All-in-one **jmap + jstack + jcmd + jinfo** functionality in a single tiny program. No installed JDK required, works with just JRE. Supports Linux containers. @@ -66,3 +66,11 @@ On Alpine Linux, you can use the following command to install `jattach` package [jattach](https://aur.archlinux.org/packages/jattach/) package can be installed from [AUR](https://wiki.archlinux.org/index.php/Arch_User_Repository) using one of [AUR helpers](https://wiki.archlinux.org/index.php/AUR_helpers), e.g., `yay`: $ yay -S jattach + +#### Debian Linux + +On Debian Linux, you can use the following command to install `jattach` from the [official repository](https://packages.debian.org/search?keywords=jattach): + + $ apt install jattach + +Packages are provided for **bullseye** (stable), **bookworm** (testing) and **sid** (unstable). diff --git a/jattach.spec b/jattach.spec index 529cf0e..452ab91 100644 --- a/jattach.spec +++ b/jattach.spec @@ -1,5 +1,5 @@ Name: jattach -Version: 2.0 +Version: 2.1 Release: 1 Summary: JVM Dynamic Attach utility @@ -35,6 +35,10 @@ install -p -m 555 %{_sourcedir}/bin/jattach ${BIN} /usr/bin/jattach %changelog +* Mon Jul 25 2022 Vadim Tsesko - 2.1-1 +- Handle both tabs and spaces when parsing /proc/pid/status +- Socket timeout while reading response from OpenJ9 VM + * Wed Aug 11 2021 Vadim Tsesko - 2.0-1 - Attach to OpenJ9 VMs - Pass agent error codes diff --git a/src/posix/jattach.c b/src/posix/jattach.c index 804d13d..53d9dfb 100644 --- a/src/posix/jattach.c +++ b/src/posix/jattach.c @@ -64,6 +64,8 @@ int jattach(int pid, int argc, char** argv) { } } +#ifdef JATTACH_VERSION + int main(int argc, char** argv) { if (argc < 3) { printf("jattach " JATTACH_VERSION " built on " __DATE__ "\n" @@ -86,3 +88,5 @@ int main(int argc, char** argv) { return jattach(pid, argc - 2, argv + 2); } + +#endif // JATTACH_VERSION diff --git a/src/posix/jattach_openj9.c b/src/posix/jattach_openj9.c index f34f4cc..deab4c6 100644 --- a/src/posix/jattach_openj9.c +++ b/src/posix/jattach_openj9.c @@ -273,7 +273,8 @@ static int write_reply_info(int pid, int port, unsigned long long key) { } int chars = snprintf(path, sizeof(path), "%016llx\n%d\n", key, port); - write(fd, path, chars); + ssize_t r = write(fd, path, chars); + (void)r; close(fd); return 0; @@ -327,6 +328,10 @@ static int accept_client(int s, unsigned long long key) { return -1; } + // Reset the timeout, as the command execution may take arbitrary long time + struct timeval tv0 = {0, 0}; + setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &tv0, sizeof(tv0)); + return client; } diff --git a/src/posix/psutil.c b/src/posix/psutil.c index 847a060..9beea88 100644 --- a/src/posix/psutil.c +++ b/src/posix/psutil.c @@ -129,15 +129,18 @@ int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid) { int nspid_found = 0; while (getline(&line, &size, status_file) != -1) { - if (strncmp(line, "Uid:", 4) == 0) { + if (strncmp(line, "Uid:", 4) == 0 && strtok(line + 4, "\t ") != NULL) { // 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) { + *uid = (uid_t)atoi(strtok(NULL, "\t ")); + } else if (strncmp(line, "Gid:", 4) == 0 && strtok(line + 4, "\t ") != NULL) { // Get the effective GID, which is the second value in the line - *gid = (gid_t)atoi(strchr(line + 5, '\t')); + *gid = (gid_t)atoi(strtok(NULL, "\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')); + char* s; + for (s = strtok(line + 7, "\t "); s != NULL; s = strtok(NULL, "\t ")) { + *nspid = atoi(s); + } nspid_found = 1; } } diff --git a/src/windows/jattach.c b/src/windows/jattach.c index b43e2f8..1318a94 100644 --- a/src/windows/jattach.c +++ b/src/windows/jattach.c @@ -16,7 +16,8 @@ #include #include -#include +#include +#include typedef HMODULE (WINAPI *GetModuleHandle_t)(LPCTSTR lpModuleName); typedef FARPROC (WINAPI *GetProcAddress_t)(HMODULE hModule, LPCSTR lpProcName); @@ -223,32 +224,27 @@ static int read_response(HANDLE hPipe) { 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 [args ...]\n" - "\n" - "Commands:\n" - " load threaddump dumpheap setflag properties\n" - " jcmd inspectheap datadump printflag agentProperties\n" - ); - return 1; - } - - int pid = atoi(argv[1]); +int jattach(int pid, int argc, char** argv) { + // When attaching as an Administrator, make sure the target process can connect to our pipe, + // i.e. allow read-write access to everyone. For the complete format description, see + // https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format + SECURITY_ATTRIBUTES sec = {sizeof(SECURITY_ATTRIBUTES), NULL, FALSE}; + ConvertStringSecurityDescriptorToSecurityDescriptor("D:(A;;GRGW;;;WD)", SDDL_REVISION_1, + &sec.lpSecurityDescriptor, NULL); 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); + 1, 4096, 8192, NMPWAIT_USE_DEFAULT_WAIT, &sec); if (hPipe == NULL) { print_error("Could not create pipe", GetLastError()); + LocalFree(sec.lpSecurityDescriptor); return 1; } - if (!inject_thread(pid, pipeName, argc - 2, argv + 2)) { + LocalFree(sec.lpSecurityDescriptor); + + if (!inject_thread(pid, pipeName, argc, argv)) { CloseHandle(hPipe); return 1; } @@ -262,3 +258,30 @@ int main(int argc, char** argv) { return result; } + +#ifdef JATTACH_VERSION + +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 [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); +} + +#endif // JATTACH_VERSION -- 2.39.2 From 443b80898c49b466100dffc328ea8905887af2a1 Mon Sep 17 00:00:00 2001 From: Sven Hoexter Date: Tue, 16 Jan 2024 13:25:21 +0100 Subject: [PATCH 3/3] New upstream version 2.2 --- Makefile | 2 +- README.md | 23 +++++++-------- jattach.spec | 9 ++++-- src/posix/jattach.c | 19 ++++++------ src/posix/jattach_hotspot.c | 58 +++++++++++++++++++++++++------------ src/posix/jattach_openj9.c | 32 +++++++++++++------- src/posix/psutil.c | 7 ++--- src/posix/psutil.h | 10 ++++++- src/windows/jattach.c | 41 ++++++++++++++++---------- 9 files changed, 126 insertions(+), 75 deletions(-) diff --git a/Makefile b/Makefile index 7ff43df..078bca3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -JATTACH_VERSION=2.1 +JATTACH_VERSION=2.2 ifneq ($(findstring Windows,$(OS)),) CL=cl.exe diff --git a/README.md b/README.md index 3699451..9584204 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ https://docs.oracle.com/javase/8/docs/jdk/api/attach/spec/ ### Download -Binaries are available on the [Releases](https://github.com/apangin/jattach/releases) page. +Binaries are available on the [Releases](https://github.com/jattach/jattach/releases) page. On some platforms, you can also [install](#installation) jattach with a package manager. @@ -46,31 +46,30 @@ which takes .jar path and its arguments as a single options string. #### List available jcmd commands - $ jattach jcmd "help -all" + $ jattach jcmd help -all ### Installation -#### FreeBSD +#### Debian, Ubuntu -On FreeBSD, you can use the following command to install `jattach` package: +On Debian and Ubuntu, you can install `jattach` from the official repository: - $ pkg install jattach + # apt install jattach #### Alpine Linux -On Alpine Linux, you can use the following command to install `jattach` package from the edge/community repository: +On Alpine Linux, you can install `jattach` package from the edge/community repository: - $ apk add --no-cache jattach --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/ + # apk add --no-cache jattach --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/ #### Archlinux [jattach](https://aur.archlinux.org/packages/jattach/) package can be installed from [AUR](https://wiki.archlinux.org/index.php/Arch_User_Repository) using one of [AUR helpers](https://wiki.archlinux.org/index.php/AUR_helpers), e.g., `yay`: - $ yay -S jattach + # yay -S jattach -#### Debian Linux +#### FreeBSD -On Debian Linux, you can use the following command to install `jattach` from the [official repository](https://packages.debian.org/search?keywords=jattach): +On FreeBSD, you can use the following command to install `jattach`: - $ apt install jattach + # pkg install jattach -Packages are provided for **bullseye** (stable), **bookworm** (testing) and **sid** (unstable). diff --git a/jattach.spec b/jattach.spec index 452ab91..fdac742 100644 --- a/jattach.spec +++ b/jattach.spec @@ -1,11 +1,11 @@ Name: jattach -Version: 2.1 +Version: 2.2 Release: 1 Summary: JVM Dynamic Attach utility Group: Development/Tools License: ASL 2.0 -URL: https://github.com/apangin/jattach +URL: https://github.com/jattach/jattach Vendor: Andrei Pangin Packager: Vadim Tsesko @@ -35,6 +35,11 @@ install -p -m 555 %{_sourcedir}/bin/jattach ${BIN} /usr/bin/jattach %changelog +* Wed Jan 10 2024 Andrei Pangin - 2.2-1 +- Automatically concatenate jcmd arguments +- Fixed attach to OpenJ9 on macOS +- Fixed container support on Linux 3.x + * Mon Jul 25 2022 Vadim Tsesko - 2.1-1 - Handle both tabs and spaces when parsing /proc/pid/status - Socket timeout while reading response from OpenJ9 VM diff --git a/src/posix/jattach.c b/src/posix/jattach.c index 53d9dfb..a8a851e 100644 --- a/src/posix/jattach.c +++ b/src/posix/jattach.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 Andrei Pangin + * Copyright jattach authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,12 +22,14 @@ 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); +extern int jattach_openj9(int pid, int nspid, int argc, char** argv, int print_output); +extern int jattach_hotspot(int pid, int nspid, int argc, char** argv, int print_output); + +int mnt_changed = 0; __attribute__((visibility("default"))) -int jattach(int pid, int argc, char** argv) { +int jattach(int pid, int argc, char** argv, int print_output) { uid_t my_uid = geteuid(); gid_t my_gid = getegid(); uid_t target_uid = my_uid; @@ -42,7 +44,7 @@ int jattach(int pid, int argc, char** argv) { // Network and IPC namespaces are essential for OpenJ9 connection. enter_ns(pid, "net"); enter_ns(pid, "ipc"); - int mnt_changed = enter_ns(pid, "mnt"); + 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. @@ -58,9 +60,9 @@ int jattach(int pid, int argc, char** argv) { signal(SIGPIPE, SIG_IGN); if (is_openj9_process(nspid)) { - return jattach_openj9(pid, nspid, argc, argv); + return jattach_openj9(pid, nspid, argc, argv, print_output); } else { - return jattach_hotspot(pid, nspid, argc, argv); + return jattach_hotspot(pid, nspid, argc, argv, print_output); } } @@ -69,7 +71,6 @@ int jattach(int pid, int argc, char** 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 [args ...]\n" "\n" @@ -86,7 +87,7 @@ int main(int argc, char** argv) { return 1; } - return jattach(pid, argc - 2, argv + 2); + return jattach(pid, argc - 2, argv + 2, 1); } #endif // JATTACH_VERSION diff --git a/src/posix/jattach_hotspot.c b/src/posix/jattach_hotspot.c index e23e460..68d8805 100644 --- a/src/posix/jattach_hotspot.c +++ b/src/posix/jattach_hotspot.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 Andrei Pangin + * Copyright jattach authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,8 @@ #include "psutil.h" +extern int mnt_changed; + // Check if remote JVM has already opened socket for Dynamic Attach static int check_socket(int pid) { char path[MAX_PATH]; @@ -46,7 +48,7 @@ static uid_t get_file_owner(const char* path) { // 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); + snprintf(path, sizeof(path), "/proc/%d/cwd/.attach_pid%d", mnt_changed > 0 ? nspid : pid, nspid); int fd = creat(path, 0660); if (fd == -1 || (close(fd) == 0 && get_file_owner(path) != geteuid())) { @@ -102,23 +104,37 @@ static int connect_socket(int pid) { // Send command with arguments to socket static int write_command(int fd, int argc, char** argv) { + char buf[8192]; + const char* const limit = buf + sizeof(buf); + + // jcmd has 2 arguments maximum; merge excessive arguments into one + int cmd_args = argc >= 2 && strcmp(argv[0], "jcmd") == 0 ? 2 : argc >= 4 ? 4 : argc; + // Protocol version - if (write(fd, "1", 2) <= 0) { - return -1; - } + char* p = stpncpy(buf, "1", sizeof(buf)) + 1; int i; - for (i = 0; i < 4; i++) { - const char* arg = i < argc ? argv[i] : ""; - if (write(fd, arg, strlen(arg) + 1) <= 0) { + for (i = 0; i < argc && p < limit; i++) { + if (i >= cmd_args) p[-1] = ' '; + p = stpncpy(p, argv[i], limit - p) + 1; + } + for (i = cmd_args; i < 4 && p < limit; i++) { + *p++ = 0; + } + + const char* q = p < limit ? p : limit; + for (p = buf; p < q; ) { + ssize_t bytes = write(fd, p, q - p); + if (bytes <= 0) { return -1; } + p += (size_t)bytes; } return 0; } // Mirror response from remote JVM to stdout -static int read_response(int fd, int argc, char** argv) { +static int read_response(int fd, int argc, char** argv, int print_output) { char buf[8192]; ssize_t bytes = read(fd, buf, sizeof(buf) - 1); if (bytes == 0) { @@ -146,18 +162,20 @@ static int read_response(int fd, int argc, char** argv) { 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"); + if (print_output) { + // 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) { +int jattach_hotspot(int pid, int nspid, int argc, char** argv, int print_output) { if (check_socket(nspid) != 0 && start_attach_mechanism(pid, nspid) != 0) { perror("Could not start attach mechanism"); return 1; @@ -169,7 +187,9 @@ int jattach_hotspot(int pid, int nspid, int argc, char** argv) { return 1; } - printf("Connected to remote JVM\n"); + if (print_output) { + printf("Connected to remote JVM\n"); + } if (write_command(fd, argc, argv) != 0) { perror("Error writing to socket"); @@ -177,7 +197,7 @@ int jattach_hotspot(int pid, int nspid, int argc, char** argv) { return 1; } - int result = read_response(fd, argc, argv); + int result = read_response(fd, argc, argv, print_output); close(fd); return result; diff --git a/src/posix/jattach_openj9.c b/src/posix/jattach_openj9.c index deab4c6..90683c5 100644 --- a/src/posix/jattach_openj9.c +++ b/src/posix/jattach_openj9.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 Andrei Pangin + * Copyright jattach authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,11 @@ static void translate_command(char* buf, size_t bufsize, int argc, char** argv) } } else if (strcmp(cmd, "jcmd") == 0) { - snprintf(buf, bufsize, "ATTACH_DIAGNOSTICS:%s,%s", argc > 1 ? argv[1] : "help", argc > 2 ? argv[2] : ""); + size_t n = snprintf(buf, bufsize, "ATTACH_DIAGNOSTICS:%s", argc > 1 ? argv[1] : "help"); + int i; + for (i = 2; i < argc && n < bufsize; i++) { + n += snprintf(buf + n, bufsize - n, ",%s", argv[i]); + } } else if (strcmp(cmd, "threaddump") == 0) { snprintf(buf, bufsize, "ATTACH_DIAGNOSTICS:Thread.print,%s", argc > 1 ? argv[1] : ""); @@ -123,7 +127,7 @@ static int write_command(int fd, const char* cmd) { } // Mirror response from remote JVM to stdout -static int read_response(int fd, const char* cmd) { +static int read_response(int fd, const char* cmd, int print_output) { size_t size = 8192; char* buf = malloc(size); @@ -160,7 +164,7 @@ static int read_response(int fd, const char* cmd) { // 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) { + } else if (strncmp(cmd, "ATTACH_DIAGNOSTICS:", 19) == 0 && print_output) { char* p = strstr(buf, "openj9_diagnostics.string_result="); if (p != NULL) { // The result of a diagnostic command is encoded in Java Properties format @@ -170,8 +174,10 @@ static int read_response(int fd, const char* cmd) { } } - buf[off - 1] = '\n'; - fwrite(buf, 1, off, stdout); + if (print_output) { + buf[off - 1] = '\n'; + fwrite(buf, 1, off, stdout); + } free(buf); return result; @@ -221,7 +227,8 @@ 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}; + struct sockaddr_in6 addr = {0}; + addr.sin6_family = AF_INET6; socklen_t addrlen = sizeof(addr); if (bind(s, (struct sockaddr*)&addr, addrlen) == 0 && listen(s, 0) == 0 && getsockname(s, (struct sockaddr*)&addr, &addrlen) == 0) { @@ -229,7 +236,8 @@ static int create_attach_socket(int* port) { return s; } } else if ((s = socket(AF_INET, SOCK_STREAM, 0)) != -1) { - struct sockaddr_in addr = {AF_INET, 0}; + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; socklen_t addrlen = sizeof(addr); if (bind(s, (struct sockaddr*)&addr, addrlen) == 0 && listen(s, 0) == 0 && getsockname(s, (struct sockaddr*)&addr, &addrlen) == 0) { @@ -373,7 +381,7 @@ int is_openj9_process(int pid) { return stat(path, &stats) == 0; } -int jattach_openj9(int pid, int nspid, int argc, char** argv) { +int jattach_openj9(int pid, int nspid, int argc, char** argv, int print_output) { int attach_lock = acquire_lock("", "_attachlock"); if (attach_lock < 0) { perror("Could not acquire attach lock"); @@ -411,7 +419,9 @@ int jattach_openj9(int pid, int nspid, int argc, char** argv) { notify_semaphore(-1, notif_count); release_lock(attach_lock); - printf("Connected to remote JVM\n"); + if (print_output) { + printf("Connected to remote JVM\n"); + } char cmd[8192]; translate_command(cmd, sizeof(cmd), argc, argv); @@ -422,7 +432,7 @@ int jattach_openj9(int pid, int nspid, int argc, char** argv) { return 1; } - int result = read_response(fd, cmd); + int result = read_response(fd, cmd, print_output); if (result != 1) { detach(fd); } diff --git a/src/posix/psutil.c b/src/posix/psutil.c index 9beea88..d0287f5 100644 --- a/src/posix/psutil.c +++ b/src/posix/psutil.c @@ -1,5 +1,5 @@ /* - * Copyright 2021 Andrei Pangin + * Copyright jattach authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -93,15 +93,14 @@ static int alt_lookup_nspid(int pid) { // Check if /proc//sched points back to 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); + pid = atoi(entry->d_name); + break; } } } closedir(dir); } - // Could not find container pid; return host pid as the last resort return pid; } diff --git a/src/posix/psutil.h b/src/posix/psutil.h index fa1c416..5dc3040 100644 --- a/src/posix/psutil.h +++ b/src/posix/psutil.h @@ -1,5 +1,5 @@ /* - * Copyright 2021 Andrei Pangin + * Copyright jattach authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,10 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + #define MAX_PATH 1024 extern char tmp_path[]; @@ -43,4 +47,8 @@ int get_process_info(int pid, uid_t* uid, gid_t* gid, int* nspid); // -1, if the attempt failed. int enter_ns(int pid, const char* type); +#ifdef __cplusplus +} +#endif + #endif // _PSUTIL_H diff --git a/src/windows/jattach.c b/src/windows/jattach.c index 1318a94..f25f1d6 100644 --- a/src/windows/jattach.c +++ b/src/windows/jattach.c @@ -1,5 +1,5 @@ /* - * Copyright 2016 Andrei Pangin + * Copyright jattach authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,10 +85,19 @@ static LPVOID allocate_data(HANDLE hProcess, char* pipeName, int argc, char** ar strcpy(data.strJvm, "jvm"); strcpy(data.strEnqueue, "_JVM_EnqueueOperation"); strcpy(data.pipeName, pipeName); + data.args[0][0] = data.args[1][0] = data.args[2][0] = data.args[3][0] = 0; + // jcmd has 2 arguments maximum; merge excessive arguments into one + int cmd_args = argc >= 2 && strcmp(argv[0], "jcmd") == 0 ? 2 : argc >= 4 ? 4 : argc; + + size_t n = 0; int i; - for (i = 0; i < 4; i++) { - strcpy(data.args[i], i < argc ? argv[i] : ""); + for (i = 0; i < argc; i++) { + if (i < cmd_args) { + n = snprintf(data.args[i], sizeof(data.args[i]), "%s", argv[i]); + } else if (n < sizeof(data.args[cmd_args - 1])) { + n += snprintf(data.args[cmd_args - 1] + n, sizeof(data.args[cmd_args - 1]) - n, " %s", argv[i]); + } } LPVOID remoteData = VirtualAllocEx(hProcess, NULL, sizeof(CallData), MEM_COMMIT, PAGE_READWRITE); @@ -203,7 +212,7 @@ static int inject_thread(int pid, char* pipeName, int argc, char** argv) { } // JVM response is read from the pipe and mirrored to stdout -static int read_response(HANDLE hPipe) { +static int read_response(HANDLE hPipe, int print_output) { ConnectNamedPipe(hPipe, NULL); char buf[8192]; @@ -217,14 +226,19 @@ static int read_response(HANDLE hPipe) { buf[bytesRead] = 0; int result = atoi(buf); - do { - fwrite(buf, 1, bytesRead, stdout); - } while (ReadFile(hPipe, buf, sizeof(buf), &bytesRead, NULL)); + if (print_output) { + // Mirror JVM response to stdout + printf("JVM response code = "); + do { + fwrite(buf, 1, bytesRead, stdout); + } while (ReadFile(hPipe, buf, sizeof(buf), &bytesRead, NULL)); + printf("\n"); + } return result; } -int jattach(int pid, int argc, char** argv) { +int jattach(int pid, int argc, char** argv, int print_output) { // When attaching as an Administrator, make sure the target process can connect to our pipe, // i.e. allow read-write access to everyone. For the complete format description, see // https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format @@ -236,7 +250,7 @@ int jattach(int pid, int argc, char** argv) { 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, &sec); - if (hPipe == NULL) { + if (hPipe == INVALID_HANDLE_VALUE) { print_error("Could not create pipe", GetLastError()); LocalFree(sec.lpSecurityDescriptor); return 1; @@ -249,11 +263,7 @@ int jattach(int pid, int argc, char** argv) { return 1; } - printf("Response code = "); - fflush(stdout); - - int result = read_response(hPipe); - printf("\n"); + int result = read_response(hPipe, print_output); CloseHandle(hPipe); return result; @@ -264,7 +274,6 @@ int jattach(int pid, int argc, char** 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 [args ...]\n" "\n" @@ -281,7 +290,7 @@ int main(int argc, char** argv) { return 1; } - return jattach(pid, argc - 2, argv + 2); + return jattach(pid, argc - 2, argv + 2, 1); } #endif // JATTACH_VERSION -- 2.39.2