From: Sven Hoexter Date: Mon, 16 Aug 2021 08:48:46 +0000 (+0200) Subject: New upstream version 2.0 X-Git-Tag: upstream/2.0 X-Git-Url: https://git.sven.stormbind.net/?p=sven%2Fjattach.git;a=commitdiff_plain;h=56444fd02976a827c021c3f00de2793bf8c43e2b;hp=b330d6f5cff47911c6b1faf488d0d2791e1a181b New upstream version 2.0 --- 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/jattach_windows.c b/src/jattach_windows.c deleted file mode 100644 index 8f8d25f..0000000 --- a/src/jattach_windows.c +++ /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 -#include -#include - -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 [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 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/windows/jattach.c b/src/windows/jattach.c new file mode 100644 index 0000000..b43e2f8 --- /dev/null +++ b/src/windows/jattach.c @@ -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 +#include +#include + +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 [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; +}