From: Sven Höxter Date: Thu, 22 Aug 2019 15:46:34 +0000 (+0200) Subject: Import Upstream version 1.5 X-Git-Tag: upstream/1.5^0 X-Git-Url: https://git.sven.stormbind.net/?a=commitdiff_plain;h=b330d6f5cff47911c6b1faf488d0d2791e1a181b;p=sven%2Fjattach.git Import Upstream version 1.5 --- b330d6f5cff47911c6b1faf488d0d2791e1a181b diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..495a75e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8fd6d4d --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +JATTACH_VERSION=1.5 + +ifneq ($(findstring Windows,$(OS)),) + CL=cl.exe + CFLAGS=/O2 /D_CRT_SECURE_NO_WARNINGS + JATTACH_EXE=jattach.exe +else + UNAME_S:=$(shell uname -s) + ifneq ($(findstring FreeBSD,$(UNAME_S)),) + CC=cc + CFLAGS=-O2 + JATTACH_EXE=jattach + else + 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 + +all: build build/$(JATTACH_EXE) + +build: + mkdir -p build + +build/jattach: src/jattach_posix.c + $(CC) $(CFLAGS) -DJATTACH_VERSION=\"$(JATTACH_VERSION)\" -o $@ $^ + +build/jattach.exe: src/jattach_windows.c + $(CL) $(CFLAGS) /DJATTACH_VERSION=\"$(JATTACH_VERSION)\" /Fobuild/jattach.obj /Fe$@ $^ advapi32.lib /link /SUBSYSTEM:CONSOLE,5.02 + +clean: + rm -rf build + +$(RPM_ROOT): + mkdir -p $(RPM_ROOT) + +rpm-dirs: $(RPM_ROOT) + mkdir -p $(RPM_ROOT)/SPECS + mkdir -p $(SOURCES)/bin + mkdir -p $(RPM_ROOT)/BUILD + mkdir -p $(RPM_ROOT)/SRPMS + mkdir -p $(RPM_ROOT)/RPMS + mkdir -p $(RPM_ROOT)/tmp + +rpm: rpm-dirs build build/$(JATTACH_EXE) + cp $(SPEC_FILE) $(RPM_ROOT)/ + cp build/jattach $(SOURCES)/bin/ + rpmbuild -bb \ + --define '_topdir $(RPM_ROOT)' \ + --define '_tmppath $(RPM_ROOT)/tmp' \ + --clean \ + --rmsource \ + --rmspec \ + --buildroot $(RPM_ROOT)/tmp/build-root \ + $(RPM_ROOT)/jattach.spec diff --git a/README.md b/README.md new file mode 100644 index 0000000..7532b56 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +## jattach + +### JVM Dynamic Attach utility + +The utility to send commands to remote JVM via Dynamic Attach mechanism. + +All-in-one **jmap + jstack + jcmd + jinfo** functionality in a single tiny program. +No installed JDK required, works with just JRE. Supports Linux containers. + +This is the lightweight native version of HotSpot Attach API +https://docs.oracle.com/javase/8/docs/jdk/api/attach/spec/ + +[Supported commands](http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/812ed44725b8/src/share/vm/services/attachListener.cpp#l388): + - **load** : load agent library + - **properties** : print system properties + - **agentProperties** : print agent properties + - **datadump** : show heap and thread summary + - **threaddump** : dump all stack traces (like jstack) + - **dumpheap** : dump heap (like jmap) + - **inspectheap** : heap histogram (like jmap -histo) + - **setflag** : modify manageable VM flag + - **printflag** : print VM flag + - **jcmd** : execute jcmd command + +### Examples +#### Load JVMTI agent + + $ jattach load <.so-path> { true | false } [ options ] + +Where `true` means that the path is absolute, `false` -- the path is relative. + +`options` are passed to the agent. + +#### List available jcmd commands + + $ jattach jcmd "help -all" + +### Installation +#### FreeBSD + +On FreeBSD, you can use the following command to install `jattach` package: + + $ pkg install jattach + +#### Alpine Linux + +On Alpine Linux, you can use the following command to install `jattach` package from the edge/testing repository: + + $ apk add --no-cache jattach --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ + +#### Archlinux + +[jattach](https://aur.archlinux.org/packages/jattach/) package can be installed from [AUR](https://wiki.archlinux.org/index.php/Arch_User_Repository) using one of [AUR helpers](https://wiki.archlinux.org/index.php/AUR_helpers), e.g., `yay`: + + $ yay -S jattach diff --git a/jattach.spec b/jattach.spec new file mode 100644 index 0000000..92dff2e --- /dev/null +++ b/jattach.spec @@ -0,0 +1,39 @@ +Name: jattach +Version: 1.3 +Release: 1 +Summary: JVM Dynamic Attach utility + +Group: Development/Tools +License: ASL 2.0 +URL: https://github.com/apangin/jattach +Vendor: Andrei Pangin +Packager: Vadim Tsesko + +BuildRequires: gcc +BuildRequires: make + +%description +The utility to send commands to remote JVM via Dynamic Attach mechanism. + +All-in-one jmap + jstack + jcmd + jinfo functionality in a single tiny program. +No installed JDK required, works with just JRE. + +This is the lightweight native version of HotSpot Attach API: +https://docs.oracle.com/javase/8/docs/jdk/api/attach/spec/ + +%build +# Do nothing + +%install +BIN=%{buildroot}/usr/bin + +mkdir -p ${BIN} + +install -p -m 555 %{_sourcedir}/bin/jattach ${BIN} + +%files +/usr/bin/jattach + +%changelog +* Wed Nov 30 2016 Vadim Tsesko - 0.1-1 +- Initial version diff --git a/src/jattach_posix.c b/src/jattach_posix.c new file mode 100644 index 0000000..cdf7821 --- /dev/null +++ b/src/jattach_posix.c @@ -0,0 +1,433 @@ +/* + * 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 new file mode 100644 index 0000000..8f8d25f --- /dev/null +++ b/src/jattach_windows.c @@ -0,0 +1,219 @@ +/* + * 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; +}