8339289: Enhance Attach API to support arbitrary length arguments - Windows

Reviewed-by: kevinw, sspitsyn
This commit is contained in:
Alex Menkov 2024-10-25 18:08:21 +00:00
parent ff165f9f0c
commit 36d71735e3
7 changed files with 845 additions and 213 deletions

View File

@ -32,17 +32,24 @@
#include <signal.h> // SIGBREAK
#include <stdio.h>
// The AttachListener thread services a queue of operations. It blocks in the dequeue
// function until an operation is enqueued. A client enqueues an operation by creating
// The AttachListener thread services a queue of operation requests. It blocks in the dequeue
// function until a request is enqueued. A client enqueues a request by creating
// a thread in this process using the Win32 CreateRemoteThread function. That thread
// executes a small stub generated by the client. The stub invokes the
// JVM_EnqueueOperation function which checks the operation parameters and enqueues
// the operation to the queue serviced by the attach listener. The thread created by
// JVM_EnqueueOperation or JVM_EnqueueOperation_v2 function which checks the operation parameters
// and enqueues the operation request to the queue. The thread created by
// the client is a native thread and is restricted to a single page of stack. To keep
// it simple operations are pre-allocated at initialization time. An enqueue thus
// takes a preallocated operation, populates the operation parameters, adds it to
// it simple operation requests are pre-allocated at initialization time. An enqueue thus
// takes a preallocated request, populates the operation parameters, adds it to
// queue and wakes up the attach listener.
//
// Differences between Attach API v1 and v2:
// In v1 (jdk6+) client calls JVM_EnqueueOperation function and passes all operation parameters
// as arguments of the function.
// In v2 (jdk24+) client calls JVM_EnqueueOperation_v2 function and passes only pipe name.
// Attach listeners connects to the pipe (in read/write mode) and reads all operation parameters
// (the same way as other platform implementations read them using sockets).
//
// When an operation has completed the attach listener is required to send the
// operation result and any result data to the client. In this implementation the
// client is a pipe server. In the enqueue operation it provides the name of pipe
@ -55,8 +62,154 @@
// this wasn't worth worrying about.
// forward reference
class Win32AttachOperation;
class PipeChannel : public AttachOperation::RequestReader, public AttachOperation::ReplyWriter {
private:
HANDLE _hPipe;
public:
PipeChannel() : _hPipe(INVALID_HANDLE_VALUE) {}
~PipeChannel() {
close();
}
bool opened() const {
return _hPipe != INVALID_HANDLE_VALUE;
}
bool open(const char* pipe, bool write_only) {
_hPipe = ::CreateFile(pipe,
GENERIC_WRITE | (write_only ? 0 : GENERIC_READ),
0, // no sharing
nullptr, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
nullptr); // no template file
if (_hPipe == INVALID_HANDLE_VALUE) {
log_error(attach)("could not open (%d) pipe %s", GetLastError(), pipe);
return false;
}
return true;
}
void close() {
if (opened()) {
CloseHandle(_hPipe);
_hPipe = INVALID_HANDLE_VALUE;
}
}
// RequestReader
int read(void* buffer, int size) override {
assert(opened(), "must be");
DWORD nread;
BOOL fSuccess = ReadFile(_hPipe,
buffer,
(DWORD)size,
&nread,
nullptr); // not overlapped
return fSuccess ? (int)nread : -1;
}
// ReplyWriter
int write(const void* buffer, int size) override {
assert(opened(), "must be");
DWORD written;
BOOL fSuccess = WriteFile(_hPipe,
buffer,
(DWORD)size,
&written,
nullptr); // not overlapped
return fSuccess ? (int)written : -1;
}
void flush() override {
assert(opened(), "must be");
FlushFileBuffers(_hPipe);
}
};
class Win32AttachOperation: public AttachOperation {
public:
enum {
pipe_name_max = 256 // maximum pipe name
};
private:
PipeChannel _pipe;
public:
// for v1 pipe must be write-only
void open_pipe(const char* pipe_name, bool write_only) {
_pipe.open(pipe_name, write_only);
}
bool read_request() {
return AttachOperation::read_request(&_pipe);
}
public:
void complete(jint result, bufferedStream* result_stream) override;
};
// Win32AttachOperationRequest is an element of AttachOperation request list.
class Win32AttachOperationRequest {
private:
AttachAPIVersion _ver;
char _name[AttachOperation::name_length_max + 1];
char _arg[AttachOperation::arg_count_max][AttachOperation::arg_length_max + 1];
char _pipe[Win32AttachOperation::pipe_name_max + 1];
Win32AttachOperationRequest* _next;
void set_value(char* dst, const char* str, size_t dst_size) {
if (str != nullptr) {
assert(strlen(str) < dst_size, "exceeds maximum length");
strncpy(dst, str, dst_size - 1);
dst[dst_size - 1] = '\0';
} else {
strcpy(dst, "");
}
}
public:
void set(AttachAPIVersion ver, const char* pipename,
const char* cmd = nullptr,
const char* arg0 = nullptr,
const char* arg1 = nullptr,
const char* arg2 = nullptr) {
_ver = ver;
set_value(_name, cmd, sizeof(_name));
set_value(_arg[0], arg0, sizeof(_arg[0]));
set_value(_arg[1], arg1, sizeof(_arg[1]));
set_value(_arg[2], arg2, sizeof(_arg[2]));
set_value(_pipe, pipename, sizeof(_pipe));
}
AttachAPIVersion ver() const {
return _ver;
}
const char* cmd() const {
return _name;
}
const char* arg(int i) const {
return (i >= 0 && i < AttachOperation::arg_count_max) ? _arg[i] : nullptr;
}
const char* pipe() const {
return _pipe;
}
Win32AttachOperationRequest* next() const {
return _next;
}
void set_next(Win32AttachOperationRequest* next) {
_next = next;
}
// noarg constructor as operation is preallocated
Win32AttachOperationRequest() {
set(ATTACH_API_V1, "<nopipe>");
set_next(nullptr);
}
};
class Win32AttachListener: AllStatic {
@ -69,18 +222,18 @@ class Win32AttachListener: AllStatic {
static HANDLE _mutex;
// head of preallocated operations list
static Win32AttachOperation* _avail;
static Win32AttachOperationRequest* _avail;
// head and tail of enqueue operations list
static Win32AttachOperation* _head;
static Win32AttachOperation* _tail;
static Win32AttachOperationRequest* _head;
static Win32AttachOperationRequest* _tail;
static Win32AttachOperation* head() { return _head; }
static void set_head(Win32AttachOperation* head) { _head = head; }
static Win32AttachOperationRequest* head() { return _head; }
static void set_head(Win32AttachOperationRequest* head) { _head = head; }
static Win32AttachOperation* tail() { return _tail; }
static void set_tail(Win32AttachOperation* tail) { _tail = tail; }
static Win32AttachOperationRequest* tail() { return _tail; }
static void set_tail(Win32AttachOperationRequest* tail) { _tail = tail; }
// A semaphore is used for communication about enqueued operations.
@ -101,11 +254,12 @@ class Win32AttachListener: AllStatic {
static int init();
static HANDLE mutex() { return _mutex; }
static Win32AttachOperation* available() { return _avail; }
static void set_available(Win32AttachOperation* avail) { _avail = avail; }
static Win32AttachOperationRequest* available() { return _avail; }
static void set_available(Win32AttachOperationRequest* avail) { _avail = avail; }
// enqueue an operation to the end of the list
static int enqueue(char* cmd, char* arg1, char* arg2, char* arg3, char* pipename);
static int enqueue(AttachAPIVersion ver, const char* cmd,
const char* arg1, const char* arg2, const char* arg3, const char* pipename);
// dequeue an operation from from head of the list
static Win32AttachOperation* dequeue();
@ -114,48 +268,9 @@ class Win32AttachListener: AllStatic {
// statics
HANDLE Win32AttachListener::_mutex;
HANDLE Win32AttachListener::_enqueued_ops_semaphore;
Win32AttachOperation* Win32AttachListener::_avail;
Win32AttachOperation* Win32AttachListener::_head;
Win32AttachOperation* Win32AttachListener::_tail;
// Win32AttachOperation is an AttachOperation that additionally encapsulates the name
// of a pipe which is used to send the operation reply/output to the client.
// Win32AttachOperation can also be linked in a list.
class Win32AttachOperation: public AttachOperation {
private:
friend class Win32AttachListener;
enum {
pipe_name_max = 256 // maximum pipe name
};
char _pipe[pipe_name_max + 1];
const char* pipe() const { return _pipe; }
void set_pipe(const char* pipe) {
assert(strlen(pipe) <= pipe_name_max, "exceeds maximum length of pipe name");
os::snprintf(_pipe, sizeof(_pipe), "%s", pipe);
}
HANDLE open_pipe();
static BOOL write_pipe(HANDLE hPipe, char* buf, int len);
Win32AttachOperation* _next;
Win32AttachOperation* next() const { return _next; }
void set_next(Win32AttachOperation* next) { _next = next; }
// noarg constructor as operation is preallocated
Win32AttachOperation() : AttachOperation("<noname>") {
set_pipe("<nopipe>");
set_next(nullptr);
}
public:
void complete(jint result, bufferedStream* result_stream);
};
Win32AttachOperationRequest* Win32AttachListener::_avail;
Win32AttachOperationRequest* Win32AttachListener::_head;
Win32AttachOperationRequest* Win32AttachListener::_tail;
// Preallocate the maximum number of operations that can be enqueued.
@ -171,18 +286,24 @@ int Win32AttachListener::init() {
set_available(nullptr);
for (int i=0; i<max_enqueued_operations; i++) {
Win32AttachOperation* op = new Win32AttachOperation();
Win32AttachOperationRequest* op = new Win32AttachOperationRequest();
op->set_next(available());
set_available(op);
}
AttachListener::set_supported_version(ATTACH_API_V2);
return 0;
}
// Enqueue an operation. This is called from a native thread that is not attached to VM.
// Also we need to be careful not to execute anything that results in more than a 4k stack.
//
int Win32AttachListener::enqueue(char* cmd, char* arg0, char* arg1, char* arg2, char* pipename) {
int Win32AttachListener::enqueue(AttachAPIVersion ver, const char* cmd,
const char* arg0, const char* arg1, const char* arg2, const char* pipename) {
log_debug(attach)("AttachListener::enqueue, ver = %d, cmd = %s", (int)ver, cmd);
// wait up to 10 seconds for listener to be up and running
int sleep_count = 0;
while (!AttachListener::is_initialized()) {
@ -210,7 +331,7 @@ int Win32AttachListener::enqueue(char* cmd, char* arg0, char* arg1, char* arg2,
}
// try to get an operation from the available list
Win32AttachOperation* op = available();
Win32AttachOperationRequest* op = available();
if (op != nullptr) {
set_available(op->next());
@ -223,11 +344,7 @@ int Win32AttachListener::enqueue(char* cmd, char* arg0, char* arg1, char* arg2,
}
set_tail(op);
op->set_name(cmd);
op->set_arg(0, arg0);
op->set_arg(1, arg1);
op->set_arg(2, arg2);
op->set_pipe(pipename);
op->set(ver, pipename, cmd, arg0, arg1, arg2);
// Increment number of enqueued operations.
// Side effect: Semaphore will be signaled and will release
@ -236,6 +353,7 @@ int Win32AttachListener::enqueue(char* cmd, char* arg0, char* arg1, char* arg2,
::ReleaseSemaphore(enqueued_ops_semaphore(), 1, nullptr);
guarantee(not_exceeding_semaphore_maximum_count, "invariant");
}
::ReleaseMutex(mutex());
return (op != nullptr) ? 0 : ATTACH_ERROR_RESOURCE;
@ -255,107 +373,63 @@ Win32AttachOperation* Win32AttachListener::dequeue() {
guarantee(res != WAIT_FAILED, "WaitForSingleObject failed with error code: %lu", GetLastError());
guarantee(res == WAIT_OBJECT_0, "WaitForSingleObject failed with return value: %lu", res);
Win32AttachOperation* op = nullptr;
Win32AttachOperationRequest* request = head();
if (request != nullptr) {
log_debug(attach)("AttachListener::dequeue, got request, ver = %d, cmd = %s", request->ver(), request->cmd());
Win32AttachOperation* op = head();
if (op != nullptr) {
set_head(op->next());
set_head(request->next());
if (head() == nullptr) { // list is empty
set_tail(nullptr);
}
switch (request->ver()) {
case ATTACH_API_V1:
op = new Win32AttachOperation();
op->set_name(request->cmd());
for (int i = 0; i < AttachOperation::arg_count_max; i++) {
op->append_arg(request->arg(i));
}
op->open_pipe(request->pipe(), true/*write-only*/);
break;
case ATTACH_API_V2:
op = new Win32AttachOperation();
op->open_pipe(request->pipe(), false/*write-only*/);
if (!op->read_request()) {
log_error(attach)("AttachListener::dequeue, reading request ERROR");
delete op;
op = nullptr;
}
break;
default:
log_error(attach)("AttachListener::dequeue, unsupported version: %d", request->ver(), request->cmd());
break;
}
}
// put the operation back on the available list
request->set_next(Win32AttachListener::available());
Win32AttachListener::set_available(request);
::ReleaseMutex(mutex());
if (op != nullptr) {
log_debug(attach)("AttachListener::dequeue, return op: %s", op->name());
return op;
}
}
}
// open the pipe to the client
HANDLE Win32AttachOperation::open_pipe() {
HANDLE hPipe = ::CreateFile( pipe(), // pipe name
GENERIC_WRITE, // write only
0, // no sharing
nullptr, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
nullptr); // no template file
return hPipe;
}
// write to the pipe
BOOL Win32AttachOperation::write_pipe(HANDLE hPipe, char* buf, int len) {
do {
DWORD nwrote;
BOOL fSuccess = WriteFile( hPipe, // pipe handle
(LPCVOID)buf, // message
(DWORD)len, // message length
&nwrote, // bytes written
nullptr); // not overlapped
if (!fSuccess) {
return fSuccess;
}
buf += nwrote;
len -= nwrote;
} while (len > 0);
return TRUE;
}
// Complete the operation:
// - open the pipe to the client
// - write the operation result (a jint)
// - write the operation output (the result stream)
//
void Win32AttachOperation::complete(jint result, bufferedStream* result_stream) {
JavaThread* thread = JavaThread::current();
ThreadBlockInVM tbivm(thread);
HANDLE hPipe = open_pipe();
int lastError = (int)::GetLastError();
if (hPipe != INVALID_HANDLE_VALUE) {
BOOL fSuccess;
write_reply(&_pipe, result, result_stream);
char msg[32];
os::snprintf(msg, sizeof(msg), "%d\n", result);
msg[sizeof(msg) - 1] = '\0';
fSuccess = write_pipe(hPipe, msg, (int)strlen(msg));
if (fSuccess) {
fSuccess = write_pipe(hPipe, (char*)result_stream->base(), (int)(result_stream->size()));
}
lastError = (int)::GetLastError();
// Need to flush buffers
FlushFileBuffers(hPipe);
CloseHandle(hPipe);
if (fSuccess) {
log_debug(attach)("wrote result of attach operation %s to pipe %s", name(), pipe());
} else {
log_error(attach)("failure (%d) writing result of operation %s to pipe %s", lastError, name(), pipe());
}
} else {
log_error(attach)("could not open (%d) pipe %s to send result of operation %s", lastError, pipe(), name());
}
DWORD res = ::WaitForSingleObject(Win32AttachListener::mutex(), INFINITE);
assert(res != WAIT_FAILED, "WaitForSingleObject failed with error code: %lu", GetLastError());
assert(res == WAIT_OBJECT_0, "WaitForSingleObject failed with return value: %lu", res);
if (res == WAIT_OBJECT_0) {
// put the operation back on the available list
set_next(Win32AttachListener::available());
Win32AttachListener::set_available(this);
::ReleaseMutex(Win32AttachListener::mutex());
}
delete this;
}
// AttachOperation functions
// AttachListener functions
AttachOperation* AttachListener::dequeue() {
JavaThread* thread = JavaThread::current();
@ -404,8 +478,12 @@ void AttachListener::pd_detachall() {
// Native thread started by remote client executes this.
extern "C" {
JNIEXPORT jint JNICALL
JVM_EnqueueOperation(char* cmd, char* arg0, char* arg1, char* arg2, char* pipename) {
return (jint)Win32AttachListener::enqueue(cmd, arg0, arg1, arg2, pipename);
}
JVM_EnqueueOperation(char* cmd, char* arg0, char* arg1, char* arg2, char* pipename) {
return (jint)Win32AttachListener::enqueue(ATTACH_API_V1, cmd, arg0, arg1, arg2, pipename);
}
JNIEXPORT jint JNICALL
JVM_EnqueueOperation_v2(char* pipename) {
return (jint)Win32AttachListener::enqueue(ATTACH_API_V2, "", "", "", "", pipename);
}
} // extern

View File

@ -50,6 +50,38 @@
volatile AttachListenerState AttachListener::_state = AL_NOT_INITIALIZED;
AttachAPIVersion AttachListener::_supported_version = ATTACH_API_V1;
static bool get_bool_sys_prop(const char* name, bool default_value, TRAPS) {
ResourceMark rm(THREAD);
HandleMark hm(THREAD);
// setup the arguments to getProperty
Handle key_str = java_lang_String::create_from_str(name, CHECK_(default_value));
// return value
JavaValue result(T_OBJECT);
// public static String getProperty(String key, String def);
JavaCalls::call_static(&result,
vmClasses::System_klass(),
vmSymbols::getProperty_name(),
vmSymbols::string_string_signature(),
key_str,
CHECK_(default_value));
oop value_oop = result.get_oop();
if (value_oop != nullptr) {
// convert Java String to utf8 string
char* value = java_lang_String::as_utf8_string(value_oop);
if (strcasecmp(value, "true") == 0) {
return true;
}
if (strcasecmp(value, "false") == 0) {
return false;
}
}
return default_value;
}
// Implementation of "properties" command.
//
// Invokes VMSupport.serializePropertiesToByteArray to serialize
@ -351,6 +383,12 @@ static jint print_flag(AttachOperation* op, outputStream* out) {
return JNI_OK;
}
// Implementation of "getversion" command
static jint get_version(AttachOperation* op, outputStream* out) {
out->print("%d", (int)AttachListener::get_supported_version());
return JNI_OK;
}
// Table to map operation names to functions.
// names must be of length <= AttachOperation::name_length_max
@ -365,6 +403,7 @@ static AttachOperationFunctionInfo funcs[] = {
{ "setflag", set_flag },
{ "printflag", print_flag },
{ "jcmd", jcmd },
{ "getversion", get_version },
{ nullptr, nullptr }
};
@ -472,3 +511,175 @@ void AttachListener::detachall() {
// call the platform dependent clean-up
pd_detachall();
}
void AttachListener::set_supported_version(AttachAPIVersion version) {
// _supported_version = version;
const char* prop_name = "jdk.attach.compat";
if (!get_bool_sys_prop(prop_name, false, JavaThread::current())) {
_supported_version = version;
}
}
AttachAPIVersion AttachListener::get_supported_version() {
return _supported_version;
}
int AttachOperation::RequestReader::read_uint() {
const int MAX_VALUE = INT_MAX / 20;
char ch;
int value = 0;
while (true) {
int n = read(&ch, 1);
if (n != 1) {
// IO errors (n < 0) are logged by read().
if (n == 0) { // EOF
log_error(attach)("Failed to read int value: EOF");
}
return -1;
}
if (ch == '\0') {
return value;
}
if (ch < '0' || ch > '9') {
log_error(attach)("Failed to read int value: unexpected symbol: %c", ch);
return -1;
}
// Ensure there is no integer overflow.
if (value >= MAX_VALUE) {
log_error(attach)("Failed to read int value: too big");
return -1;
}
value = value * 10 + (ch - '0');
}
}
// Reads operation name and arguments.
// buffer_size: maximum data size;
// min_str_count: minimum number of strings in the request (name + arguments);
// min_read_size: minimum data size.
bool AttachOperation::read_request_data(AttachOperation::RequestReader* reader,
int buffer_size, int min_str_count, int min_read_size) {
char* buffer = (char*)os::malloc(buffer_size, mtServiceability);
int str_count = 0;
int off = 0;
int left = buffer_size;
// Read until all (expected) strings or expected bytes have been read, the buffer is full, or EOF.
do {
int n = reader->read(buffer + off, left);
if (n < 0) {
os::free(buffer);
return false;
}
if (n == 0) { // EOF
break;
}
if (min_str_count > 0) { // need to count arguments
for (int i = 0; i < n; i++) {
if (buffer[off + i] == '\0') {
str_count++;
}
}
}
off += n;
left -= n;
} while (left > 0 && (off < min_read_size || str_count < min_str_count));
if (off < min_read_size || str_count < min_str_count) { // unexpected EOF
log_error(attach)("Failed to read request: incomplete request");
os::free(buffer);
return false;
}
// Request must ends with '\0'.
if (buffer[off - 1] != '\0') {
log_error(attach)("Failed to read request: not terminated");
os::free(buffer);
return false;
}
// Parse request.
// Command name is the 1st string.
set_name(buffer);
log_debug(attach)("read request: cmd = %s", buffer);
// Arguments.
char* end = buffer + off;
for (char* cur = strchr(buffer, '\0') + 1; cur < end; cur = strchr(cur, '\0') + 1) {
log_debug(attach)("read request: arg = %s", cur);
append_arg(cur);
}
os::free(buffer);
return true;
}
bool AttachOperation::read_request(RequestReader* reader) {
uint ver = reader->read_uint();
int buffer_size = 0;
// Read conditions:
int min_str_count = 0; // expected number of strings in the request
int min_read_size = 1; // expected size of the request data (by default 1 symbol for terminating '\0')
switch (ver) {
case ATTACH_API_V1: // <ver>0<cmd>0<arg>0<arg>0<arg>0
// Always contain a command (up to name_length_max chars)
// and arg_count_max(3) arguments (each up to arg_length_max chars).
buffer_size = (name_length_max + 1) + arg_count_max * (arg_length_max + 1);
min_str_count = 1 /*name*/ + arg_count_max;
break;
case ATTACH_API_V2: // <ver>0<size>0<cmd>0<arg>0<arg>0<arg>0
if (AttachListener::get_supported_version() < 2) {
log_error(attach)("Failed to read request: v2 is unsupported ot disabled");
return false;
}
// read size of the data
buffer_size = reader->read_uint();
if (buffer_size < 0) {
return false;
}
log_debug(attach)("v2 request, data size = %d", buffer_size);
// Sanity check: max request size is 256K.
if (buffer_size > 256 * 1024) {
log_error(attach)("Failed to read request: too big");
return false;
}
// Must contain exact 'buffer_size' bytes.
min_read_size = buffer_size;
break;
default:
log_error(attach)("Failed to read request: unknown version (%d)", ver);
return false;
}
return read_request_data(reader, buffer_size, min_str_count, min_read_size);
}
bool AttachOperation::ReplyWriter::write_fully(const void* buffer, int size) {
const char* buf = (const char*)buffer;
do {
int n = write(buf, size);
if (n < 0) {
return false;
}
buf += n;
size -= n;
} while (size > 0);
return true;
}
bool AttachOperation::write_reply(ReplyWriter* writer, jint result, bufferedStream* result_stream) {
char msg[32];
os::snprintf_checked(msg, sizeof(msg), "%d\n", result);
if (!writer->write_fully(msg, (int)strlen(msg))) {
return false;
}
if (!writer->write_fully(result_stream->base(), (int)result_stream->size())) {
return false;
}
writer->flush();
return true;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2024, 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
@ -32,6 +32,7 @@
#include "utilities/debug.hpp"
#include "utilities/exceptions.hpp"
#include "utilities/globalDefinitions.hpp"
#include "utilities/growableArray.hpp"
#include "utilities/macros.hpp"
#include "utilities/ostream.hpp"
@ -59,6 +60,20 @@ enum AttachListenerState {
AL_INITIALIZED
};
/*
Version 1 (since jdk6): attach operations always have 3 (AttachOperation::arg_count_max)
arguments, each up to 1024 (AttachOperation::arg_length_max) chars.
Version 2 (since jdk24): attach operations may have any number of arguments of any length;
for safety default implementation restricts attach operation request size by 256KB.
To detect if target VM supports version 2, client sends "getversion" command.
Old VM reports "Operation not recognized" error, newer VM reports version supported by the implementation.
If the target VM does not support version 2, client uses version 1 to enqueue operations.
*/
enum AttachAPIVersion: int {
ATTACH_API_V1 = 1,
ATTACH_API_V2 = 2
};
class AttachListenerThread : public JavaThread {
private:
static void thread_entry(JavaThread* thread, TRAPS);
@ -93,7 +108,12 @@ class AttachListener: AllStatic {
private:
static volatile AttachListenerState _state;
static AttachAPIVersion _supported_version;
public:
static void set_supported_version(AttachAPIVersion version);
static AttachAPIVersion get_supported_version();
static void set_state(AttachListenerState new_state) {
Atomic::store(&_state, new_state);
}
@ -136,8 +156,9 @@ class AttachListener: AllStatic {
};
#if INCLUDE_SERVICES
class AttachOperation: public CHeapObj<mtInternal> {
public:
class AttachOperation: public CHeapObj<mtServiceability> {
public:
// v1 constants
enum {
name_length_max = 16, // maximum length of name
arg_length_max = 1024, // maximum length of argument
@ -148,51 +169,100 @@ class AttachOperation: public CHeapObj<mtInternal> {
// clients detach
static char* detachall_operation_name() { return (char*)"detachall"; }
private:
char _name[name_length_max+1];
char _arg[arg_count_max][arg_length_max+1];
private:
char* _name;
GrowableArrayCHeap<char*, mtServiceability> _args;
public:
const char* name() const { return _name; }
static char* copy_str(const char* value) {
return value == nullptr ? nullptr : os::strdup(value, mtServiceability);
}
public:
const char* name() const { return _name; }
// set the operation name
void set_name(const char* name) {
assert(strlen(name) <= name_length_max, "exceeds maximum name length");
size_t len = MIN2(strlen(name), (size_t)name_length_max);
memcpy(_name, name, len);
_name[len] = '\0';
os::free(_name);
_name = copy_str(name);
}
int arg_count() const {
return _args.length();
}
// get an argument value
const char* arg(int i) const {
assert(i>=0 && i<arg_count_max, "invalid argument index");
return _arg[i];
// Historically clients expect empty string for absent or null arguments.
if (i >= _args.length() || _args.at(i) == nullptr) {
static char empty_str[] = "";
return empty_str;
}
return _args.at(i);
}
// appends an argument
void append_arg(const char* arg) {
_args.append(copy_str(arg));
}
// set an argument value
void set_arg(int i, char* arg) {
assert(i>=0 && i<arg_count_max, "invalid argument index");
if (arg == nullptr) {
_arg[i][0] = '\0';
} else {
assert(strlen(arg) <= arg_length_max, "exceeds maximum argument length");
size_t len = MIN2(strlen(arg), (size_t)arg_length_max);
memcpy(_arg[i], arg, len);
_arg[i][len] = '\0';
void set_arg(int i, const char* arg) {
_args.at_put_grow(i, copy_str(arg), nullptr);
}
// create an v1 operation of a given name (for compatibility, deprecated)
AttachOperation(const char* name) : _name(nullptr) {
set_name(name);
for (int i = 0; i < arg_count_max; i++) {
set_arg(i, nullptr);
}
}
// create an operation of a given name
AttachOperation(const char* name) {
set_name(name);
for (int i=0; i<arg_count_max; i++) {
set_arg(i, nullptr);
AttachOperation() : _name(nullptr) {
}
virtual ~AttachOperation() {
os::free(_name);
for (GrowableArrayIterator<char*> it = _args.begin(); it != _args.end(); ++it) {
os::free(*it);
}
}
// complete operation by sending result code and any result data to the client
virtual void complete(jint result, bufferedStream* result_stream) = 0;
// Helper classes/methods for platform-specific implementations.
class RequestReader {
public:
// Returns number of bytes read,
// 0 on EOF, negative value on error.
virtual int read(void* buffer, int size) = 0;
// Reads unsigned value, returns -1 on error.
int read_uint();
};
// Reads standard operation request (v1 or v2).
bool read_request(RequestReader* reader);
class ReplyWriter {
public:
// Returns number of bytes written, negative value on error.
virtual int write(const void* buffer, int size) = 0;
virtual void flush() {}
bool write_fully(const void* buffer, int size);
};
// Writes standard operation reply (to be called from 'complete' method).
bool write_reply(ReplyWriter* writer, jint result, bufferedStream* result_stream);
private:
bool read_request_data(AttachOperation::RequestReader* reader, int buffer_size, int min_str_count, int min_read_size);
};
#endif // INCLUDE_SERVICES
#endif // SHARE_SERVICES_ATTACHLISTENER_HPP

View File

@ -42,6 +42,8 @@ import java.security.PrivilegedAction;
import java.util.Properties;
import java.util.stream.Collectors;
import java.nio.charset.StandardCharsets;
/*
* The HotSpot implementation of com.sun.tools.attach.VirtualMachine.
*/
@ -102,7 +104,7 @@ public abstract class HotSpotVirtualMachine extends VirtualMachine {
agentLibrary,
isAbsolute ? "true" : "false",
options);
String result = readErrorMessage(in);
String result = readMessage(in);
if (result.isEmpty()) {
throw new AgentLoadException("Target VM did not respond");
} else if (result.startsWith(msgPrefix)) {
@ -327,6 +329,45 @@ public abstract class HotSpotVirtualMachine extends VirtualMachine {
}
}
// Attach API version support
protected static final int VERSION_1 = 1;
protected static final int VERSION_2 = 2;
/*
* Detects Attach API version supported by target VM.
*/
protected int detectVersion() throws IOException {
try {
InputStream reply = execute("getversion");
String message = readMessage(reply);
reply.close();
try {
int supportedVersion = Integer.parseUnsignedInt(message);
// we expect only VERSION_2
if (supportedVersion == VERSION_2) {
return VERSION_2;
}
} catch (NumberFormatException nfe) {
// bad reply - fallback to VERSION_1
}
} catch (AttachOperationFailedException | AgentLoadException ex) {
// the command is not supported, the VM supports VERSION_1 only
}
return VERSION_1;
}
/*
* For testing purposes Attach API v2 may be disabled.
*/
protected boolean isAPIv2Enabled() {
// if "jdk.attach.compat" property is set, only v1 is enabled.
try {
String value = System.getProperty("jdk.attach.compat");
return !("true".equalsIgnoreCase(value));
} catch (SecurityException se) {
}
return true;
}
/*
* Utility method to read an 'int' from the input stream. Ideally
@ -367,7 +408,7 @@ public abstract class HotSpotVirtualMachine extends VirtualMachine {
/*
* Utility method to read data into a String.
*/
String readErrorMessage(InputStream in) throws IOException {
String readMessage(InputStream in) throws IOException {
String s;
StringBuilder message = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
@ -400,7 +441,7 @@ public abstract class HotSpotVirtualMachine extends VirtualMachine {
}
if (completionStatus != 0) {
// read from the stream and use that as the error message
String message = readErrorMessage(sis);
String message = readMessage(sis);
sis.close();
// In the event of a protocol mismatch then the target VM
@ -417,6 +458,51 @@ public abstract class HotSpotVirtualMachine extends VirtualMachine {
}
}
/*
* Helper writer interface to send commands to the target VM.
*/
public static interface AttachOutputStream {
abstract void write(byte[] buffer, int offset, int length) throws IOException;
}
private int dataSize(Object obj) {
return (obj == null ? 0 : obj.toString().getBytes(StandardCharsets.UTF_8).length) + 1;
}
/*
* Writes object (usually String or Integer) to the attach writer.
*/
private void writeString(AttachOutputStream writer, Object obj) throws IOException {
if (obj != null) {
String s = obj.toString();
if (s.length() > 0) {
byte[] b = s.getBytes(StandardCharsets.UTF_8);
writer.write(b, 0, b.length);
}
}
byte b[] = new byte[1];
b[0] = 0;
writer.write(b, 0, 1);
}
protected void writeCommand(AttachOutputStream writer, int ver, String cmd, Object ... args) throws IOException {
writeString(writer, ver);
if (ver == VERSION_2) {
// for v2 write size of the data
int size = dataSize(cmd);
for (Object arg: args) {
size += dataSize(arg);
}
writeString(writer, size);
}
writeString(writer, cmd);
// v1 commands always write 3 arguments
int argNumber = ver == VERSION_1 ? 3 : args.length;
for (int i = 0; i < argNumber; i++) {
writeString(writer, i < args.length ? args[i] : null);
}
}
/*
* InputStream for the socket connection to get target VM
*/

View File

@ -26,6 +26,7 @@ package sun.tools.attach;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.AttachOperationFailedException;
import com.sun.tools.attach.spi.AttachProvider;
import java.io.IOException;
@ -42,6 +43,7 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
private static byte[] stub;
private volatile long hProcess; // handle to the process
private int ver = VERSION_1; // updated in ctor depending on detectVersion result
VirtualMachineImpl(AttachProvider provider, String id)
throws AttachNotSupportedException, IOException
@ -51,11 +53,15 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
int pid = Integer.parseInt(id);
hProcess = openProcess(pid);
// The target VM might be a pre-6.0 VM so we enqueue a "null" command
// which minimally tests that the enqueue function exists in the target
// VM.
try {
enqueue(hProcess, stub, null, null);
if (isAPIv2Enabled()) {
ver = detectVersion();
} else {
// The target VM might be a pre-6.0 VM so we enqueue a "null" command
// which minimally tests that the enqueue function exists in the target
// VM.
enqueue(hProcess, stub, VERSION_1, null, null);
}
} catch (IOException x) {
throw new AttachNotSupportedException(x.getMessage());
}
@ -73,7 +79,6 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
InputStream execute(String cmd, Object ... args)
throws AgentLoadException, IOException
{
assert args.length <= 3; // includes null
checkNulls(args);
// create a pipe using a random name
@ -83,12 +88,12 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
String pipename = pipeprefix + r;
long hPipe;
try {
hPipe = createPipe(pipename);
hPipe = createPipe(ver, pipename);
} catch (IOException ce) {
// Retry with another random pipe name.
r = rnd.nextInt();
pipename = pipeprefix + r;
hPipe = createPipe(pipename);
hPipe = createPipe(ver, pipename);
}
// check if we are detached - in theory it's possible that detach is invoked
@ -99,18 +104,34 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
}
try {
// enqueue the command to the process
enqueue(hProcess, stub, cmd, pipename, args);
// enqueue the command to the process.
if (ver == VERSION_1) {
enqueue(hProcess, stub, ver, cmd, pipename, args);
} else {
// for v2 operations request contains only pipe name.
enqueue(hProcess, stub, ver, null, pipename);
}
// wait for command to complete - process will connect with the
// completion status
// wait for the target VM to connect to the pipe.
connectPipe(hPipe);
IOException ioe = null;
if (ver == VERSION_2) {
PipeOutputStream writer = new PipeOutputStream(hPipe);
try {
writeCommand(writer, ver, cmd, args);
} catch (IOException x) {
ioe = x;
}
}
// create an input stream for the pipe
SocketInputStreamImpl in = new SocketInputStreamImpl(hPipe);
// Process the command completion status
processCompletionStatus(null, cmd, in);
processCompletionStatus(ioe, cmd, in);
// return the input stream
return in;
@ -121,6 +142,17 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
}
}
private static class PipeOutputStream implements AttachOutputStream {
private long hPipe;
public PipeOutputStream(long hPipe) {
this.hPipe = hPipe;
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
VirtualMachineImpl.writePipe(hPipe, buffer, offset, length);
}
}
// An InputStream based on a pipe to the target VM
private static class SocketInputStreamImpl extends SocketInputStream {
public SocketInputStreamImpl(long fd) {
@ -149,7 +181,7 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
static native void closeProcess(long hProcess) throws IOException;
static native long createPipe(String name) throws IOException;
static native long createPipe(int ver, String name) throws IOException;
static native void closePipe(long hPipe) throws IOException;
@ -157,7 +189,9 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
static native int readPipe(long hPipe, byte buf[], int off, int buflen) throws IOException;
static native void enqueue(long hProcess, byte[] stub,
static native void writePipe(long hPipe, byte buf[], int off, int buflen) throws IOException;
static native void enqueue(long hProcess, byte[] stub, int ver,
String cmd, String pipename, Object ... args) throws IOException;
static {

View File

@ -46,9 +46,11 @@ static IsWow64ProcessFunc _IsWow64Process;
typedef BOOL (WINAPI *EnumProcessModulesFunc) (HANDLE, HMODULE *, DWORD, LPDWORD );
typedef DWORD (WINAPI *GetModuleFileNameExFunc) ( HANDLE, HMODULE, LPTSTR, DWORD );
/* exported function in target VM */
/* exported functions in target VM */
typedef jint (WINAPI* EnqueueOperationFunc)
(const char* cmd, const char* arg1, const char* arg2, const char* arg3, const char* pipename);
typedef jint (WINAPI* EnqueueOperationFunc_v2)
(const char* pipename);
/* OpenProcess with SE_DEBUG_NAME privilege */
static HANDLE
@ -70,11 +72,13 @@ static jboolean jstring_to_cstring(JNIEnv* env, jstring jstr, char* cstr, size_t
#define MAX_PIPE_NAME_LENGTH 256
typedef struct {
jint version;
GetModuleHandleFunc _GetModuleHandle;
GetProcAddressFunc _GetProcAddress;
char jvmLib[MAX_LIBNAME_LENGTH]; /* "jvm.dll" */
char func1[MAX_FUNC_LENGTH];
char func2[MAX_FUNC_LENGTH];
char func_v2[MAX_FUNC_LENGTH];
char cmd[MAX_CMD_LENGTH + 1]; /* "load", "dump", ... */
char arg[MAX_ARGS][MAX_ARG_LENGTH + 1]; /* arguments to command */
char pipename[MAX_PIPE_NAME_LENGTH + 1];
@ -102,27 +106,36 @@ DEF_STATIC_JNI_OnLoad
DWORD WINAPI jvm_attach_thread_func(DataBlock *pData)
{
HINSTANCE h;
EnqueueOperationFunc addr;
h = pData->_GetModuleHandle(pData->jvmLib);
if (h == NULL) {
return ERR_OPEN_JVM_FAIL;
}
addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func1));
if (addr == NULL) {
addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func2));
}
if (addr == NULL) {
if (pData->version == 1) {
EnqueueOperationFunc addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func1));
if (addr == NULL) {
addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func2));
}
if (addr == NULL) {
return ERR_GET_ENQUEUE_FUNC_FAIL;
}
/* "null" command - does nothing in the target VM */
if (pData->cmd[0] == '\0') {
return 0;
} else {
return (*addr)(pData->cmd, pData->arg[0], pData->arg[1], pData->arg[2], pData->pipename);
}
} else if (pData->version == 2) {
EnqueueOperationFunc_v2 addr = (EnqueueOperationFunc_v2)(pData->_GetProcAddress(h, pData->func_v2));
if (addr == NULL) {
return ERR_GET_ENQUEUE_FUNC_FAIL;
}
return (*addr)(pData->pipename);
} else {
return ERR_GET_ENQUEUE_FUNC_FAIL;
}
/* "null" command - does nothing in the target VM */
if (pData->cmd[0] == '\0') {
return 0;
} else {
return (*addr)(pData->cmd, pData->arg[0], pData->arg[1], pData->arg[2], pData->pipename);
}
}
/* This function marks the end of jvm_attach_thread_func. */
@ -261,7 +274,7 @@ JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_closeProcess
* Signature: (Ljava/lang/String;)J
*/
JNIEXPORT jlong JNICALL Java_sun_tools_attach_VirtualMachineImpl_createPipe
(JNIEnv *env, jclass cls, jstring pipename)
(JNIEnv *env, jclass cls, jint ver, jstring pipename)
{
HANDLE hPipe;
char name[MAX_PIPE_NAME_LENGTH];
@ -289,7 +302,8 @@ JNIEXPORT jlong JNICALL Java_sun_tools_attach_VirtualMachineImpl_createPipe
hPipe = CreateNamedPipe(
name, // pipe name
PIPE_ACCESS_INBOUND, // read access
ver == 1 ? PIPE_ACCESS_INBOUND // read access
: PIPE_ACCESS_DUPLEX, // read-write access
PIPE_TYPE_BYTE | // byte mode
PIPE_READMODE_BYTE |
PIPE_WAIT, // blocking mode
@ -377,14 +391,46 @@ JNIEXPORT jint JNICALL Java_sun_tools_attach_VirtualMachineImpl_readPipe
return (jint)nread;
}
/*
* Class: sun_tools_attach_VirtualMachineImpl
* Method: writePipe
* Signature: (J[BII)V
*/
JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_writePipe
(JNIEnv *env, jclass cls, jlong hPipe, jbyteArray buffer, jint offset, jint length)
{
jsize remaining = length;
do {
jbyte buf[128];
jsize len = sizeof(buf);
DWORD written;
if (len > remaining) {
len = remaining;
}
(*env)->GetByteArrayRegion(env, buffer, offset, len, buf);
BOOL fSuccess = WriteFile((HANDLE)hPipe, buf, len, &written, NULL);
if (!fSuccess) {
JNU_ThrowIOExceptionWithLastError(env, "WriteFile");
return;
}
offset += written;
remaining -= written;
} while (remaining > 0);
}
/*
* Class: sun_tools_attach_VirtualMachineImpl
* Method: enqueue
* Signature: (JZLjava/lang/String;[Ljava/lang/Object;)V
*/
JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_enqueue
(JNIEnv *env, jclass cls, jlong handle, jbyteArray stub, jstring cmd,
jstring pipename, jobjectArray args)
(JNIEnv *env, jclass cls, jlong handle, jbyteArray stub, jint ver,
jstring cmd, jstring pipename, jobjectArray args)
{
DataBlock data;
DataBlock* pData;
@ -399,12 +445,15 @@ JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_enqueue
* Setup data to copy to target process
*/
memset(&data, 0, sizeof(data));
data.version = ver;
data._GetModuleHandle = _GetModuleHandle;
data._GetProcAddress = _GetProcAddress;
strcpy(data.jvmLib, "jvm");
strcpy(data.func1, "JVM_EnqueueOperation");
strcpy(data.func2, "_JVM_EnqueueOperation@20");
strcpy(data.func_v2, "JVM_EnqueueOperation_v2");
/*
* Command and arguments

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) 2024, 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.
*
* 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.
*/
/*
* @test
* @summary Basic compatibility test for Attach API v2
* @bug 8219896
* @library /test/lib
* @modules jdk.attach/sun.tools.attach
*
* @run main/othervm -Xlog:attach=trace CompatTest
* @run main/othervm -Xlog:attach=trace -Djdk.attach.compat=true CompatTest
*/
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import com.sun.tools.attach.VirtualMachine;
import sun.tools.attach.HotSpotVirtualMachine;
import jdk.test.lib.apps.LingeredApp;
public class CompatTest {
public static void main(String[] args) throws Exception {
// if the test (client part) in the "compat" mode
boolean clientCompat = "true".equals(System.getProperty("jdk.attach.compat"));
System.out.println("Client is in compat mode: " + clientCompat);
LingeredApp app = null;
try {
app = LingeredApp.startApp("-Xlog:attach=trace");
test(app, clientCompat);
} finally {
LingeredApp.stopApp(app);
}
try {
app = LingeredApp.startApp("-Xlog:attach=trace", "-Djdk.attach.compat=true");
// target VM in "compat" mode, always expect failure
test(app, true);
} finally {
LingeredApp.stopApp(app);
}
}
// The test uses HotSpotVirtualMachine.setFlag method with long flag value.
// For attach API v1 an exception is expected to be thrown (argument cannot be longer than 1024 characters).
private static String flagName = "HeapDumpPath";
// long for v1
private static String flagValue = "X" + "A".repeat(1024) + "X";
private static void test(LingeredApp app, boolean expectFailure) throws Exception {
System.out.println("======== Start ========");
HotSpotVirtualMachine vm = (HotSpotVirtualMachine)VirtualMachine.attach(String.valueOf(app.getPid()));
BufferedReader replyReader = null;
try {
replyReader = new BufferedReader(new InputStreamReader(
vm.setFlag(flagName, flagValue)));
if (expectFailure) {
throw new RuntimeException("No expected exception is thrown");
}
String line;
while ((line = replyReader.readLine()) != null) {
System.out.println("setFlag reply: " + line);
}
replyReader.close();
} catch (IOException ex) {
System.out.println("OK: setFlag thrown expected exception:");
ex.printStackTrace(System.out);
} finally {
vm.detach();
}
System.out.println("======== End ========");
System.out.println();
}
}