diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java index 35725683..7053f2aa 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java @@ -11,4 +11,5 @@ public enum BytecodeMarshalingErrorCode { MARSHAL_VERIFY_PRECHECK_EXPORT_FUNC_INDEX_INVALID, MARSHAL_VERIFY_PRECHECK_HOSTCALL_INDEX_INVALID, MARSHAL_VERIFY_PRECHECK_TRUNCATED_INSTRUCTION, + MARSHAL_VERIFY_PRECHECK_UNTERMINATED_FUNCTION, } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodePreloadVerifierService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodePreloadVerifierService.java new file mode 100644 index 00000000..9c5f7ed2 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodePreloadVerifierService.java @@ -0,0 +1,67 @@ +package p.studio.compiler.backend.bytecode; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Set; + +public class BytecodePreloadVerifierService { + private static final Set TERMINATORS = Set.of(0x01, 0x02, 0x51); + + public void verify(final BytecodeModule module) { + final var input = module == null ? BytecodeModule.empty() : module; + for (final var function : input.functions()) { + verifyFunction(input.code(), function); + } + } + + private void verifyFunction(final byte[] code, final BytecodeModule.FunctionMeta function) { + final var start = function.codeOffset(); + final var end = start + function.codeLen(); + if (end <= start) { + return; + } + + var pc = start; + var lastOpcode = -1; + while (pc < end) { + if (pc + 2 > end) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_TRUNCATED_INSTRUCTION, + "truncated opcode in function at pc=" + pc); + } + final var opcode = readU16(code, pc); + if (opcode == 0x70) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD, + "raw syscall is forbidden in pre-load artifact"); + } + final var size = sizeForOpcode(opcode); + if (pc + size > end) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_TRUNCATED_INSTRUCTION, + "truncated instruction in function at pc=" + pc); + } + lastOpcode = opcode; + pc += size; + } + + if (!TERMINATORS.contains(lastOpcode)) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_UNTERMINATED_FUNCTION, + "function ends without terminator opcode"); + } + } + + private int sizeForOpcode(final int opcode) { + return switch (opcode) { + case 0x50, 0x70, 0x71, 0x72, 0x02, 0x03, 0x04, 0x10, 0x18, 0x40, 0x41, 0x42, 0x43, 0x56 -> 6; + case 0x14, 0x15, 0x52, 0x54 -> 10; + case 0x16 -> 3; + default -> 2; + }; + } + + private int readU16(final byte[] code, final int offset) { + return ByteBuffer.wrap(code, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF; + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java index 4c0628ab..5f0266a2 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java @@ -22,7 +22,8 @@ public class BuilderPipelineService { new LowerToIRVMPipelineStage(), new OptimizeIRVMPipelineStage(), new EmitBytecodePipelineStage(), - new LinkBytecodePipelineStage() + new LinkBytecodePipelineStage(), + new VerifyBytecodePipelineStage() ); INSTANCE = new BuilderPipelineService(stages); } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/VerifyBytecodePipelineStage.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/VerifyBytecodePipelineStage.java new file mode 100644 index 00000000..e8ee069e --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/VerifyBytecodePipelineStage.java @@ -0,0 +1,48 @@ +package p.studio.compiler.workspaces.stages; + +import lombok.extern.slf4j.Slf4j; +import p.studio.compiler.backend.bytecode.BytecodeMarshalingException; +import p.studio.compiler.backend.bytecode.BytecodePreloadVerifierService; +import p.studio.compiler.messages.BuildingIssueSink; +import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.compiler.workspaces.PipelineStage; +import p.studio.utilities.logs.LogAggregator; + +@Slf4j +public class VerifyBytecodePipelineStage implements PipelineStage { + private final BytecodePreloadVerifierService verifierService; + + public VerifyBytecodePipelineStage() { + this(new BytecodePreloadVerifierService()); + } + + VerifyBytecodePipelineStage(final BytecodePreloadVerifierService verifierService) { + this.verifierService = verifierService; + } + + @Override + public BuildingIssueSink run(BuilderPipelineContext ctx, LogAggregator logs) { + if (ctx.bytecodeModule == null) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .phase("BACKEND_VERIFY_PRELOAD") + .code("MARSHAL_FORMAT_STAGE_INPUT_MISSING") + .message("[BUILD]: bytecode module is missing before VerifyBytecode stage")); + } + + try { + verifierService.verify(ctx.bytecodeModule); + } catch (BytecodeMarshalingException e) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .phase("BACKEND_VERIFY_PRELOAD") + .code(e.code().name()) + .message("[BUILD]: preload verifier failed: " + e.getMessage()) + .exception(e)); + } + + return BuildingIssueSink.empty(); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodePreloadVerifierServiceTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodePreloadVerifierServiceTest.java new file mode 100644 index 00000000..cb77488b --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodePreloadVerifierServiceTest.java @@ -0,0 +1,80 @@ +package p.studio.compiler.backend.bytecode; + +import org.junit.jupiter.api.Test; +import p.studio.utilities.structures.ReadOnlyList; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BytecodePreloadVerifierServiceTest { + + private final BytecodePreloadVerifierService service = new BytecodePreloadVerifierService(); + + @Test + void verifyMustAcceptTerminatedFunctionWithoutRawSyscall() { + final var module = new BytecodeModule( + 0, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 2, 0, 0, 0, 1)), + codeRet(), + null, + ReadOnlyList.empty(), + ReadOnlyList.empty()); + + assertDoesNotThrow(() -> service.verify(module)); + } + + @Test + void verifyMustRejectRawSyscallInPreload() { + final var module = new BytecodeModule( + 0, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 6, 0, 0, 0, 1)), + codeRawSyscall(0x1001), + null, + ReadOnlyList.empty(), + ReadOnlyList.empty()); + + final var thrown = assertThrows(BytecodeMarshalingException.class, () -> service.verify(module)); + assertEquals(BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD, thrown.code()); + } + + @Test + void verifyMustRejectFunctionWithoutTerminator() { + final var module = new BytecodeModule( + 0, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 6, 0, 0, 0, 1)), + codeHostcall(0), + null, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.SyscallDecl("gfx", "draw", 1, 0, 0))); + + final var thrown = assertThrows(BytecodeMarshalingException.class, () -> service.verify(module)); + assertEquals(BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_UNTERMINATED_FUNCTION, thrown.code()); + } + + private byte[] codeHostcall(final int index) { + final var out = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN); + out.putShort((short) 0x71); + out.putInt(index); + return out.array(); + } + + private byte[] codeRawSyscall(final int index) { + final var out = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN); + out.putShort((short) 0x70); + out.putInt(index); + return out.array(); + } + + private byte[] codeRet() { + final var out = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN); + out.putShort((short) 0x51); + return out.array(); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java index 01539d02..91ecf0e1 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java @@ -8,6 +8,7 @@ import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage; import p.studio.compiler.workspaces.stages.LowerToIRVMPipelineStage; import p.studio.compiler.workspaces.stages.OptimizeIRVMPipelineStage; import p.studio.compiler.workspaces.stages.ResolveDepsPipelineStage; +import p.studio.compiler.workspaces.stages.VerifyBytecodePipelineStage; import java.lang.reflect.Field; import java.util.List; @@ -33,7 +34,8 @@ class BuilderPipelineServiceOrderTest { LowerToIRVMPipelineStage.class, OptimizeIRVMPipelineStage.class, EmitBytecodePipelineStage.class, - LinkBytecodePipelineStage.class), + LinkBytecodePipelineStage.class, + VerifyBytecodePipelineStage.class), stageTypes); } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/VerifyBytecodePipelineStageTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/VerifyBytecodePipelineStageTest.java new file mode 100644 index 00000000..48c8a009 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/VerifyBytecodePipelineStageTest.java @@ -0,0 +1,56 @@ +package p.studio.compiler.workspaces.stages; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.backend.bytecode.BytecodeModule; +import p.studio.compiler.messages.BuilderPipelineConfig; +import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.utilities.logs.LogAggregator; +import p.studio.utilities.structures.ReadOnlyList; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class VerifyBytecodePipelineStageTest { + + @Test + void runMustFailWhenBytecodeModuleIsMissing() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + final var stage = new VerifyBytecodePipelineStage(); + + final var issues = stage.run(ctx, LogAggregator.empty()); + final var firstIssue = issues.asCollection().iterator().next(); + + assertTrue(issues.hasErrors()); + assertEquals("BACKEND_VERIFY_PRELOAD", firstIssue.getPhase()); + assertEquals("MARSHAL_FORMAT_STAGE_INPUT_MISSING", firstIssue.getCode()); + } + + @Test + void runMustRejectRawSyscallInPreload() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + ctx.bytecodeModule = new BytecodeModule( + 0, + ReadOnlyList.empty(), + ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 6, 0, 0, 0, 1)), + codeRawSyscall(0x1001), + null, + ReadOnlyList.empty(), + ReadOnlyList.empty()); + + final var issues = new VerifyBytecodePipelineStage().run(ctx, LogAggregator.empty()); + final var firstIssue = issues.asCollection().iterator().next(); + + assertTrue(issues.hasErrors()); + assertEquals("MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD", firstIssue.getCode()); + } + + private byte[] codeRawSyscall(final int index) { + final var out = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN); + out.putShort((short) 0x70); + out.putInt(index); + return out.array(); + } +}