// SPDX-License-Identifier: GPL-3.0-or-later #include "rcc/Tooling/MIRFuzzer.h" #include "rcc/CodeGen/GenericOpcodes.h" #include "rcc/CodeGen/InstructionDescriptor.h" #include "rcc/CodeGen/LowLevelType.h" #include "rcc/CodeGen/MIRPrinter.h" #include "rcc/CodeGen/MachineBasicBlock.h" #include "rcc/CodeGen/MachineFunction.h" #include "rcc/CodeGen/MachineIRBuilder.h" #include "rcc/CodeGen/MachineInstr.h" #include "rcc/CodeGen/MachineOperand.h" #include "rcc/CodeGen/MachineRegisterInfo.h" #include "rcc/CodeGen/MachineVerifier.h" #include "rcc/CodeGen/TargetDescription.h" #include "rcc/Target/RISCV/RISCVPipeline.h" #include "rcc/Tooling/ArtifactStore.h" #include "rcc/Tooling/BackendDriver.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace rcc::tooling { namespace { class DeterministicRandom final { public: explicit DeterministicRandom(std::uint64_t Seed) noexcept : State(Seed == 0 ? 0x243f6a8885a308d3ULL : Seed) {} [[nodiscard]] std::uint64_t next() noexcept { State ^= State >> 12U; State ^= State << 25U; State ^= State >> 27U; return State * 0x2545f4914f6cdd1dULL; } [[nodiscard]] std::size_t index(std::size_t Bound) noexcept { return Bound == 0 ? 0 : static_cast(next() % Bound); } private: std::uint64_t State; }; [[nodiscard]] std::vector defaultLayers() { return { MIRFuzzLayer::Parser, MIRFuzzLayer::BuilderStructural, MIRFuzzLayer::PropertyStage, MIRFuzzLayer::Mutation, MIRFuzzLayer::Pipeline, MIRFuzzLayer::RestrictedSemantics, }; } [[nodiscard]] Error fuzzFailure(MIRFuzzLayer Layer, std::size_t Iteration, const Error &Failure) { return Error(ErrorCode::FuzzingFailure, std::string(spelling(Layer)) + " fuzz case " + std::to_string(Iteration) + " violated an invariant: " + std::string(Failure.message())); } [[nodiscard]] std::string byteMutation(std::string_view Input, DeterministicRandom &Random, std::size_t MaximumSize) { std::string Result(Input); switch (Random.index(5)) { case 0: if (!Result.empty()) Result[Random.index(Result.size())] = static_cast(Random.next() & 0xffU); break; case 1: if (!Result.empty()) Result.erase(Random.index(Result.size()), 1); break; case 2: if (Result.size() < MaximumSize) Result.insert(Result.begin() + static_cast(Random.index(Result.size() + 1U)), static_cast(Random.next() & 0xffU)); break; case 3: if (!Result.empty()) Result.resize(Random.index(Result.size())); break; case 4: if (!Result.empty() && Result.size() < MaximumSize) { const std::size_t Begin = Random.index(Result.size()); const std::size_t Length = std::min(Random.index(32U) + 1U, Result.size() - Begin); const std::string Fragment = Result.substr(Begin, Length); const std::size_t Available = MaximumSize - Result.size(); Result.append(Fragment.substr(0, Available)); } break; default: break; } return Result; } [[nodiscard]] std::string structuralTextMutation(std::string_view Input, DeterministicRandom &Random) { std::string Result(Input); std::vector LineStarts{0}; for (std::size_t Position = 0; Position < Result.size(); ++Position) { if (Result[Position] == '\n' && Position + 1U < Result.size()) LineStarts.push_back(Position + 1U); } if (LineStarts.empty()) return Result; const std::size_t LineIndex = Random.index(LineStarts.size()); const std::size_t Begin = LineStarts[LineIndex]; const std::size_t End = Result.find('\n', Begin); const std::size_t LineEnd = End == std::string::npos ? Result.size() : End + 1U; switch (Random.index(4)) { case 0: Result.erase(Begin, LineEnd - Begin); break; case 1: Result.insert(LineEnd, Result.substr(Begin, LineEnd - Begin)); break; case 2: { const std::size_t Digit = Result.find_first_of("0123456789", Begin); if (Digit < LineEnd) Result[Digit] = static_cast('0' + Random.index(10)); break; } case 3: { const std::size_t Percent = Result.find('%', Begin); if (Percent < LineEnd && Percent + 1U < Result.size()) Result[Percent + 1U] = static_cast('0' + Random.index(10)); break; } default: break; } return Result; } [[nodiscard]] Expected verifyCanonicalRoundTrip(std::string Text, const TargetDescription &Target, const BackendInvocation &Invocation) { auto First = parseAndCanonicalizeMIR("fuzz-round-trip.mir", std::move(Text), Target, Invocation); if (!First) return std::unexpected(First.error()); auto Second = parseAndCanonicalizeMIR("fuzz-round-trip-2.mir", First->Text, Target, Invocation); if (!Second) return std::unexpected(Second.error()); if (First->Text != Second->Text) { return makeUnexpected(ErrorCode::FuzzingFailure, "successful MIR round-trip is not structurally deterministic"); } return {}; } [[nodiscard]] Expected parserCase(std::string_view Canonical, DeterministicRandom &Random, const TargetDescription &Target, const BackendInvocation &Invocation, std::size_t MaximumSize) { std::string Mutated = byteMutation(Canonical, Random, MaximumSize); auto Parsed = parseAndCanonicalizeMIR("parser-fuzz.mir", Mutated, Target, Invocation); if (!Parsed) return false; auto RoundTrip = verifyCanonicalRoundTrip(std::move(Parsed->Text), Target, Invocation); if (!RoundTrip) return std::unexpected(RoundTrip.error()); return true; } [[nodiscard]] Expected builderCase(std::string_view Canonical, const TargetDescription &Target, const BackendInvocation &Invocation) { auto Parsed = parseAndCanonicalizeMIR("builder-fuzz.mir", std::string(Canonical), Target, Invocation); if (!Parsed) return std::unexpected(Parsed.error()); MachineFunction *Function = Parsed->Document.unit().functionAt(0); if (Function == nullptr || stageForFunction(*Function) != BackendStage::PreLegalizerMIR) return false; MachineBasicBlock *Block = Function->firstBlock(); if (Block == nullptr) return false; auto Type = LowLevelType::makeCategoryNeutralScalar(64); if (!Type) return std::unexpected(Type.error()); auto Register = Function->registerInfo().createVirtualRegister(*Type); if (!Register) return std::unexpected(Register.error()); MachineIRBuilder Builder(*Function, Target); Expected InsertPoint = Block->firstInstruction() == nullptr ? Builder.setInsertPoint(*Block) : Builder.setInsertPointBefore(*Block->firstInstruction()); if (!InsertPoint) return std::unexpected(InsertPoint.error()); auto Pending = Builder.buildInstr(opcode(GenericOpcode::G_IMPLICIT_DEF)); if (!Pending) return std::unexpected(Pending.error()); auto Added = Pending->addDef(*Register); if (!Added) return std::unexpected(Added.error()); auto Built = Pending->build(); if (!Built) return std::unexpected(Built.error()); auto Verified = MachineVerifier::verify(*Function, Target); if (!Verified) return std::unexpected(Verified.error()); mir::MIRDocumentPrinterOptions PrintOptions; PrintOptions.Mode = mir::MIPrintMode::Complete; PrintOptions.EmitEmbeddedIR = true; PrintOptions.DataLayout = Invocation.DataLayout; auto Printed = mir::printMIRDocument(Parsed->Document, Target, std::move(PrintOptions)); if (!Printed) return std::unexpected(Printed.error()); auto RoundTrip = verifyCanonicalRoundTrip(std::move(*Printed), Target, Invocation); if (!RoundTrip) return std::unexpected(RoundTrip.error()); return true; } [[nodiscard]] ArtifactKind outputForStage(BackendStage Stage) noexcept { switch (Stage) { case BackendStage::PreLegalizerMIR: return ArtifactKind::PreLegalizerMIR; case BackendStage::LegalizedMIR: return ArtifactKind::LegalizedMIR; case BackendStage::RegisterBankMIR: return ArtifactKind::RegisterBankMIR; case BackendStage::SelectedMIR: return ArtifactKind::SelectedMIR; case BackendStage::PostSSAMIR: return ArtifactKind::PostSSAMIR; case BackendStage::PostRegisterAllocationMIR: return ArtifactKind::PostRegisterAllocationMIR; case BackendStage::FinalMIR: return ArtifactKind::FinalMIR; } return ArtifactKind::FinalMIR; } [[nodiscard]] Expected propertyCase(std::string_view Canonical, DeterministicRandom &Random, const TargetDescription &Target, const BackendInvocation &Invocation) { auto Parsed = parseAndCanonicalizeMIR("property-fuzz.mir", std::string(Canonical), Target, Invocation); if (!Parsed) return std::unexpected(Parsed.error()); constexpr std::array Stages{ BackendStage::PreLegalizerMIR, BackendStage::LegalizedMIR, BackendStage::RegisterBankMIR, BackendStage::SelectedMIR, BackendStage::PostSSAMIR, BackendStage::PostRegisterAllocationMIR, BackendStage::FinalMIR, }; const BackendStage Stage = Stages[Random.index(Stages.size())]; const MachineFunction *Function = Parsed->Document.unit().functionAt(0); if (Function == nullptr || isEarlierStage(Stage, stageForFunction(*Function))) { return false; } auto Run = runBackend(std::move(*Parsed), Target, Invocation, outputForStage(Stage)); if (!Run) return std::unexpected(Run.error()); auto RoundTrip = verifyCanonicalRoundTrip(std::string(Run->text()), Target, Invocation); if (!RoundTrip) return std::unexpected(RoundTrip.error()); return true; } [[nodiscard]] Expected mutationCase(std::string_view Canonical, DeterministicRandom &Random, const TargetDescription &Target, const BackendInvocation &Invocation) { std::string Mutated = structuralTextMutation(Canonical, Random); auto Parsed = parseAndCanonicalizeMIR("mutation-fuzz.mir", std::move(Mutated), Target, Invocation); if (!Parsed) return false; auto RoundTrip = verifyCanonicalRoundTrip(std::move(Parsed->Text), Target, Invocation); if (!RoundTrip) return std::unexpected(RoundTrip.error()); return true; } [[nodiscard]] Expected pipelineCase(std::string_view Canonical, DeterministicRandom &Random, const TargetDescription &Target, BackendInvocation Invocation) { auto Parsed = parseAndCanonicalizeMIR("pipeline-fuzz.mir", std::string(Canonical), Target, Invocation); if (!Parsed) return std::unexpected(Parsed.error()); const MachineFunction *Function = Parsed->Document.unit().functionAt(0); if (Function == nullptr) return std::unexpected(Error(ErrorCode::InternalError, "pipeline fuzz input has no function")); auto Features = riscv::RISCVSubtargetFeatures::fromFeatureList(Invocation.Features); if (!Features) return std::unexpected(Features.error()); const riscv::RISCVSubtarget Subtarget(*Features); auto Names = riscv::canonicalPipelinePassNames(*Function, Subtarget); if (!Names) return std::unexpected(Names.error()); PipelineControls Controls; if (!Names->empty()) Controls.StopAfter = (*Names)[Random.index(Names->size())]; Controls.VerifyEach = true; Controls.DetectUnreportedChanges = true; Invocation.Determinism.ThreadCount = std::array{1, 2, 4}[Random.index(3U)]; const std::string Stop = Controls.StopAfter ? *Controls.StopAfter : "end"; auto Run = runBackend(std::move(*Parsed), Target, Invocation, ArtifactKind::FinalMIR, Controls); if (!Run) return std::unexpected(Run.error()); auto RoundTrip = verifyCanonicalRoundTrip(std::string(Run->text()), Target, Invocation); if (!RoundTrip) { return std::unexpected( Error(RoundTrip.error().code(), "pipeline stop-after '" + Stop + "': " + std::string(RoundTrip.error().message()))); } return true; } using RegisterValues = std::unordered_map; [[nodiscard]] Expected registerAt(const MachineInstr &Instruction, std::size_t Index) { const MachineOperand *Operand = Instruction.operandAt(Index); if (Operand == nullptr) return makeUnexpected(ErrorCode::Unsupported, "semantic subset requires register operands"); const std::optional Reg = Operand->registerValue(); if (!Reg) return makeUnexpected(ErrorCode::Unsupported, "semantic subset requires register operands"); return *Reg; } [[nodiscard]] Expected immediateAt(const MachineInstr &Instruction, std::size_t Index) { const MachineOperand *Operand = Instruction.operandAt(Index); if (Operand == nullptr) return makeUnexpected(ErrorCode::Unsupported, "semantic subset requires an immediate operand"); if (const auto *Immediate = Operand->getIf()) return static_cast(*Immediate); if (const auto *Immediate = Operand->getIf()) { if (Immediate->Value.bitWidth() > 64 || Immediate->Value.words().empty()) return makeUnexpected(ErrorCode::Unsupported, "semantic subset only supports integer immediates up to 64 bits"); return Immediate->Value.words().front(); } return makeUnexpected(ErrorCode::Unsupported, "semantic subset requires an integer immediate"); } [[nodiscard]] std::uint64_t readRegister(Register Reg, const RegisterValues &Values, PhysicalRegister Zero) noexcept { if (Reg.physicalRegister() == Zero) return 0; const auto Position = Values.find(Reg.opaqueValue()); return Position == Values.end() ? 0 : Position->second; } [[nodiscard]] Expected interpretStraightLine(const MachineFunction &Function, const TargetDescription &Target) { if (Function.blockCount() != 1 || Function.firstBlock() == nullptr || !Function.firstBlock()->successors().empty()) { return makeUnexpected(ErrorCode::Unsupported, "semantic subset requires one straight-line basic block"); } const PhysicalRegisterDesc *ZeroDescription = Target.registers().physicalRegister("x0"); const PhysicalRegisterDesc *ReturnDescription = Target.registers().physicalRegister("x10"); if (ZeroDescription == nullptr || ReturnDescription == nullptr) return makeUnexpected(ErrorCode::Unsupported, "semantic subset requires RV64 integer registers"); const PhysicalRegister Zero = ZeroDescription->Reg; const Register Return = Register::fromPhysical(ReturnDescription->Reg); RegisterValues Values; for (const MachineInstr *Instruction = Function.firstBlock()->firstInstruction(); Instruction != nullptr; Instruction = Instruction->next()) { const InstructionDescriptor *Descriptor = Target.instructionDescriptor(Instruction->opcode()); if (Descriptor == nullptr) return makeUnexpected(ErrorCode::Unsupported, "semantic subset encountered an unknown opcode"); const std::string_view Name = Descriptor->Name; if (Name == "PseudoRET" || Name == "JALR_RET") return readRegister(Return, Values, Zero); if (Name == "G_CONSTANT") { auto Destination = registerAt(*Instruction, 0); auto Immediate = immediateAt(*Instruction, 1); if (!Destination || !Immediate) return makeUnexpected(ErrorCode::Unsupported, "unsupported generic constant form"); Values[Destination->opaqueValue()] = *Immediate; continue; } if (Name == "COPY") { auto Destination = registerAt(*Instruction, 0); auto Source = registerAt(*Instruction, 1); if (!Destination || !Source) return makeUnexpected(ErrorCode::Unsupported, "unsupported copy form"); Values[Destination->opaqueValue()] = readRegister(*Source, Values, Zero); continue; } if (Name == "LUI") { auto Destination = registerAt(*Instruction, 0); auto Immediate = immediateAt(*Instruction, 1); if (!Destination || !Immediate) return makeUnexpected(ErrorCode::Unsupported, "unsupported LUI form"); Values[Destination->opaqueValue()] = *Immediate << 12U; continue; } const bool ImmediateForm = Name == "ADDI" || Name == "XORI" || Name == "ORI" || Name == "ANDI" || Name == "SLLI" || Name == "SRLI" || Name == "SRAI"; const bool RegisterForm = Name == "G_ADD" || Name == "G_SUB" || Name == "G_MUL" || Name == "G_AND" || Name == "G_OR" || Name == "G_XOR" || Name == "G_SHL" || Name == "G_LSHR" || Name == "G_ASHR" || Name == "ADD" || Name == "SUB" || Name == "MUL" || Name == "AND" || Name == "OR" || Name == "XOR" || Name == "SLL" || Name == "SRL" || Name == "SRA"; if (!ImmediateForm && !RegisterForm && !Descriptor->Flags.has(InstructionFlag::ProducesCode)) { continue; } if (!ImmediateForm && !RegisterForm) { return makeUnexpected(ErrorCode::Unsupported, "semantic subset does not model opcode '" + std::string(Name) + "'"); } auto Destination = registerAt(*Instruction, 0); auto LeftRegister = registerAt(*Instruction, 1); if (!Destination || !LeftRegister) return makeUnexpected(ErrorCode::Unsupported, "unsupported binary instruction form"); const std::uint64_t Left = readRegister(*LeftRegister, Values, Zero); std::uint64_t Right = 0; if (ImmediateForm) { auto Immediate = immediateAt(*Instruction, 2); if (!Immediate) return makeUnexpected(ErrorCode::Unsupported, "unsupported immediate instruction form"); Right = *Immediate; } else { auto RightRegister = registerAt(*Instruction, 2); if (!RightRegister) return makeUnexpected(ErrorCode::Unsupported, "unsupported register instruction form"); Right = readRegister(*RightRegister, Values, Zero); } std::uint64_t Value = 0; if (Name == "G_ADD" || Name == "ADD" || Name == "ADDI") Value = Left + Right; else if (Name == "G_SUB" || Name == "SUB") Value = Left - Right; else if (Name == "G_MUL" || Name == "MUL") Value = Left * Right; else if (Name == "G_AND" || Name == "AND" || Name == "ANDI") Value = Left & Right; else if (Name == "G_OR" || Name == "OR" || Name == "ORI") Value = Left | Right; else if (Name == "G_XOR" || Name == "XOR" || Name == "XORI") Value = Left ^ Right; else if (Name == "G_SHL" || Name == "SLL" || Name == "SLLI") Value = Left << (Right & 63U); else if (Name == "G_LSHR" || Name == "SRL" || Name == "SRLI") Value = Left >> (Right & 63U); else Value = static_cast(std::bit_cast(Left) >> (Right & 63U)); Values[Destination->opaqueValue()] = Value; } return makeUnexpected(ErrorCode::Unsupported, "semantic subset did not encounter a return"); } [[nodiscard]] Expected semanticCase(std::string_view Canonical, const TargetDescription &Target, const BackendInvocation &Invocation) { auto Original = parseAndCanonicalizeMIR("semantic-input.mir", std::string(Canonical), Target, Invocation); if (!Original) return std::unexpected(Original.error()); const MachineFunction *InputFunction = Original->Document.unit().functionAt(0); if (InputFunction == nullptr) return std::unexpected(Error(ErrorCode::InternalError, "semantic fuzz input has no function")); auto ExpectedValue = interpretStraightLine(*InputFunction, Target); if (!ExpectedValue && ExpectedValue.error().code() == ErrorCode::Unsupported) return false; if (!ExpectedValue) return std::unexpected(ExpectedValue.error()); auto CompiledInput = parseAndCanonicalizeMIR("semantic-pipeline.mir", std::string(Canonical), Target, Invocation); if (!CompiledInput) return std::unexpected(CompiledInput.error()); auto Compiled = runBackend(std::move(*CompiledInput), Target, Invocation, ArtifactKind::FinalMIR); if (!Compiled) return std::unexpected(Compiled.error()); auto Final = parseAndCanonicalizeMIR("semantic-final.mir", std::string(Compiled->text()), Target, Invocation); if (!Final) return std::unexpected(Final.error()); const MachineFunction *FinalFunction = Final->Document.unit().functionAt(0); if (FinalFunction == nullptr) return std::unexpected(Error(ErrorCode::InternalError, "semantic final MIR has no function")); auto ActualValue = interpretStraightLine(*FinalFunction, Target); if (!ActualValue && ActualValue.error().code() == ErrorCode::Unsupported) return false; if (!ActualValue) return std::unexpected(ActualValue.error()); if (*ExpectedValue != *ActualValue) { return makeUnexpected(ErrorCode::FuzzingFailure, "restricted semantic comparison changed the integer return value from " + std::to_string(*ExpectedValue) + " to " + std::to_string(*ActualValue)); } return true; } } // namespace std::size_t MIRFuzzResult::executed() const noexcept { std::size_t Result = 0; for (const MIRFuzzLayerResult &Layer : Layers) Result += Layer.Executed; return Result; } std::size_t MIRFuzzResult::accepted() const noexcept { std::size_t Result = 0; for (const MIRFuzzLayerResult &Layer : Layers) Result += Layer.Accepted; return Result; } std::size_t MIRFuzzResult::rejected() const noexcept { std::size_t Result = 0; for (const MIRFuzzLayerResult &Layer : Layers) Result += Layer.Rejected; return Result; } Expected fuzzMIR(std::string SourceName, std::string SourceText, BackendInvocation Invocation, const TargetDescription &Target, MIRFuzzOptions Options) { if (Options.Iterations == 0 || Options.MaximumInputSize == 0) { return makeUnexpected(ErrorCode::InvalidArgument, "MIR fuzzing requires nonzero iteration and input-size limits"); } if (std::ranges::any_of(Options.Layers, [](MIRFuzzLayer Layer) { return spelling(Layer) == "invalid"; })) { return makeUnexpected(ErrorCode::InvalidArgument, "MIR fuzz layer is not recognized"); } auto InvocationVerification = Invocation.canonicalizeAndVerify(); if (!InvocationVerification) return std::unexpected(InvocationVerification.error()); auto Input = parseAndCanonicalizeMIR(std::move(SourceName), std::move(SourceText), Target, Invocation); if (!Input) return std::unexpected(Input.error()); if (Input->Text.size() > Options.MaximumInputSize) return makeUnexpected(ErrorCode::InvalidArgument, "MIR fuzz seed exceeds the input-size limit"); if (Options.Layers.empty()) Options.Layers = defaultLayers(); std::ranges::sort(Options.Layers); if (std::ranges::adjacent_find(Options.Layers) != Options.Layers.end()) { return makeUnexpected(ErrorCode::InvalidArgument, "MIR fuzz layers must be unique"); } MIRFuzzResult Result{.Seed = Options.Seed, .Layers = {}}; DeterministicRandom Random(Options.Seed); for (MIRFuzzLayer Layer : Options.Layers) { MIRFuzzLayerResult LayerResult{.Layer = Layer}; const std::size_t Iterations = Layer == MIRFuzzLayer::RestrictedSemantics ? 1U : Options.Iterations; for (std::size_t Iteration = 0; Iteration < Iterations; ++Iteration) { ++LayerResult.Executed; Expected Case = false; switch (Layer) { case MIRFuzzLayer::Parser: Case = parserCase(Input->Text, Random, Target, Invocation, Options.MaximumInputSize); break; case MIRFuzzLayer::BuilderStructural: Case = builderCase(Input->Text, Target, Invocation); break; case MIRFuzzLayer::PropertyStage: Case = propertyCase(Input->Text, Random, Target, Invocation); break; case MIRFuzzLayer::Mutation: Case = mutationCase(Input->Text, Random, Target, Invocation); break; case MIRFuzzLayer::Pipeline: Case = pipelineCase(Input->Text, Random, Target, Invocation); break; case MIRFuzzLayer::RestrictedSemantics: Case = semanticCase(Input->Text, Target, Invocation); break; } if (!Case) return std::unexpected(fuzzFailure(Layer, Iteration, Case.error())); if (*Case) ++LayerResult.Accepted; else ++LayerResult.Rejected; } Result.Layers.push_back(LayerResult); } return Result; } std::string_view spelling(MIRFuzzLayer Layer) noexcept { switch (Layer) { case MIRFuzzLayer::Parser: return "parser"; case MIRFuzzLayer::BuilderStructural: return "builder"; case MIRFuzzLayer::PropertyStage: return "property-stage"; case MIRFuzzLayer::Mutation: return "mutation"; case MIRFuzzLayer::Pipeline: return "pipeline"; case MIRFuzzLayer::RestrictedSemantics: return "semantics"; } return "invalid"; } Expected parseMIRFuzzLayer(std::string_view Value) { if (Value == "parser") return MIRFuzzLayer::Parser; if (Value == "builder") return MIRFuzzLayer::BuilderStructural; if (Value == "property-stage") return MIRFuzzLayer::PropertyStage; if (Value == "mutation") return MIRFuzzLayer::Mutation; if (Value == "pipeline") return MIRFuzzLayer::Pipeline; if (Value == "semantics") return MIRFuzzLayer::RestrictedSemantics; return makeUnexpected(ErrorCode::InvalidInvocation, "unknown MIR fuzz layer"); } } // namespace rcc::tooling