diff --git a/src/hotspot/os/windows/attachListener_windows.cpp b/src/hotspot/os/windows/attachListener_windows.cpp index 3f6ca941c209b..bfa377d52cf2f 100644 --- a/src/hotspot/os/windows/attachListener_windows.cpp +++ b/src/hotspot/os/windows/attachListener_windows.cpp @@ -32,17 +32,24 @@ #include // SIGBREAK #include -// 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, ""); + 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("") { - set_pipe(""); - 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; iset_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; - - 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); + write_reply(&_pipe, result, result_stream); - 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 diff --git a/src/hotspot/share/services/attachListener.cpp b/src/hotspot/share/services/attachListener.cpp index 36931531a4e02..90f5930cce0a9 100644 --- a/src/hotspot/share/services/attachListener.cpp +++ b/src/hotspot/share/services/attachListener.cpp @@ -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: // 00000 + // 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: // 000000 + 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; +} + diff --git a/src/hotspot/share/services/attachListener.hpp b/src/hotspot/share/services/attachListener.hpp index c49f996cdb438..093fa410d123e 100644 --- a/src/hotspot/share/services/attachListener.hpp +++ b/src/hotspot/share/services/attachListener.hpp @@ -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 { - public: +class AttachOperation: public CHeapObj { +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 { // 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 _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= _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 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 diff --git a/src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java b/src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java index 93656cdf51381..10d23f335354e 100644 --- a/src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java +++ b/src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java @@ -42,6 +42,8 @@ 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 @@ private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String op 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 InputStream executeCommand(String cmd, Object ... args) throws IOExceptio } } + // 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 @@ int readInt(InputStream in) throws IOException { /* * 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 @@ void processCompletionStatus(IOException ioe, String cmd, InputStream sis) throw } 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 @@ void processCompletionStatus(IOException ioe, String cmd, InputStream sis) throw } } + /* + * 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 */ diff --git a/src/jdk.attach/windows/classes/sun/tools/attach/VirtualMachineImpl.java b/src/jdk.attach/windows/classes/sun/tools/attach/VirtualMachineImpl.java index 9c30f5618d66e..23c15ad06e187 100644 --- a/src/jdk.attach/windows/classes/sun/tools/attach/VirtualMachineImpl.java +++ b/src/jdk.attach/windows/classes/sun/tools/attach/VirtualMachineImpl.java @@ -26,6 +26,7 @@ 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 void detach() throws IOException { 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 @@ InputStream execute(String cmd, Object ... args) 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 @@ InputStream execute(String cmd, Object ... args) } 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 @@ InputStream execute(String cmd, Object ... args) } } + 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 @@ protected void close(long fd) throws IOException { 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 @@ protected void close(long fd) throws IOException { 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 { diff --git a/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c b/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c index a8b17c22d83f2..7327ff6f61ec2 100644 --- a/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c +++ b/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c @@ -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 diff --git a/test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java b/test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java new file mode 100644 index 0000000000000..444b87bb6eaed --- /dev/null +++ b/test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java @@ -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(); + } +}