From a0ff7562cc5f9b57e46d08dd1362b87e4d7a9214 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 25 Jun 2024 23:55:48 -0400 Subject: [PATCH] misc: Add tests to validate recent fix commits --- .../analysis/jvm/ValuedJvmAnalysisEngine.java | 2 + .../darknet/assembler/SampleCompilerTest.java | 55 +++++++++ .../java/me/darknet/assembler/TestUtils.java | 108 ++++++++++-------- .../samples/jasm/Example-push-type.jasm | 12 ++ .../samples/jasm/Example-wide-invoke.jasm | 16 +++ 5 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 jasm-composition-jvm/src/test/resources/samples/jasm/Example-push-type.jasm create mode 100644 jasm-composition-jvm/src/test/resources/samples/jasm/Example-wide-invoke.jasm diff --git a/jasm-composition-jvm/src/main/java/me/darknet/assembler/compile/analysis/jvm/ValuedJvmAnalysisEngine.java b/jasm-composition-jvm/src/main/java/me/darknet/assembler/compile/analysis/jvm/ValuedJvmAnalysisEngine.java index 1f37767..d096931 100644 --- a/jasm-composition-jvm/src/main/java/me/darknet/assembler/compile/analysis/jvm/ValuedJvmAnalysisEngine.java +++ b/jasm-composition-jvm/src/main/java/me/darknet/assembler/compile/analysis/jvm/ValuedJvmAnalysisEngine.java @@ -577,6 +577,8 @@ public void execute(MethodInstruction instruction) { Value value = frame.pop(type); parameters.add(0, value); canLookup &= value.isKnown(); + if (value instanceof Value.VoidValue) + warn(instruction, "Cannot pass 'void' as method argument"); } Value.ObjectValue contextObject = null; diff --git a/jasm-composition-jvm/src/test/java/me/darknet/assembler/SampleCompilerTest.java b/jasm-composition-jvm/src/test/java/me/darknet/assembler/SampleCompilerTest.java index 0dac9f9..73cbcb9 100644 --- a/jasm-composition-jvm/src/test/java/me/darknet/assembler/SampleCompilerTest.java +++ b/jasm-composition-jvm/src/test/java/me/darknet/assembler/SampleCompilerTest.java @@ -1,5 +1,6 @@ package me.darknet.assembler; +import dev.xdark.blw.code.instruction.MethodInstruction; import dev.xdark.blw.type.Types; import me.darknet.assembler.compile.analysis.AnalysisResults; import me.darknet.assembler.compile.analysis.Local; @@ -8,6 +9,7 @@ import me.darknet.assembler.compile.analysis.frame.Frame; import me.darknet.assembler.compile.analysis.frame.ValuedFrame; import me.darknet.assembler.compile.analysis.BasicMethodValueLookup; +import me.darknet.assembler.compile.analysis.jvm.MethodValueLookup; import me.darknet.assembler.compile.analysis.jvm.TypedJvmAnalysisEngine; import me.darknet.assembler.compile.analysis.jvm.ValuedJvmAnalysisEngine; import me.darknet.assembler.compiler.ReflectiveInheritanceChecker; @@ -99,6 +101,26 @@ void intMath(String name) throws Throwable { }); } + @Test + void ldcPushType() throws Throwable { + TestArgument arg = TestArgument.fromName("Example-push-type.jasm"); + String source = arg.source.get(); + TestJvmCompilerOptions options = new TestJvmCompilerOptions(); + options.engineProvider(ValuedJvmAnalysisEngine::new); + processJvm(source, options, result -> { + AnalysisResults results = result.analysisLookup().allResults().values().iterator().next(); + assertNull(results.getAnalysisFailure()); + assertFalse(results.terminalFrames().isEmpty()); + + ValuedFrame frame = (ValuedFrame) results.terminalFrames().values().iterator().next(); + if (frame.peek() instanceof Value.ObjectValue objectValue) { + assertEquals(Types.instanceType(Class.class), objectValue.type(), "Pushing type to stack did not yield class reference"); + } else { + fail("Did not yield object value"); + } + }); + } + @Test void methodLookup() throws Throwable { TestArgument arg = TestArgument.fromName("Example-string-ops.jasm"); @@ -376,6 +398,39 @@ void athrowDoesNotAllowFlowThroughToNextFrameAndClearsStack() throws Throwable { }); } + @Test + void stackPopForInvokes() throws Throwable { + TestArgument arg = TestArgument.fromName("Example-wide-invoke.jasm"); + String source = arg.source.get(); + + // Create an analysis engine which will observe the invokestatic method in the source. + // If wide types are mishandled it will not get visited. + boolean[] visited = new boolean[1]; + TestJvmCompilerOptions options = new TestJvmCompilerOptions(); + options.engineProvider(lookup -> { + ValuedJvmAnalysisEngine engine = new ValuedJvmAnalysisEngine(lookup); + engine.setMethodValueLookup(new MethodValueLookup() { + @Override + public @NotNull Value accept(@NotNull MethodInstruction instruction, Value.@Nullable ObjectValue context, @NotNull List parameters) { + visited[0] = true; + return Values.LONG_VALUE; + } + }); + return engine; + }); + + processJvm(source, options, result -> { + AnalysisResults results = result.analysisLookup().allResults().values().iterator().next(); + assertNull(results.getAnalysisFailure()); + assertFalse(results.terminalFrames().isEmpty()); + }, warns -> { + // Void type usage in the engine for method parameters should emit a warning. + // If this occurs we've broken something. + fail("Expected no warnings, found: " + warns); + }); + assertTrue(visited[0], "Method call was not visited"); + } + @Test void checkcastChangesType() throws Throwable { TestArgument arg = TestArgument.fromName("Example-checkcast.jasm"); diff --git a/jasm-composition-jvm/src/test/java/me/darknet/assembler/TestUtils.java b/jasm-composition-jvm/src/test/java/me/darknet/assembler/TestUtils.java index 3a5e503..9ef7770 100644 --- a/jasm-composition-jvm/src/test/java/me/darknet/assembler/TestUtils.java +++ b/jasm-composition-jvm/src/test/java/me/darknet/assembler/TestUtils.java @@ -23,6 +23,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.PrintWriter; +import java.util.List; +import java.util.function.Consumer; import java.util.regex.Pattern; public class TestUtils { @@ -35,59 +37,75 @@ public class TestUtils { * * @param source Jasm source to process. * @param options Jasm compiler options. + * @param outputConsumer Consumer to act on the compilation result. */ public static void processJvm(@NotNull String source, @NotNull CompilerOptions options, @Nullable ThrowingConsumer outputConsumer) { - Processor.processSource(source, "", (ast) -> { - JvmCompiler compiler = new JvmCompiler(); - compiler.compile(ast, options).ifOk(result -> { - try { - if (outputConsumer != null) - outputConsumer.accept(result); - } catch (AssertionFailedError e) { - // Pass up the chain - throw e; - } catch (Throwable e) { - // Consumer should fail instead of us handling it generically here - fail(e); - return; - } + processJvm(source, options, outputConsumer, null); + } - // Check if bytes are a valid class file - JavaClassRepresentation representation = result.representation(); - byte[] bytes = representation.classFile(); - try { - compiler.library().read(new ByteArrayInputStream(bytes), new GenericClassBuilder()); - } catch (IOException e) { - fail("Generated class was not readable", e); - } + /** + * Asserts that valid output was emitted with no errors (Warnings are ok though). + * + * @param source Jasm source to process. + * @param options Jasm compiler options. + * @param outputConsumer Consumer to act on the compilation result. + * @param warningConsumer Consumer to act on warnings. + */ + public static void processJvm(@NotNull String source, @NotNull CompilerOptions options, + @Nullable ThrowingConsumer outputConsumer, + @Nullable Consumer> warningConsumer) { + Processor.processSource(source, "", (ast) -> { + JvmCompiler compiler = new JvmCompiler(); + compiler.compile(ast, options).ifOk(result -> { + try { + if (outputConsumer != null) + outputConsumer.accept(result); + } catch (AssertionFailedError e) { + // Pass up the chain + throw e; + } catch (Throwable e) { + // Consumer should fail instead of us handling it generically here + fail(e); + return; + } - // And double check that its verifiable - try { - CheckClassAdapter.verify( - new ClassReader(bytes), - true, - new PrintWriter(System.out) - ); - } catch (Throwable e) { - fail("Generated class was not verifiable", e); - } + // Check if bytes are a valid class file + JavaClassRepresentation representation = result.representation(); + byte[] bytes = representation.classFile(); + try { + compiler.library().read(new ByteArrayInputStream(bytes), new GenericClassBuilder()); + } catch (IOException e) { + fail("Generated class was not readable", e); + } + // And double check that its verifiable + try { + CheckClassAdapter.verify( + new ClassReader(bytes), + true, + new PrintWriter(System.out) + ); + } catch (Throwable e) { + fail("Generated class was not verifiable", e); + } - }).ifErr(errors -> { - for (Error error : errors) { - System.err.println(error); - } - fail("Failed to analyze/compile class, errors were reported"); - }); - }, errors -> { - for (Error error : errors) { - System.err.println(error); - } - fail("Failed to parse class"); - }, BytecodeFormat.JVM); - } + }).ifErr(errors -> { + for (Error error : errors) { + System.err.println(error); + } + fail("Failed to analyze/compile class, errors were reported"); + }).ifWarn(warns -> { + if (warningConsumer != null) warningConsumer.accept(warns); + }); + }, errors -> { + for (Error error : errors) { + System.err.println(error); + } + fail("Failed to parse class"); + }, BytecodeFormat.JVM); + } /** * Asserts that errors were emitted. diff --git a/jasm-composition-jvm/src/test/resources/samples/jasm/Example-push-type.jasm b/jasm-composition-jvm/src/test/resources/samples/jasm/Example-push-type.jasm new file mode 100644 index 0000000..fcb042e --- /dev/null +++ b/jasm-composition-jvm/src/test/resources/samples/jasm/Example-push-type.jasm @@ -0,0 +1,12 @@ +.super java/lang/Object +.class public super Example { + .method public exampleMethod ()Ljava/lang/Class; { + parameters: { this }, + code: { + A: + ldc Ljava/lang/String; + areturn + B: + } + } +} \ No newline at end of file diff --git a/jasm-composition-jvm/src/test/resources/samples/jasm/Example-wide-invoke.jasm b/jasm-composition-jvm/src/test/resources/samples/jasm/Example-wide-invoke.jasm new file mode 100644 index 0000000..afe47da --- /dev/null +++ b/jasm-composition-jvm/src/test/resources/samples/jasm/Example-wide-invoke.jasm @@ -0,0 +1,16 @@ +.super java/lang/Object +.class public super Example { + .method public exampleMethod ()I { + parameters: { this }, + code: { + A: + iconst_1 + ldc 10000000L + bipush 250 + invokestatic java/lang/Math.multiplyExact (JI)J + pop2 + ireturn + B: + } + } +} \ No newline at end of file