1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
|
/*
* Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.tools.attach;
import com.sun.tools.attach.AttachOperationFailedException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.spi.AttachProvider;
import java.io.InputStream;
import java.io.IOException;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
/*
* Linux implementation of HotSpotVirtualMachine
*/
public class VirtualMachineImpl extends HotSpotVirtualMachine {
// "/tmp" is used as a global well-known location for the files
// .java_pid<pid>. and .attach_pid<pid>. It is important that this
// location is the same for all processes, otherwise the tools
// will not be able to find all Hotspot processes.
// Any changes to this needs to be synchronized with HotSpot.
private static final String tmpdir = "/tmp";
String socket_path;
/**
* Attaches to the target VM
*/
VirtualMachineImpl(AttachProvider provider, String vmid)
throws AttachNotSupportedException, IOException
{
super(provider, vmid);
// This provider only understands pids
int pid;
try {
pid = Integer.parseInt(vmid);
} catch (NumberFormatException x) {
throw new AttachNotSupportedException("Invalid process identifier");
}
// Try to resolve to the "inner most" pid namespace
int ns_pid = getNamespacePid(pid);
// Find the socket file. If not found then we attempt to start the
// attach mechanism in the target VM by sending it a QUIT signal.
// Then we attempt to find the socket file again.
File socket_file = findSocketFile(pid, ns_pid);
socket_path = socket_file.getPath();
if (!socket_file.exists()) {
File f = createAttachFile(pid, ns_pid);
try {
sendQuitTo(pid);
// give the target VM time to start the attach mechanism
final int delay_step = 100;
final long timeout = attachTimeout();
long time_spend = 0;
long delay = 0;
do {
// Increase timeout on each attempt to reduce polling
delay += delay_step;
try {
Thread.sleep(delay);
} catch (InterruptedException x) { }
time_spend += delay;
if (time_spend > timeout/2 && !socket_file.exists()) {
// Send QUIT again to give target VM the last chance to react
sendQuitTo(pid);
}
} while (time_spend <= timeout && !socket_file.exists());
if (!socket_file.exists()) {
throw new AttachNotSupportedException(
String.format("Unable to open socket file %s: " +
"target process %d doesn't respond within %dms " +
"or HotSpot VM not loaded", socket_path, pid,
time_spend));
}
} finally {
f.delete();
}
}
// Check that the file owner/permission to avoid attaching to
// bogus process
checkPermissions(socket_path);
// Check that we can connect to the process
// - this ensures we throw the permission denied error now rather than
// later when we attempt to enqueue a command.
int s = socket();
try {
connect(s, socket_path);
} finally {
close(s);
}
}
/**
* Detach from the target VM
*/
public void detach() throws IOException {
synchronized (this) {
if (socket_path != null) {
socket_path = null;
}
}
}
// protocol version
private final static String PROTOCOL_VERSION = "1";
// known errors
private final static int ATTACH_ERROR_BADVERSION = 101;
/**
* Execute the given command in the target VM.
*/
InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
assert args.length <= 3; // includes null
checkNulls(args);
// did we detach?
synchronized (this) {
if (socket_path == null) {
throw new IOException("Detached from target VM");
}
}
// create UNIX socket
int s = socket();
// connect to target VM
try {
connect(s, socket_path);
} catch (IOException x) {
close(s);
throw x;
}
IOException ioe = null;
// connected - write request
// <ver> <cmd> <args...>
try {
writeString(s, PROTOCOL_VERSION);
writeString(s, cmd);
for (int i=0; i<3; i++) {
if (i < args.length && args[i] != null) {
writeString(s, (String)args[i]);
} else {
writeString(s, "");
}
}
} catch (IOException x) {
ioe = x;
}
// Create an input stream to read reply
SocketInputStream sis = new SocketInputStream(s);
// Read the command completion status
int completionStatus;
try {
completionStatus = readInt(sis);
} catch (IOException x) {
sis.close();
if (ioe != null) {
throw ioe;
} else {
throw x;
}
}
if (completionStatus != 0) {
// read from the stream and use that as the error message
String message = readErrorMessage(sis);
sis.close();
// In the event of a protocol mismatch then the target VM
// returns a known error so that we can throw a reasonable
// error.
if (completionStatus == ATTACH_ERROR_BADVERSION) {
throw new IOException("Protocol mismatch with target VM");
}
// Special-case the "load" command so that the right exception is
// thrown.
if (cmd.equals("load")) {
String msg = "Failed to load agent library";
if (!message.isEmpty())
msg += ": " + message;
throw new AgentLoadException(msg);
} else {
if (message.isEmpty())
message = "Command failed in target VM";
throw new AttachOperationFailedException(message);
}
}
// Return the input stream so that the command output can be read
return sis;
}
/*
* InputStream for the socket connection to get target VM
*/
private class SocketInputStream extends InputStream {
int s;
public SocketInputStream(int s) {
this.s = s;
}
public synchronized int read() throws IOException {
byte b[] = new byte[1];
int n = this.read(b, 0, 1);
if (n == 1) {
return b[0] & 0xff;
} else {
return -1;
}
}
public synchronized int read(byte[] bs, int off, int len) throws IOException {
if ((off < 0) || (off > bs.length) || (len < 0) ||
((off + len) > bs.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
return VirtualMachineImpl.read(s, bs, off, len);
}
public void close() throws IOException {
VirtualMachineImpl.close(s);
}
}
// Return the socket file for the given process.
private File findSocketFile(int pid, int ns_pid) throws IOException {
String root = findTargetProcessTmpDirectory(pid, ns_pid);
return new File(root, ".java_pid" + ns_pid);
}
// On Linux a simple handshake is used to start the attach mechanism
// if not already started. The client creates a .attach_pid<pid> file in the
// target VM's working directory (or temp directory), and the SIGQUIT handler
// checks for the file.
private File createAttachFile(int pid, int ns_pid) throws IOException {
String fn = ".attach_pid" + ns_pid;
String path = "/proc/" + pid + "/cwd/" + fn;
File f = new File(path);
try {
f.createNewFile();
} catch (IOException x) {
String root = findTargetProcessTmpDirectory(pid, ns_pid);
f = new File(root, fn);
f.createNewFile();
}
return f;
}
private String findTargetProcessTmpDirectory(int pid, int ns_pid) throws IOException {
String root;
if (pid != ns_pid) {
// A process may not exist in the same mount namespace as the caller.
// Instead, attach relative to the target root filesystem as exposed by
// procfs regardless of namespaces.
String procRootDirectory = "/proc/" + pid + "/root";
if (!Files.isReadable(Path.of(procRootDirectory))) {
throw new IOException(
String.format("Unable to access root directory %s " +
"of target process %d", procRootDirectory, pid));
}
root = procRootDirectory + "/" + tmpdir;
} else {
root = tmpdir;
}
return root;
}
/*
* Write/sends the given to the target VM. String is transmitted in
* UTF-8 encoding.
*/
private void writeString(int fd, String s) throws IOException {
if (s.length() > 0) {
byte b[];
try {
b = s.getBytes("UTF-8");
} catch (java.io.UnsupportedEncodingException x) {
throw new InternalError(x);
}
VirtualMachineImpl.write(fd, b, 0, b.length);
}
byte b[] = new byte[1];
b[0] = 0;
write(fd, b, 0, 1);
}
// Return the inner most namespaced PID if there is one,
// otherwise return the original PID.
private int getNamespacePid(int pid) throws AttachNotSupportedException, IOException {
// Assuming a real procfs sits beneath, reading this doesn't block
// nor will it consume a lot of memory.
String statusFile = "/proc/" + pid + "/status";
File f = new File(statusFile);
if (!f.exists()) {
return pid; // Likely a bad pid, but this is properly handled later.
}
Path statusPath = Paths.get(statusFile);
try {
for (String line : Files.readAllLines(statusPath, StandardCharsets.UTF_8)) {
String[] parts = line.split(":");
if (parts.length == 2 && parts[0].trim().equals("NSpid")) {
parts = parts[1].trim().split("\\s+");
// The last entry represents the PID the JVM "thinks" it is.
// Even in non-namespaced pids these entries should be
// valid. You could refer to it as the inner most pid.
int ns_pid = Integer.parseInt(parts[parts.length - 1]);
return ns_pid;
}
}
// Old kernels may not have NSpid field (i.e. 3.10).
// Fallback to original pid in the event we cannot deduce.
return pid;
} catch (NumberFormatException | IOException x) {
throw new AttachNotSupportedException("Unable to parse namespace");
}
}
//-- native methods
static native void sendQuitTo(int pid) throws IOException;
static native void checkPermissions(String path) throws IOException;
static native int socket() throws IOException;
static native void connect(int fd, String path) throws IOException;
static native void close(int fd) throws IOException;
static native int read(int fd, byte buf[], int off, int bufLen) throws IOException;
static native void write(int fd, byte buf[], int off, int bufLen) throws IOException;
static {
System.loadLibrary("attach");
}
}
|