JDK-8239818 : [nestmates] problems with debugging hidden classes with jdb
  • Type: Bug
  • Component: core-svc
  • Sub-Component: debugger
  • Affected Version: repo-valhalla
  • Priority: P4
  • Status: Resolved
  • Resolution: Duplicate
  • Submitted: 2020-02-22
  • Updated: 2020-04-17
  • Resolved: 2020-04-17
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
repo-valhallaResolved
Related Reports
Duplicate :  
Relates :  
Description
There are several problems with debugging hidden classes.
These problems are in both jdb and JDI implementation, they are:
  - jdb command "classes" prints incorrect hidden class name
  - hidden class name is rejected by JDI as invalid when trying to set breakpoint, watchpoint and some other events
  - jdb commands "eval", "print" and "dump" treat hidden class names incorrectly

Comments
Closing this is a dup of the sub-task JDK-8241214.
17-04-2020

The suggestion fix is: Webrev: http://cr.openjdk.java.net/~sspitsyn/webrevs/2020/valhalla-hidden-jdb.1/ Patch: --- old/src/jdk.jdi/share/classes/com/sun/tools/example/debug/expr/ExpressionParser.java 2020-03-15 08:25:43.245406173 +0000 +++ new/src/jdk.jdi/share/classes/com/sun/tools/example/debug/expr/ExpressionParser.java 2020-03-15 08:25:42.604392527 +0000 @@ -192,13 +192,17 @@ label_2: while (true) { if (jj_2_1(2)) { - ; + jj_consume_token(DOT); + jj_consume_token(IDENTIFIER); + sb.append('.'); sb.append(token); + } else if (jj_2_1_forHiddenClass(2)) { + // accept SLASH in a class name + jj_consume_token(SLASH); + jj_consume_token(INTEGER_LITERAL); + sb.append('/'); sb.append(token); } else { - break label_2; + break label_2; } - jj_consume_token(DOT); - jj_consume_token(IDENTIFIER); - sb.append('.'); sb.append(token); } {if (true) return sb.toString();} throw new Error("Missing return statement in function"); @@ -1108,6 +1112,19 @@ } } + private boolean jj_2_1_forHiddenClass(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { + if (jj_scan_token(SLASH)) return false; + if (jj_scan_token(INTEGER_LITERAL)) return false; + return true; + } + catch (LookaheadSuccess ls) { + return true; + } + finally { jj_save(0, xla); } + } + private boolean jj_2_1(int xla) { jj_la = xla; jj_lastpos = jj_scanpos = token; try { return !jj_3_1(); } --- old/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/PatternReferenceTypeSpec.java 2020-03-15 08:25:44.400430761 +0000 +++ new/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/PatternReferenceTypeSpec.java 2020-03-15 08:25:43.760417137 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2020, 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 @@ -101,6 +101,22 @@ } private void checkClassName(String className) throws ClassNotFoundException { + int slashIdx = className.indexOf("/"); + + // Slash is present in hidden class names only. + if (slashIdx != -1) { + // A hidden class name is ending with a slash following by a suffix. + int lastSlashIdx = className.lastIndexOf("/"); + int lastDotIdx = className.lastIndexOf("."); + + // There must be just one slash with a following suffix but no dots. + if (slashIdx != lastSlashIdx || lastDotIdx > slashIdx || slashIdx + 1 == className.length()) { + throw new ClassNotFoundException(); + } + // Replace slash with a valid character ('_') to comply with the checks below. + className = className.replace("/", "_"); + } + // Do stricter checking of class name validity on deferred // because if the name is invalid, it will // never match a future loaded class, and we'll be silent --- /dev/null 2019-10-31 20:27:13.347000000 +0000 +++ new/test/hotspot/jtreg/vmTestbase/nsk/jdb/hidden_class/hc001/hc001.java 2020-03-15 08:25:44.923441896 +0000 @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2020, 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 test for hidden classes (replacement of unsafe VM anonymous classes). + * + * @library /vmTestbase + * /test/lib + * @run driver jdk.test.lib.FileInstaller . . + * @build nsk.jdb.hidden_class.hc001.hc001 + * nsk.jdb.hidden_class.hc001.hc001a + * + * @run main/othervm PropertyResolvingWrapper nsk.jdb.hidden_class.hc001.hc001 + * -arch=${os.family}-${os.simpleArch} + * -waittime=5 + * -debugee.vmkind=java + * -transport.address=dynamic + * -jdb=${test.jdk}/bin/jdb + * -java.options="${test.vm.opts} ${test.java.opts}" + * -workdir=. + * -debugee.vmkeys="${test.vm.opts} ${test.java.opts}" + */ + +package nsk.jdb.hidden_class.hc001; + +import java.io.*; +import java.util.*; +import nsk.share.*; +import nsk.share.jdb.*; + +public class hc001 extends JdbTest { + public static final String CLASSES_DIR = System.getProperty("test.classes"); + static final String DEBUGGEE_CLASS = "nsk.jdb.hidden_class.hc001.hc001a"; + static final String HC_NAME_FIELD = DEBUGGEE_CLASS + ".hcName"; + static final String MAIN_METHOD_NAME = DEBUGGEE_CLASS + ".main"; + static final String EMPTY_METHOD_NAME = DEBUGGEE_CLASS + ".emptyMethod"; + static final String HC_METHOD_NAME = "hcMethod"; + static final String HC_FIELD_NAME = "hcField"; + + public static void main(String argv[]) { + System.exit(run(argv, System.out) + JCK_STATUS_BASE); + } + + public static int run(String argv[], PrintStream out) { + debuggeeClass = DEBUGGEE_CLASS; // needed for JdbTest.runTest + firstBreak = MAIN_METHOD_NAME; // needed for JdbTest.runTest + return new hc001().runTest(argv, out); + } + + /* Make a required cooperated setup with the debuggee: + * - set the debuggee's classesDir for loading HiddenClass.class bytecodes + * - transition the debuggee's execution to expected execution point + * (emptyMethod start) at which hidden class has been already loaded + * - get the hidden class name from the debuggee + * Return the hidden class name. + */ + private String runPrologue() { + String[] reply = null; + String command = null; + + log.println("\n### Debugger: runPrologue"); + + // uncomment this line to enable verbose output from jdb + // log.enableVerbose(true); + + // set debuggee's classesDir field for loading HiddenClass.class bytecodes + command = DEBUGGEE_CLASS + ".classesDir=\"" + CLASSES_DIR + "\""; + jdb.receiveReplyFor(JdbCommand.set + command); + + // run jdb command "stop in" + jdb.setBreakpointInMethod(EMPTY_METHOD_NAME); + log.println("\nDebugger: breakpoint is set at:\n\t" + EMPTY_METHOD_NAME); + + // run jdb command "cont" + reply = jdb.receiveReplyFor(JdbCommand.cont); + if (!jdb.isAtBreakpoint(reply, EMPTY_METHOD_NAME)) { + failure("Debugger: Missed breakpoint at:\n\t" + EMPTY_METHOD_NAME); + } + log.println("\nDebugger: breakpoint is hit at:\n\t" + EMPTY_METHOD_NAME); + + // run jdb command "eval" for hidden class field "hcName" + reply = jdb.receiveReplyFor(JdbCommand.eval + HC_NAME_FIELD); + int beg = reply[0].indexOf('"') + 1; + int end = reply[0].lastIndexOf('"'); + String hiddenClassName = reply[0].substring(beg, end); // we know the hidden class name now + log.println("\nDebugger: jdb command eval returned hidden class name:\n\t" + hiddenClassName); + + return hiddenClassName; + } + + /* Test jdb commands "classes" and "class" for hidden class. */ + private void testClassCommands(String hcName) { + String[] reply = null; + boolean found = false; + + log.println("\n### Debugger: testClassCommands"); + + // run jdb command "classes" + reply = jdb.receiveReplyFor(JdbCommand.classes); + for (int idx = 0; idx < reply.length; idx++) { + if (reply[idx].indexOf("HiddenClass") != -1) { + log.println("\nDebugger: found matched class in classes reply:\n\t" + reply[idx]); + found = true; + } + } + if (!found) { + failure("Debugger: expected jdb command classes to list hidden class: HiddenClass/0x.."); + } + + // run jdb command "class" for hidden class + reply = jdb.receiveReplyFor(JdbCommand._class + hcName); + if (reply[0].indexOf(hcName) == -1) { + failure("Debugger: expected hiddenclass name in jdb comman class reply: " + hcName); + } + } + + /* Transition the debuggee's execution to the hidden class method start. */ + private void stopInHiddenClassMethod(String hcName) { + final String hcMethodName = hcName + "." + HC_METHOD_NAME; + String[] reply = null; + + log.println("\n### Debugger: stopInHiddenClassMethod"); + + // set a breakpoint in hidden class method hcMethodName() + jdb.setBreakpointInMethod(hcMethodName); + log.println("\nDebugger: breakpoint is set at:\n\t" + hcMethodName); + + // run jdb command "clear": should list breakpoint in hcMethodName + reply = jdb.receiveReplyFor(JdbCommand.clear); + if (reply[reply.length - 2].indexOf(hcMethodName) == -1) { + failure("Debugger: expected jdb clear command to list breakpoint: " + hcMethodName); + } + log.println("\nDebugger: jdb command clear lists breakpoint at:\n\t" + hcMethodName); + + // run jdb command "cont" + jdb.receiveReplyFor(JdbCommand.cont); + log.println("\nDebugger: executed jdb command cont"); + } + + /* Test the jdb commands "up" and "where" for hidden class. */ + private void testUpWhereCommands(String hcName) { + final String hcMethodName = hcName + "." + HC_METHOD_NAME; + String[] reply = null; + + log.println("\n### Debugger: testUpWhereCommands"); + + // run jdb command "where": should list hcMethodName frame + reply = jdb.receiveReplyFor(JdbCommand.where); + if (reply[0].indexOf(hcMethodName) == -1) { + failure("Debugger: jdb command where does not show expected frame: " + hcMethodName); + } + log.println("\nDebugger: jdb command where showed expected frame:\n\t" + hcMethodName); + + // run jdb command "up" + jdb.receiveReplyFor(JdbCommand.up); + log.println("\nDebugger: executed jdb command up"); + + // run jdb command "where": should not list hcMethodName frame + reply = jdb.receiveReplyFor(JdbCommand.where); + if (reply[0].indexOf(hcMethodName) != -1) { + failure("Debugger: jdb command where showed unexpected frame: " + hcMethodName); + } + log.println("\nDebugger: jdb command where does not show unexpected frame:\n\t" + hcMethodName); + } + + /* Test the jdb commands "down" and "where" for hidden class. */ + private void testDownWhereCommands(String hcName) { + final String hcMethodName = hcName + "." + HC_METHOD_NAME; + String[] reply = null; + + log.println("\n### Debugger: testDownWhereCommands"); + + // run jdb command "down" + jdb.receiveReplyFor(JdbCommand.down); + log.println("\nDebugger: executed jdb command down"); + + // run jdb command "where": should list hcMethodName frame again + reply = jdb.receiveReplyFor(JdbCommand.where); + if (reply[0].indexOf(hcMethodName) == -1) { + failure("Debugger: jdb command where does not show expected frame: " + hcMethodName); + } + log.println("\nDebugger: jdb command where showed expected frame:\n\t" + reply[0]); + } + + /* Test the jdb commands "fileds" and "methods" for hidden class. */ + private void testFieldsMethods(String hcName) { + String[] reply = null; + + log.println("\n### Debugger: testFieldsMethods"); + + // run jdb command "methods" for hidden class + reply = jdb.receiveReplyFor(JdbCommand.methods + hcName); + if (reply[1].indexOf(hcName) == -1) { + failure("Debugger: no expected hidden class name in its methods: " + reply[1]); + } + log.println("\nDebugger: jdb command \"methods\" showed expected method:\n\t" + HC_METHOD_NAME); + + // run jdb command "fields" for hidden class + reply = jdb.receiveReplyFor(JdbCommand.fields + hcName); + if (reply[1].indexOf(HC_FIELD_NAME) == -1) { + failure("Debugger: no expected hidden class field in its fields: " + HC_FIELD_NAME); + } + log.println("\nDebugger: jdb command \"fields\" showed expected field:\n\t" + reply[1]); + } + + /* Test the jdb commands "watch" and "unwatch" for hidden class. */ + private void testWatchCommands(String hcName) { + final String hcFieldName = hcName + "." + HC_FIELD_NAME; + String[] reply = null; + + log.println("\n### Debugger: testWatchCommands"); + + // run jdb command "watch" for hidden class field HC_FIELD_NAME + reply = jdb.receiveReplyFor(JdbCommand.watch + hcFieldName); + if (reply[0].indexOf(hcFieldName) == -1) { + failure("Debugger: was not able to set watch point: " + hcFieldName); + } + log.println("\nDebugger: jdb command \"watch\" added expected field to watch:\n\t" + reply[0]); + + // run jdb command "cont" + jdb.receiveReplyFor(JdbCommand.cont); + jdb.receiveReplyFor(JdbCommand.next); + + // run jdb command "unwatch" for hidden class field HC_FIELD_NAME + reply = jdb.receiveReplyFor(JdbCommand.unwatch + hcFieldName); + if (reply[0].indexOf(hcFieldName) == -1) { + failure("Debugger: expect field name in unwatch reply: " + hcFieldName); + } + log.println("\nDebugger: jdb command \"unwatch\" removed expected field from watch:\n\t" + reply[0]); + } + + /* Test the jdb commands "eval", "print" and "dump" for hidden class. */ + private void testEvalCommands(String hcName) { + final String hcFieldName = hcName + "." + HC_FIELD_NAME; + String[] reply = null; + + log.println("\n### Debugger: testEvalCommands"); + + // run jdb command "eval" for hidden class field HC_FIELD_NAME + reply = jdb.receiveReplyFor(JdbCommand.eval + hcFieldName); + if (reply[0].indexOf(hcName) == -1) { + failure("Debugger: eval of field " + hcFieldName + " is expected to have: " + hcName); + } + log.println("\nDebugger: jdb command \"eval\" showed expected hidden class name:\n\t" + reply[0]); + + // run jdb command "print" for hidden class field HC_FIELD_NAME + reply = jdb.receiveReplyFor(JdbCommand.print + hcFieldName); + if (reply[0].indexOf(hcName) == -1) { + failure("Debugger: print of field " + hcFieldName + " is expected to have: " + hcName); + } + log.println("\nDebugger: jdb command \"print\" showed expected hidden class name:\n\t" + reply[0]); + + // execute jdb command "dump" for hidden class field HC_FIELD_NAME + reply = jdb.receiveReplyFor(JdbCommand.dump + hcFieldName); + if (reply[0].indexOf(hcFieldName) == -1) { + failure("Debugger: expect field name in field dump reply: " + hcFieldName); + } + log.println("\nDebugger: jdb command \"dump\" showed expected hidden class name:\n\t" + reply[0]); + } + + /* Test the jdb command "watch" with an invalid class name. */ + private void testInvWatchCommand(String hcName) { + final String hcFieldName = hcName + "." + HC_FIELD_NAME; + final String MsgBase = "\nDebugger: jdb command \"watch\" with invalid field " + hcFieldName; + String[] reply = null; + + // run jdb command "watch" with an invalid class name + reply = jdb.receiveReplyFor(JdbCommand.watch + hcFieldName); + if (reply[0].indexOf("Deferring watch modification") != -1) { + failure(MsgBase + " must not set deferred watch point"); + } + log.println(MsgBase + " did not set deferred watch point"); + } + + /* Test the jdb command "eval" with an invalid class name. */ + private void testInvEvalCommand(String hcName) { + final String hcFieldName = hcName + "." + HC_FIELD_NAME; + final String MsgBase = "\nDebugger: jdb command \"eval\" with invalid field " + hcFieldName; + String[] reply = null; + + // run jdb command "eval" with an invalid class name + reply = jdb.receiveReplyFor(JdbCommand.eval + hcFieldName); + if (reply[0].indexOf("ParseException") == -1) { + failure(MsgBase + " must be rejected with ParseException"); + } + log.println(MsgBase + " was rejected with ParseException"); + } + + /* Test the jdb commands "watch" and "eval" with various invalid class names. */ + private void testInvalidCommands() { + final String MsgBase = "\nDebugger: jdb command \"class\" with invalid class name "; + final String[] invClassNames = { "xx.yyy/0x111/0x222", "xx./0x111.0x222", "xx.yyy.zzz/" }; + String[] reply = null; + String className = null; + + log.println("\n### Debugger: testInvalidCommands"); + + // run jdb commands "watch" and "eval" with invalid class names + for (int idx = 0; idx < invClassNames.length; idx++) { + className = invClassNames[idx]; + testInvWatchCommand(className + "." + HC_FIELD_NAME); + testInvEvalCommand(className + "." + HC_FIELD_NAME); + } + } + + /* Main testing method. */ + protected void runCases() { + String hcName = runPrologue(); + + testClassCommands(hcName); + stopInHiddenClassMethod(hcName); + + testUpWhereCommands(hcName); + testDownWhereCommands(hcName); + + testFieldsMethods(hcName); + testWatchCommands(hcName); + + testEvalCommands(hcName); + testInvalidCommands(); + + jdb.contToExit(1); + } +} --- /dev/null 2019-10-31 20:27:13.347000000 +0000 +++ new/test/hotspot/jtreg/vmTestbase/nsk/jdb/hidden_class/hc001/hc001a.java 2020-03-15 08:25:46.112467208 +0000 @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2020, 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. + */ + +package nsk.jdb.hidden_class.hc001; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.PrintStream; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; + +import nsk.share.*; +import nsk.share.jpda.*; +import nsk.share.jdb.*; + + +interface Test { + void hcMethod(); +} + +class HiddenClass implements Test { + static String hcField = null; + + private String getClassName() { + return this.getClass().getName(); + } + + public void hcMethod() { + hcField = getClassName(); + if (hcField.indexOf("HiddenClass") == -1) { + throw new RuntimeException("Debuggee: Unexpected HiddenClass name: " + hcField); + } + } +} + +/* This is debuggee aplication */ +public class hc001a { + static final String HCName = "nsk/jdb/hidden_class/hc001/HiddenClass.class"; + static String classesDir = null; // the debugger sets value of this field + static String hcName = null; // the debugger gets value of this field + + public static void main(String args[]) throws Exception { + hc001a testApp = new hc001a(); + + int status = testApp.runIt(args, System.out); + System.exit(hc001.JCK_STATUS_BASE + status); + } + + // this method is to hit breakpoint at expected execution point + void emptyMethod() {} + + public int runIt(String args[], PrintStream out) throws Exception { + JdbArgumentHandler argumentHandler = new JdbArgumentHandler(args); + Log log = new Log(out, argumentHandler); + log.display("Debuggee: runIt: started"); + + String HCFullPath = classesDir + File.separator + HCName; + Class<?> c = defineHiddenClass(HCFullPath); + hcName = c.getName(); + log.display("Debuggee: runIt: Defined HiddenClass: " + hcName); + + Test t = (Test)c.newInstance(); + log.display("Debuggee: runIt: created an instance of a hidden class: " + hcName); + + log.display("Debuggee: runIt: invoking emptyMethod to hit expected breakpoint"); + emptyMethod(); + + log.display("Debuggee: runIt: invoking a method of a hidden class: " + hcName); + t.hcMethod(); + + log.display("Debuggee: runIt: finished"); + return hc001.PASSED; + } + + static byte[] readClassFile(String classFileName) throws Exception { + File classFile = new File(classFileName); + try (FileInputStream in = new FileInputStream(classFile); + ByteArrayOutputStream out = new ByteArrayOutputStream()) + { + int b; + while ((b = in.read()) != -1) { + out.write(b); + } + return out.toByteArray(); + } + } + + static Class<?> defineHiddenClass(String classFileName) throws Exception { + Lookup lookup = MethodHandles.lookup(); + byte[] bytes = readClassFile(classFileName); + Class<?> hc = lookup.defineHiddenClass(bytes, false).lookupClass(); + return hc; + } +}
16-03-2020