1 /*
2 * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24 /*
25 * @test
26 * @summary Testing ClassFile local variable table.
27 * @compile -g testdata/Lvt.java
28 * @run junit LvtTest
29 */
30 import helpers.ClassRecord;
31 import helpers.Transforms;
32 import java.lang.classfile.*;
33
34 import java.io.*;
35 import java.lang.constant.ClassDesc;
36 import java.net.URI;
37 import java.nio.file.Files;
38 import java.nio.file.Paths;
39 import java.util.ArrayList;
40 import java.util.List;
41
42 import java.lang.classfile.Attributes;
43 import java.lang.classfile.attribute.SourceFileAttribute;
44 import java.lang.classfile.constantpool.ConstantPoolBuilder;
45 import java.lang.classfile.constantpool.Utf8Entry;
46 import java.lang.classfile.instruction.LocalVariable;
47 import java.lang.classfile.instruction.LocalVariableType;
48 import java.lang.reflect.AccessFlag;
49 import org.junit.jupiter.api.Test;
50
51 import static helpers.TestConstants.CD_ArrayList;
52 import static helpers.TestConstants.CD_PrintStream;
53 import static helpers.TestConstants.CD_System;
54 import static helpers.TestConstants.MTD_INT_VOID;
55 import static helpers.TestConstants.MTD_VOID;
56 import static helpers.TestUtil.ExpectedLvRecord;
57 import static helpers.TestUtil.ExpectedLvtRecord;
58 import static java.lang.classfile.ClassFile.ACC_PUBLIC;
59 import static java.lang.classfile.ClassFile.ACC_STATIC;
60 import static java.lang.constant.ConstantDescs.*;
61 import java.lang.constant.MethodTypeDesc;
62 import static org.junit.jupiter.api.Assertions.*;
63
64 class LvtTest {
65 static byte[] fileBytes;
66
67 static {
68 try {
69 fileBytes = Files.readAllBytes(Paths.get(URI.create(testdata.Lvt.class.getResource("Lvt.class").toString())));
70 }
71 catch (IOException e) {
72 throw new ExceptionInInitializerError(e);
73 }
74 }
75
76 @Test
77 void getLVTEntries() {
78 ClassModel c = ClassFile.of().parse(fileBytes);
79 CodeModel co = c.methods().stream()
80 .filter(mm -> mm.methodName().stringValue().equals("m"))
81 .map(MethodModel::code)
82 .findFirst()
83 .get()
84 .orElseThrow();
85
86 List<LocalVariable> lvs = new ArrayList<>();
87 co.forEach(e -> {
88 if (e instanceof LocalVariable l) lvs.add(l);
89 });
90
91 List<ExpectedLvRecord> expected = List.of(
92 ExpectedLvRecord.of(5, "j", "I", 9, 21),
93 ExpectedLvRecord.of(0, "this", "Ltestdata/Lvt;", 0, 31),
94 ExpectedLvRecord.of(1, "a", "Ljava/lang/String;", 0, 31),
95 ExpectedLvRecord.of(4, "d", "[C", 6, 25));
96
97 // Exploits non-symmetric "equals" in ExpectedLvRecord
98 assertTrue(expected.equals(lvs));
99 }
100
101 @Test
102 void buildLVTEntries() throws Exception {
103 var cc = ClassFile.of();
104 ClassModel c = cc.parse(fileBytes);
105
106 // Compare transformed model and original with CodeBuilder filter
107 byte[] newClass = cc.transformClass(c, Transforms.threeLevelNoop);
108 ClassRecord orig = ClassRecord.ofClassModel(cc.parse(fileBytes), ClassRecord.CompatibilityFilter.By_ClassBuilder);
109 ClassRecord transformed = ClassRecord.ofClassModel(cc.parse(newClass), ClassRecord.CompatibilityFilter.By_ClassBuilder);
110 ClassRecord.assertEqualsDeep(transformed, orig);
111 }
112
113 @Test
114 void testCreateLoadLVT() throws Exception {
115 var cc = ClassFile.of();
116 byte[] bytes = cc.build(ClassDesc.of("MyClass"), cb -> {
117 cb.withFlags(AccessFlag.PUBLIC);
118 cb.withVersion(52, 0);
119 cb.with(SourceFileAttribute.of(cb.constantPool().utf8Entry(("MyClass.java"))))
120 .withMethod("<init>", MethodTypeDesc.of(CD_void), 0, mb -> mb
121 .withCode(codeb -> codeb.aload(0)
122 .invokespecial(CD_Object, "<init>", MTD_VOID, false)
123 .return_()
124 )
125 )
126 .withMethod("main", MethodTypeDesc.of(CD_void, CD_String.arrayType()),
127 ACC_PUBLIC | ACC_STATIC,
128 mb -> mb
129 .withCode(c0 -> {
130 ConstantPoolBuilder cpb = cb.constantPool();
131 Utf8Entry slotName = cpb.utf8Entry("this");
132 Utf8Entry desc = cpb.utf8Entry("LMyClass;");
133 Utf8Entry i1n = cpb.utf8Entry("res");
134 Utf8Entry i2 = cpb.utf8Entry("i");
135 Utf8Entry intSig = cpb.utf8Entry("I");
136 Label start = c0.newLabel();
137 Label end = c0.newLabel();
138 Label i1 = c0.newLabel();
139 Label preEnd = c0.newLabel();
140 Label loopTop = c0.newLabel();
141 Label loopEnd = c0.newLabel();
142 c0.localVariable(1, i1n, intSig, i1, preEnd) // LV Entries can be added before the labels
143 .localVariable(2, i2, intSig, loopTop, preEnd)
144 .labelBinding(start)
145 .iconst_1() // 0
146 .istore(1) // 1
147 .labelBinding(i1)
148 .iconst_1() // 2
149 .istore(2) // 3
150 .labelBinding(loopTop)
151 .iload(2) // 4
152 .bipush(10) // 5
153 .if_icmpge(loopEnd) // 6
154 .iload(1) // 7
155 .iload(2) // 8
156 .imul() // 9
157 .istore(1) // 10
158 .iinc(2, 1) // 11
159 .goto_(loopTop) // 12
160 .labelBinding(loopEnd)
161 .getstatic(CD_System, "out", CD_PrintStream) // 13
162 .iload(1)
163 .invokevirtual(CD_PrintStream, "println", MTD_INT_VOID) // 15
164 .labelBinding(preEnd)
165 .return_()
166 .labelBinding(end)
167 .localVariable(0, slotName, desc, start, end); // and lv entries can be added after the labels
168 }));
169 });
170
171 var c = cc.parse(bytes);
172 var main = c.methods().get(1);
173 var lvt = main.code().get().findAttribute(Attributes.localVariableTable()).get();
174 var lvs = lvt.localVariables();
175
176 assertEquals(lvs.size(), 3);
177 List<ExpectedLvRecord> expected = List.of(
178 ExpectedLvRecord.of(1, "res", "I", 2, 25),
179 ExpectedLvRecord.of(2, "i", "I", 4, 23),
180 ExpectedLvRecord.of(0, "this", "LMyClass;", 0, 28));
181
182 // Exploits non-symmetric "equals" in ExpectedLvRecord
183 assertTrue(expected.equals(lvs));
184 }
185
186 @Test
187 void getLVTTEntries() {
188 ClassModel c = ClassFile.of().parse(fileBytes);
189 CodeModel co = c.methods().stream()
190 .filter(mm -> mm.methodName().stringValue().equals("n"))
191 .map(MethodModel::code)
192 .findFirst()
193 .get()
194 .orElseThrow();
195
196 List<LocalVariableType> lvts = new ArrayList<>();
197 co.forEach(e -> {
198 if (e instanceof LocalVariableType l) lvts.add(l);
199 });
200
201 /* From javap:
202
203 LocalVariableTypeTable:
204 Start Length Slot Name Signature
205 51 8 6 f Ljava/util/List<*>;
206 0 64 1 u TU;
207 0 64 2 z Ljava/lang/Class<+Ljava/util/List<*>;>;
208 8 56 3 v Ljava/util/ArrayList<Ljava/lang/Integer;>;
209 17 47 4 s Ljava/util/Set<-Ljava/util/Set;>;
210 */
211
212 List<ExpectedLvtRecord> expected = List.of(
213 ExpectedLvtRecord.of(6, "f", "Ljava/util/List<*>;", 51, 8),
214 ExpectedLvtRecord.of(1, "u", "TU;", 0, 64),
215 ExpectedLvtRecord.of(2, "z", "Ljava/lang/Class<+Ljava/util/List<*>;>;", 0, 64),
216 ExpectedLvtRecord.of(3, "v", "Ljava/util/ArrayList<Ljava/lang/Integer;>;", 8, 56),
217 ExpectedLvtRecord.of(4, "s", "Ljava/util/Set<-Ljava/util/Set<*>;>;", 17, 47)
218 );
219
220 // Exploits non-symmetric "equals" in ExpectedLvRecord
221 for (int i = 0; i < lvts.size(); i++) {
222 assertTrue(expected.get(i).equals(lvts.get(i)));
223 }
224 }
225
226 @Test
227 void testCreateLoadLVTT() throws Exception {
228 var cc = ClassFile.of();
229 byte[] bytes = cc.build(ClassDesc.of("MyClass"), cb -> {
230 cb.withFlags(AccessFlag.PUBLIC);
231 cb.withVersion(52, 0);
232 cb.with(SourceFileAttribute.of(cb.constantPool().utf8Entry(("MyClass.java"))))
233
234 .withMethod("<init>", MethodTypeDesc.of(CD_void), 0, mb -> mb
235 .withCode(codeb -> codeb.aload(0)
236 .invokespecial(CD_Object, "<init>", MTD_VOID, false)
237 .return_()
238 )
239 )
240
241 .withMethod("m", MethodTypeDesc.of(CD_Object, CD_Object.arrayType()),
242 ACC_PUBLIC,
243 mb -> mb.withFlags(AccessFlag.PUBLIC)
244 .withCode(c0 -> {
245 ConstantPoolBuilder cpb = cb.constantPool();
246 Utf8Entry slotName = cpb.utf8Entry("this");
247 Utf8Entry desc = cpb.utf8Entry("LMyClass;");
248 Utf8Entry juList = cpb.utf8Entry("Ljava/util/List;");
249 Utf8Entry TU = cpb.utf8Entry("TU;");
250 Utf8Entry sig = cpb.utf8Entry("Ljava/util/List<+Ljava/lang/Object;>;");
251 Utf8Entry l = cpb.utf8Entry("l");
252 Utf8Entry jlObject = cpb.utf8Entry("Ljava/lang/Object;");
253 Utf8Entry u = cpb.utf8Entry("u");
254
255 Label start = c0.newLabel();
256 Label end = c0.newLabel();
257 Label beforeRet = c0.newLabel();
258
259 c0.localVariable(2, l, juList, beforeRet, end)
260 .localVariableType(1, u, TU, start, end)
261 .labelBinding(start)
262 .new_(ClassDesc.of("java.util.ArrayList"))
263 .dup()
264 .invokespecial(CD_ArrayList, "<init>", MTD_VOID, false)
265 .astore(2)
266 .labelBinding(beforeRet)
267 .localVariableType(2, l, sig, beforeRet, end)
268 .aload(1)
269 .areturn()
270 .labelBinding(end)
271 .localVariable(0, slotName, desc, start, end)
272 .localVariable(1, u, jlObject, start, end);
273 }));
274 });
275 var c = cc.parse(bytes);
276 var main = c.methods().get(1);
277 var lvtt = main.code().get().findAttribute(Attributes.localVariableTypeTable()).get();
278 var lvts = lvtt.localVariableTypes();
279
280 /* From javap:
281
282 LocalVariableTypeTable:
283 Start Length Slot Name Signature
284 0 10 1 u TU;
285 8 2 2 l Ljava/util/List<+Ljava/lang/Object;>;
286 */
287
288 List<ExpectedLvtRecord> expected = List.of(
289 ExpectedLvtRecord.of(1, "u", "TU;", 0, 10),
290 ExpectedLvtRecord.of(2, "l", "Ljava/util/List<+Ljava/lang/Object;>;", 8, 2)
291 );
292
293 // Exploits non-symmetric "equals" in ExpectedLvRecord
294 for (int i = 0; i < lvts.size(); i++) {
295 assertTrue(expected.get(i).equals(lvts.get(i)));
296 }
297 }
298
299 @Test
300 void skipDebugSkipsLVT() {
301 ClassModel c = ClassFile.of(ClassFile.DebugElementsOption.DROP_DEBUG).parse(fileBytes);
302
303 c.forEach(e -> {
304 if (e instanceof MethodModel m) {
305 m.forEach(el -> {
306 if (el instanceof CodeModel cm) {
307 cm.forEach(elem -> {
308 assertFalse(elem instanceof LocalVariable);
309 assertFalse(elem instanceof LocalVariableType);
310 });
311 }
312 });
313 }
314 });
315 }
316 }