From 3bd09c03a6a1db6043366b77fbda09a8751b582a Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Thu, 29 Aug 2024 16:03:50 -0700 Subject: [PATCH 1/8] attach api v2 --- .../os/windows/attachListener_windows.cpp | 380 +++++++++++------- src/hotspot/share/services/attachListener.cpp | 202 ++++++++++ src/hotspot/share/services/attachListener.hpp | 121 ++++-- .../tools/attach/HotSpotVirtualMachine.java | 92 ++++- .../sun/tools/attach/VirtualMachineImpl.java | 62 ++- .../native/libattach/VirtualMachineImpl.c | 83 +++- .../attach/AttachAPIv2/CompatTest.java | 104 +++++ 7 files changed, 832 insertions(+), 212 deletions(-) create mode 100644 test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java diff --git a/src/hotspot/os/windows/attachListener_windows.cpp b/src/hotspot/os/windows/attachListener_windows.cpp index 7e455cf0a49f2..497be7a16a669 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_EnqueueOperation2 function which checks the operation parameters +// and enqueues the operation request to the queue serviced by the attach listener. 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_EnqueueOperation2 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, + 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 && 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]; - _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_EnqueueOperation2(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..3606913aa6102 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,166 @@ 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'); + } +} + +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 symbols) + // and arg_count_max(3) arguments (each up to arg_length_max symbols). + 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; + } + + 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::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..30f679bd2245b 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 (AttachOparation::arg_count_max) + arguments, each up to 1024 (AttachOparation::arg_length_max) symbols. +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,97 @@ 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); + }; + #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..145f2b655b19a 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 184d07137e289..3dbf5d6bbc5d7 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; @@ -41,6 +42,7 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine { private static byte[] stub; private volatile long hProcess; // handle to the process + private int ver = VERSION_1; // updated by detectVersion on attach VirtualMachineImpl(AttachProvider provider, String id) throws AttachNotSupportedException, IOException @@ -50,11 +52,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()); } @@ -72,7 +78,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 @@ -82,12 +87,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 @@ -98,18 +103,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; @@ -120,6 +141,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) { @@ -148,7 +180,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; @@ -156,7 +188,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..9b60f87d25d12 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_EnqueueOperation2"); /* * 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..ce4bcde19d550 --- /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 symbols). + 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(); + } +} From 918500fff22254470124b8506609ce3d1f1e8174 Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Thu, 29 Aug 2024 16:03:50 -0700 Subject: [PATCH 2/8] attach api v2 --- .../os/windows/attachListener_windows.cpp | 380 +++++++++++------- src/hotspot/share/services/attachListener.cpp | 202 ++++++++++ src/hotspot/share/services/attachListener.hpp | 121 ++++-- .../tools/attach/HotSpotVirtualMachine.java | 92 ++++- .../sun/tools/attach/VirtualMachineImpl.java | 62 ++- .../native/libattach/VirtualMachineImpl.c | 83 +++- .../attach/AttachAPIv2/CompatTest.java | 104 +++++ 7 files changed, 832 insertions(+), 212 deletions(-) create mode 100644 test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java diff --git a/src/hotspot/os/windows/attachListener_windows.cpp b/src/hotspot/os/windows/attachListener_windows.cpp index 3f6ca941c209b..497be7a16a669 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_EnqueueOperation2 function which checks the operation parameters +// and enqueues the operation request to the queue serviced by the attach listener. 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_EnqueueOperation2 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, + 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 && 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_EnqueueOperation2(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..3606913aa6102 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,166 @@ 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'); + } +} + +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 symbols) + // and arg_count_max(3) arguments (each up to arg_length_max symbols). + 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; + } + + 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::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..30f679bd2245b 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 (AttachOparation::arg_count_max) + arguments, each up to 1024 (AttachOparation::arg_length_max) symbols. +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,97 @@ 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); + }; + #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..145f2b655b19a 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..4cc48d005c489 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 by detectVersion on attach 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..9b60f87d25d12 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_EnqueueOperation2"); /* * 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..ce4bcde19d550 --- /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 symbols). + 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(); + } +} From 2e904c58c80311626c859cb8765827e69c5b4548 Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Mon, 14 Oct 2024 13:28:51 -0700 Subject: [PATCH 3/8] feedback --- src/hotspot/share/services/attachListener.cpp | 87 ++++++++++--------- src/hotspot/share/services/attachListener.hpp | 3 + .../sun/tools/attach/VirtualMachineImpl.java | 2 +- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/hotspot/share/services/attachListener.cpp b/src/hotspot/share/services/attachListener.cpp index 3606913aa6102..b2ddee3f4e974 100644 --- a/src/hotspot/share/services/attachListener.cpp +++ b/src/hotspot/share/services/attachListener.cpp @@ -554,45 +554,12 @@ int AttachOperation::RequestReader::read_uint() { } } -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 symbols) - // and arg_count_max(3) arguments (each up to arg_length_max symbols). - 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; - } - +// 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; @@ -648,6 +615,48 @@ bool AttachOperation::read_request(RequestReader* reader) { 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 symbols) + // and arg_count_max(3) arguments (each up to arg_length_max symbols). + 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 { diff --git a/src/hotspot/share/services/attachListener.hpp b/src/hotspot/share/services/attachListener.hpp index 30f679bd2245b..4bfad688f7f61 100644 --- a/src/hotspot/share/services/attachListener.hpp +++ b/src/hotspot/share/services/attachListener.hpp @@ -258,6 +258,9 @@ class AttachOperation: public CHeapObj { // 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 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 4cc48d005c489..23c15ad06e187 100644 --- a/src/jdk.attach/windows/classes/sun/tools/attach/VirtualMachineImpl.java +++ b/src/jdk.attach/windows/classes/sun/tools/attach/VirtualMachineImpl.java @@ -43,7 +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 by detectVersion on attach + private int ver = VERSION_1; // updated in ctor depending on detectVersion result VirtualMachineImpl(AttachProvider provider, String id) throws AttachNotSupportedException, IOException From 1fed4ea627ba9d4065d6f7e6adeddb375f08c066 Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Mon, 14 Oct 2024 17:18:31 -0700 Subject: [PATCH 4/8] renamed JVM_EnqueueOperation2 renamed JVM_EnqueueOperation2 to JVM_EnqueueOperation_v2 for consistency (per Serguei request) --- src/hotspot/os/windows/attachListener_windows.cpp | 6 +++--- .../windows/native/libattach/VirtualMachineImpl.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hotspot/os/windows/attachListener_windows.cpp b/src/hotspot/os/windows/attachListener_windows.cpp index 497be7a16a669..de871aabd0c1d 100644 --- a/src/hotspot/os/windows/attachListener_windows.cpp +++ b/src/hotspot/os/windows/attachListener_windows.cpp @@ -36,7 +36,7 @@ // 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 or JVM_EnqueueOperation2 function which checks the operation parameters +// JVM_EnqueueOperation or JVM_EnqueueOperation_v2 function which checks the operation parameters // and enqueues the operation request to the queue serviced by the attach listener. The thread created by // the client is a native thread and is restricted to a single page of stack. To keep // it simple operation requests are pre-allocated at initialization time. An enqueue thus @@ -46,7 +46,7 @@ // 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_EnqueueOperation2 function and passes only pipe name. +// 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). // @@ -483,7 +483,7 @@ extern "C" { } JNIEXPORT jint JNICALL - JVM_EnqueueOperation2(char* pipename) { + JVM_EnqueueOperation_v2(char* pipename) { return (jint)Win32AttachListener::enqueue(ATTACH_API_V2, "", "", "", "", pipename); } } // extern diff --git a/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c b/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c index 9b60f87d25d12..7327ff6f61ec2 100644 --- a/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c +++ b/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c @@ -453,7 +453,7 @@ JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_enqueue strcpy(data.jvmLib, "jvm"); strcpy(data.func1, "JVM_EnqueueOperation"); strcpy(data.func2, "_JVM_EnqueueOperation@20"); - strcpy(data.func_v2, "JVM_EnqueueOperation2"); + strcpy(data.func_v2, "JVM_EnqueueOperation_v2"); /* * Command and arguments From e2480676addc802a3f323ab903880f5b2fe7a27c Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Tue, 15 Oct 2024 15:24:06 -0700 Subject: [PATCH 5/8] feedback --- src/hotspot/os/windows/attachListener_windows.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/hotspot/os/windows/attachListener_windows.cpp b/src/hotspot/os/windows/attachListener_windows.cpp index de871aabd0c1d..5e58eb47ac93b 100644 --- a/src/hotspot/os/windows/attachListener_windows.cpp +++ b/src/hotspot/os/windows/attachListener_windows.cpp @@ -37,7 +37,7 @@ // 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 or JVM_EnqueueOperation_v2 function which checks the operation parameters -// and enqueues the operation request to the queue serviced by the attach listener. The thread created by +// 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 operation requests are pre-allocated at initialization time. An enqueue thus // takes a preallocated request, populates the operation parameters, adds it to @@ -101,11 +101,11 @@ class PipeChannel : public AttachOperation::RequestReader, public AttachOperatio int read(void* buffer, int size) override { assert(opened(), "must be"); DWORD nread; - BOOL fSuccess= ReadFile(_hPipe, - buffer, - size, - &nread, - nullptr); // not overlapped + BOOL fSuccess = ReadFile(_hPipe, + buffer, + (DWORD)size, + &nread, + nullptr); // not overlapped return fSuccess ? (int)nread : -1; } @@ -191,7 +191,7 @@ class Win32AttachOperationRequest { return _name; } const char* arg(int i) const { - return (i >= 0 && AttachOperation::arg_count_max) ? _arg[i] : nullptr; + return (i >= 0 && i < AttachOperation::arg_count_max) ? _arg[i] : nullptr; } const char* pipe() const { return _pipe; From 86bd1cd2fd73354fd6c392f901c4fa9914b2da27 Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Tue, 15 Oct 2024 15:27:57 -0700 Subject: [PATCH 6/8] updated comment --- src/hotspot/os/windows/attachListener_windows.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hotspot/os/windows/attachListener_windows.cpp b/src/hotspot/os/windows/attachListener_windows.cpp index 5e58eb47ac93b..bfa377d52cf2f 100644 --- a/src/hotspot/os/windows/attachListener_windows.cpp +++ b/src/hotspot/os/windows/attachListener_windows.cpp @@ -137,7 +137,7 @@ class Win32AttachOperation: public AttachOperation { PipeChannel _pipe; public: - // for v1 pipe must be write-only. + // for v1 pipe must be write-only void open_pipe(const char* pipe_name, bool write_only) { _pipe.open(pipe_name, write_only); } From 334e735268012adaeeb70d4aa5aef840e590b793 Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Thu, 24 Oct 2024 11:18:13 -0700 Subject: [PATCH 7/8] typos --- src/hotspot/share/services/attachListener.cpp | 4 ++-- src/hotspot/share/services/attachListener.hpp | 4 ++-- .../jtreg/serviceability/attach/AttachAPIv2/CompatTest.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hotspot/share/services/attachListener.cpp b/src/hotspot/share/services/attachListener.cpp index b2ddee3f4e974..e1e021a30970d 100644 --- a/src/hotspot/share/services/attachListener.cpp +++ b/src/hotspot/share/services/attachListener.cpp @@ -623,8 +623,8 @@ bool AttachOperation::read_request(RequestReader* reader) { 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 symbols) - // and arg_count_max(3) arguments (each up to arg_length_max symbols). + // 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; diff --git a/src/hotspot/share/services/attachListener.hpp b/src/hotspot/share/services/attachListener.hpp index 4bfad688f7f61..a16d29c35ab25 100644 --- a/src/hotspot/share/services/attachListener.hpp +++ b/src/hotspot/share/services/attachListener.hpp @@ -61,8 +61,8 @@ enum AttachListenerState { }; /* -Version 1 (since jdk6): attach operations always have 3 (AttachOparation::arg_count_max) - arguments, each up to 1024 (AttachOparation::arg_length_max) symbols. +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. diff --git a/test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java b/test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java index ce4bcde19d550..444b87bb6eaed 100644 --- a/test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java +++ b/test/hotspot/jtreg/serviceability/attach/AttachAPIv2/CompatTest.java @@ -66,7 +66,7 @@ public static void main(String[] args) throws Exception { } // 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 symbols). + // 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"; From 9e6a34e0cbed236e09a4a102e7752b71de2978c9 Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Thu, 24 Oct 2024 11:50:35 -0700 Subject: [PATCH 8/8] renamed getVersion command --- src/hotspot/share/services/attachListener.cpp | 4 ++-- src/hotspot/share/services/attachListener.hpp | 2 +- .../share/classes/sun/tools/attach/HotSpotVirtualMachine.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hotspot/share/services/attachListener.cpp b/src/hotspot/share/services/attachListener.cpp index e1e021a30970d..90f5930cce0a9 100644 --- a/src/hotspot/share/services/attachListener.cpp +++ b/src/hotspot/share/services/attachListener.cpp @@ -383,7 +383,7 @@ static jint print_flag(AttachOperation* op, outputStream* out) { return JNI_OK; } -// Implementation of "getVersion" command +// Implementation of "getversion" command static jint get_version(AttachOperation* op, outputStream* out) { out->print("%d", (int)AttachListener::get_supported_version()); return JNI_OK; @@ -403,7 +403,7 @@ static AttachOperationFunctionInfo funcs[] = { { "setflag", set_flag }, { "printflag", print_flag }, { "jcmd", jcmd }, - { "getVersion", get_version }, + { "getversion", get_version }, { nullptr, nullptr } }; diff --git a/src/hotspot/share/services/attachListener.hpp b/src/hotspot/share/services/attachListener.hpp index a16d29c35ab25..093fa410d123e 100644 --- a/src/hotspot/share/services/attachListener.hpp +++ b/src/hotspot/share/services/attachListener.hpp @@ -65,7 +65,7 @@ Version 1 (since jdk6): attach operations always have 3 (AttachOperation::arg_co 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. + 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. */ 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 145f2b655b19a..10d23f335354e 100644 --- a/src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java +++ b/src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java @@ -338,7 +338,7 @@ public InputStream executeCommand(String cmd, Object ... args) throws IOExceptio */ protected int detectVersion() throws IOException { try { - InputStream reply = execute("getVersion"); + InputStream reply = execute("getversion"); String message = readMessage(reply); reply.close(); try {