]> git.sven.stormbind.net Git - sven/jattach.git/commitdiff
New upstream version 2.0 upstream/2.0
authorSven Hoexter <sven@stormbind.net>
Mon, 16 Aug 2021 08:48:46 +0000 (10:48 +0200)
committerSven Hoexter <sven@stormbind.net>
Mon, 16 Aug 2021 08:48:46 +0000 (10:48 +0200)
Makefile
README.md
jattach.spec
src/jattach_posix.c [deleted file]
src/jattach_windows.c [deleted file]
src/posix/jattach.c [new file with mode: 0644]
src/posix/jattach_hotspot.c [new file with mode: 0644]
src/posix/jattach_openj9.c [new file with mode: 0644]
src/posix/psutil.c [new file with mode: 0644]
src/posix/psutil.h [new file with mode: 0644]
src/windows/jattach.c [new file with mode: 0644]

index 8fd6d4dee1ca4a2633e81d697ba538cd14959966..4b04acfd31455cd610d7a7eb5b7616550aa12be2 100644 (file)
--- 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
 
 ifneq ($(findstring Windows,$(OS)),)
   CL=cl.exe
   CFLAGS=/O2 /D_CRT_SECURE_NO_WARNINGS
   JATTACH_EXE=jattach.exe
+  JATTACH_DLL=jattach.dll
 else 
 else 
+  CFLAGS ?= -O3
+  JATTACH_EXE=jattach
+
   UNAME_S:=$(shell uname -s)
   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
   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
     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
 
   endif
 endif
 
+
+.PHONY: all dll clean rpm-dirs rpm
+
 all: build build/$(JATTACH_EXE)
 
 all: build build/$(JATTACH_EXE)
 
+dll: build build/$(JATTACH_DLL)
+
 build:
        mkdir -p build
 
 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:
        $(CL) $(CFLAGS) /DJATTACH_VERSION=\"$(JATTACH_VERSION)\" /Fobuild/jattach.obj /Fe$@ $^ advapi32.lib /link /SUBSYSTEM:CONSOLE,5.02
 
 clean:
index 7532b569201ea6f2f1c8ad95d909f73f06f6a4ce..711ffe647bc81b9d5fa14c873cfa8c431cdf2e5a 100644 (file)
--- 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
 
  - **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
 ### Examples
-#### Load JVMTI agent
+#### Load native agent
 
     $ jattach <pid> load <.so-path> { true | false } [ options ]
 
 
     $ jattach <pid> 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.
 
 
 `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"
 #### List available jcmd commands 
 
     $ jattach <pid> jcmd "help -all"
@@ -44,9 +57,9 @@ On FreeBSD, you can use the following command to install `jattach` package:
 
 #### Alpine Linux
 
 
 #### 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
 
 
 #### Archlinux
 
index 92dff2eb41645ff16179371ccaf166c35392439a..529cf0e7b1c18687b083ebbcdab149af7108b21c 100644 (file)
@@ -1,5 +1,5 @@
 Name:          jattach
 Name:          jattach
-Version:       1.3
+Version:       2.0
 Release:       1
 Summary:       JVM Dynamic Attach utility
 
 Release:       1
 Summary:       JVM Dynamic Attach utility
 
@@ -35,5 +35,14 @@ install -p -m 555 %{_sourcedir}/bin/jattach ${BIN}
 /usr/bin/jattach
 
 %changelog
 /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
 * Wed Nov 30 2016 Vadim Tsesko <incubos@yandex.com> - 0.1-1
 - Initial version
diff --git a/src/jattach_posix.c b/src/jattach_posix.c
deleted file mode 100644 (file)
index cdf7821..0000000
+++ /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 <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;
-}
diff --git a/src/jattach_windows.c b/src/jattach_windows.c
deleted file mode 100644 (file)
index 8f8d25f..0000000
+++ /dev/null
@@ -1,219 +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 <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;
-}
diff --git a/src/posix/jattach.c b/src/posix/jattach.c
new file mode 100644 (file)
index 0000000..804d13d
--- /dev/null
@@ -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 <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);
+}
diff --git a/src/posix/jattach_hotspot.c b/src/posix/jattach_hotspot.c
new file mode 100644 (file)
index 0000000..e23e460
--- /dev/null
@@ -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 <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;
+}
diff --git a/src/posix/jattach_openj9.c b/src/posix/jattach_openj9.c
new file mode 100644 (file)
index 0000000..f34f4cc
--- /dev/null
@@ -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 <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;
+}
diff --git a/src/posix/psutil.c b/src/posix/psutil.c
new file mode 100644 (file)
index 0000000..847a060
--- /dev/null
@@ -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 <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
diff --git a/src/posix/psutil.h b/src/posix/psutil.h
new file mode 100644 (file)
index 0000000..fa1c416
--- /dev/null
@@ -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 <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
diff --git a/src/windows/jattach.c b/src/windows/jattach.c
new file mode 100644 (file)
index 0000000..b43e2f8
--- /dev/null
@@ -0,0 +1,264 @@
+/*
+ * 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;
+}