From e6cf7a08a95222866dd914f7f65c2457c7daca6d Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 6 Aug 2024 11:39:09 -0400 Subject: [PATCH 01/13] Add aws-smithy-fuzz utility crate --- build.gradle.kts | 4 + codegen-client/build.gradle.kts | 1 + .../codegen/core/smithy/CodegenDelegator.kt | 44 +- .../smithy/generators/CargoTomlGenerator.kt | 1 + .../smithy/rust/codegen/core/testutil/Rust.kt | 1 + .../smithy/rust/codegen/core/util/Exec.kt | 6 +- fuzzgen/build.gradle.kts | 112 +++ .../codegen/fuzz/FuzzHarnessBuildPlugin.kt | 287 +++++++ .../rust/codegen/fuzz/FuzzTargetContext.kt | 168 ++++ ...ware.amazon.smithy.build.SmithyBuildPlugin | 5 + .../fuzz/FuzzHarnessBuildPluginTest.kt | 62 ++ gradle.properties | 2 + rust-runtime/Cargo.lock | 583 +++++++++++++- rust-runtime/Cargo.toml | 1 + rust-runtime/aws-smithy-fuzz/Cargo.toml | 45 ++ rust-runtime/aws-smithy-fuzz/LICENSE | 175 +++++ rust-runtime/aws-smithy-fuzz/README.md | 166 ++++ rust-runtime/aws-smithy-fuzz/src/lib.rs | 203 +++++ rust-runtime/aws-smithy-fuzz/src/main.rs | 737 ++++++++++++++++++ rust-runtime/aws-smithy-fuzz/src/types.rs | 157 ++++ .../templates/smithy-build-fuzzer.jinja2 | 39 + .../templates/smithy-build-targetcrate.jinja2 | 37 + settings.gradle.kts | 1 + 23 files changed, 2810 insertions(+), 27 deletions(-) create mode 100644 fuzzgen/build.gradle.kts create mode 100644 fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt create mode 100644 fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt create mode 100644 fuzzgen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin create mode 100644 fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt create mode 100644 rust-runtime/aws-smithy-fuzz/Cargo.toml create mode 100644 rust-runtime/aws-smithy-fuzz/LICENSE create mode 100644 rust-runtime/aws-smithy-fuzz/README.md create mode 100644 rust-runtime/aws-smithy-fuzz/src/lib.rs create mode 100644 rust-runtime/aws-smithy-fuzz/src/main.rs create mode 100644 rust-runtime/aws-smithy-fuzz/src/types.rs create mode 100644 rust-runtime/aws-smithy-fuzz/templates/smithy-build-fuzzer.jinja2 create mode 100644 rust-runtime/aws-smithy-fuzz/templates/smithy-build-targetcrate.jinja2 diff --git a/build.gradle.kts b/build.gradle.kts index 5e11e0ab02b..0d8adb08d09 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ + buildscript { repositories { mavenCentral() @@ -14,6 +15,7 @@ buildscript { } } + allprojects { val allowLocalDeps: String by project repositories { @@ -23,8 +25,10 @@ allprojects { mavenCentral() google() } + } + val ktlint by configurations.creating val ktlintVersion: String by project diff --git a/codegen-client/build.gradle.kts b/codegen-client/build.gradle.kts index 3e1f1ec580b..5954994aba3 100644 --- a/codegen-client/build.gradle.kts +++ b/codegen-client/build.gradle.kts @@ -18,6 +18,7 @@ group = "software.amazon.smithy.rust.codegen" version = "0.1.0" val smithyVersion: String by project +val smithyRsVersion: String by project dependencies { implementation(project(":codegen-core")) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt index 9c72dd2185c..f921cf717cf 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt @@ -66,6 +66,29 @@ interface ModuleDocProvider { fun docsWriter(module: RustModule.LeafModule): Writable? } +open class RustCrate( + fileManifest: FileManifest, + private val symbolProvider: RustSymbolProvider, + coreCodegenConfig: CoreCodegenConfig, + moduleDocProvider: ModuleDocProvider, +): BasicRustCrate(fileManifest, symbolProvider, coreCodegenConfig, moduleDocProvider) { + /** + * Write into the module that this shape is [locatedIn] + */ + fun useShapeWriter( + shape: Shape, + f: Writable, + ) { + val module = symbolProvider.toSymbol(shape).module() + check(!module.isInline()) { + "Cannot use useShapeWriter with inline modules—use [RustWriter.withInlineModule] instead" + } + withModule(symbolProvider.toSymbol(shape).module(), f) + } + +} + + /** * RustCrate abstraction. * @@ -83,7 +106,7 @@ interface ModuleDocProvider { * shape locations are determined. * 2. [finalize]: Write the crate out to the file system, generating a lib.rs and Cargo.toml */ -open class RustCrate( +open class BasicRustCrate( fileManifest: FileManifest, private val symbolProvider: SymbolProvider, coreCodegenConfig: CoreCodegenConfig, @@ -95,19 +118,6 @@ open class RustCrate( // used to ensure we never create accidentally discard docs / incorrectly create modules with incorrect visibility private var duplicateModuleWarningSystem: MutableMap = mutableMapOf() - /** - * Write into the module that this shape is [locatedIn] - */ - fun useShapeWriter( - shape: Shape, - f: Writable, - ) { - val module = symbolProvider.toSymbol(shape).module() - check(!module.isInline()) { - "Cannot use useShapeWriter with inline modules—use [RustWriter.withInlineModule] instead" - } - withModule(symbolProvider.toSymbol(shape).module(), f) - } /** * Write directly into lib.rs @@ -189,7 +199,7 @@ open class RustCrate( fun withModule( module: RustModule, moduleWriter: Writable, - ): RustCrate { + ) { when (module) { is RustModule.LibRs -> lib { moduleWriter(this) } is RustModule.LeafModule -> { @@ -214,7 +224,7 @@ open class RustCrate( } } } - return this + //return this } /** @@ -223,7 +233,7 @@ open class RustCrate( fun moduleFor( shape: Shape, moduleWriter: Writable, - ): RustCrate = withModule((symbolProvider as RustSymbolProvider).moduleForShape(shape), moduleWriter) + ) = withModule((symbolProvider as RustSymbolProvider).moduleForShape(shape), moduleWriter) /** * Create a new file directly diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt index e5f49163489..cebdfbfab7d 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt @@ -113,6 +113,7 @@ class CargoTomlGenerator( "features" to cargoFeatures.toMap(), ).deepMergeWith(manifestCustomizations) + println(cargoToml) writer.writeWithNoFormatting(TomlWriter().write(cargoToml)) } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt index 4427852d427..b1287f72679 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt @@ -360,6 +360,7 @@ fun RustCrate.testModule(block: Writable) = } fun FileManifest.printGeneratedFiles() { + println("Generated files:") this.files.forEach { path -> println("file:///$path") } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt index 296d7bc39f2..0e2039d115e 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt @@ -16,15 +16,17 @@ fun String.runCommand( workdir: Path? = null, environment: Map = mapOf(), timeout: Long = 3600, + redirect: ProcessBuilder.Redirect = ProcessBuilder.Redirect.PIPE ): String { val logger = Logger.getLogger("RunCommand") logger.fine("Invoking comment $this in `$workdir` with env $environment") + println("Invoking comment $this in `$workdir` with env $environment") val start = System.currentTimeMillis() val parts = this.split("\\s".toRegex()) val builder = ProcessBuilder(*parts.toTypedArray()) - .redirectOutput(ProcessBuilder.Redirect.PIPE) - .redirectError(ProcessBuilder.Redirect.PIPE) + .redirectOutput(redirect) + .redirectError(redirect) .letIf(workdir != null) { it.directory(workdir?.toFile()) } diff --git a/fuzzgen/build.gradle.kts b/fuzzgen/build.gradle.kts new file mode 100644 index 00000000000..910e874da79 --- /dev/null +++ b/fuzzgen/build.gradle.kts @@ -0,0 +1,112 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + +plugins { + kotlin("jvm") + `maven-publish` +} + +description = "Plugin to generate a fuzz harness" +extra["displayName"] = "Smithy :: Rust :: Fuzzer Generation" +extra["moduleName"] = "software.amazon.smithy.rust.codegen.client" + +group = "software.amazon.smithy.rust.codegen.serde" +version = "0.1.0" + +val smithyVersion: String by project + +dependencies { + implementation(project(":codegen-core")) + implementation(project(":codegen-client")) + implementation(project(":codegen-server")) + implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.compileKotlin { + kotlinOptions.jvmTarget = "11" +} + +// Reusable license copySpec +val licenseSpec = copySpec { + from("${project.rootDir}/LICENSE") + from("${project.rootDir}/NOTICE") +} + +// Configure jars to include license related info +tasks.jar { + metaInf.with(licenseSpec) + inputs.property("moduleName", project.name) + manifest { + attributes["Automatic-Module-Name"] = project.name + } +} + +val sourcesJar by tasks.creating(Jar::class) { + group = "publishing" + description = "Assembles Kotlin sources jar" + archiveClassifier.set("sources") + from(sourceSets.getByName("main").allSource) +} + +val isTestingEnabled: String by project +if (isTestingEnabled.toBoolean()) { + val kotestVersion: String by project + + dependencies { + runtimeOnly(project(":rust-runtime")) + testImplementation("org.junit.jupiter:junit-jupiter:5.6.1") + testImplementation("software.amazon.smithy:smithy-validation-model:$smithyVersion") + testImplementation("software.amazon.smithy:smithy-aws-protocol-tests:1.48.0") + testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") + } + + tasks.compileTestKotlin { + kotlinOptions.jvmTarget = "11" + } + + tasks.register("generateClasspath") { + doLast { + // Get the runtime classpath + val runtimeClasspath = sourceSets["main"].runtimeClasspath + + // Add the 'libs' directory to the classpath + val libsDir = file(layout.buildDirectory.dir("libs")) + val fullClasspath = runtimeClasspath + files(libsDir.listFiles()) + + // Convert to classpath string + val classpath = fullClasspath.asPath + println(classpath) + } + } + + + tasks.test { + useJUnitPlatform() + testLogging { + events("failed") + exceptionFormat = TestExceptionFormat.FULL + showCauses = true + showExceptions = true + showStackTraces = true + } + } +} + +publishing { + publications { + create("default") { + from(components["java"]) + artifact(sourcesJar) + } + } + repositories { maven { url = uri(layout.buildDirectory.dir("repository")) } } +} diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt new file mode 100644 index 00000000000..27defb93fe5 --- /dev/null +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt @@ -0,0 +1,287 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.fuzz + +import software.amazon.smithy.build.FileManifest +import software.amazon.smithy.build.PluginContext +import software.amazon.smithy.build.SmithyBuildPlugin +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.NullableIndex +import software.amazon.smithy.model.knowledge.TopDownIndex +import software.amazon.smithy.model.neighbor.Walker +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.NumberNode +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.model.node.StringNode +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait +import software.amazon.smithy.model.traits.HttpQueryTrait +import software.amazon.smithy.model.traits.HttpTrait +import software.amazon.smithy.model.traits.JsonNameTrait +import software.amazon.smithy.model.traits.XmlNameTrait +import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.rust.codegen.core.rustlang.RustType +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.smithy.BasicRustCrate +import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig +import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings +import software.amazon.smithy.rust.codegen.core.smithy.ModuleDocProvider +import software.amazon.smithy.rust.codegen.core.smithy.ModuleProviderContext +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProviderConfig +import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor +import software.amazon.smithy.rust.codegen.core.smithy.WrappingSymbolProvider +import software.amazon.smithy.rust.codegen.core.smithy.mapRustType +import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer +import software.amazon.smithy.rust.codegen.core.util.getTrait +import software.amazon.smithy.rust.codegen.core.util.orNull +import software.amazon.smithy.rust.codegen.server.smithy.ServerModuleProvider +import software.amazon.smithy.rust.codegen.server.smithy.transformers.AttachValidationExceptionToConstrainedOperationInputsInAllowList +import java.nio.file.Path +import kotlin.streams.toList + +data class FuzzTarget(val name: String, val relativePath: String) { + companion object { + fun fromNode(node: ObjectNode): FuzzTarget { + val name = node.expectStringMember("name").value + val relativePath = node.expectStringMember("relativePath").value + return FuzzTarget(name, relativePath) + } + } + + fun targetPackage(): String { + val path = Path.of(relativePath) + val cargoToml = path.resolve("Cargo.toml").toFile() + val packageSection = cargoToml.readLines().dropWhile { it.trim() != "[package]" } + return packageSection.firstOrNull { it.startsWith("name =") }?.let { it.split("=")[1].trim() }?.trim('"') + ?: throw Exception("no package name") + } +} + +data class FuzzSettings( + val targetCratePath: List, + val service: ShapeId, + val runtimeConfig: RuntimeConfig, +) { + companion object { + fun fromNode(node: ObjectNode): FuzzSettings { + val targetCrates = node.expectArrayMember("targetCrates").map { FuzzTarget.fromNode(it.expectObjectNode()) } + val service = ShapeId.fromNode(node.expectStringMember("service")) + val runtimeConfig = RuntimeConfig.fromNode(node.getObjectMember("runtimeConfig")) + return FuzzSettings(targetCrates, service, runtimeConfig) + } + } +} + +class FuzzHarnessBuildPlugin : SmithyBuildPlugin { + override fun getName(): String = "fuzz-harness" + + override fun execute(context: PluginContext) { + val fuzzSettings = FuzzSettings.fromNode(context.settings) + + val subdir = FileManifest.create(context.fileManifest.resolvePath(Path.of("driver"))) + val model = + context.model.let(OperationNormalizer::transform) + .let(AttachValidationExceptionToConstrainedOperationInputsInAllowList::transform) + val targets = + fuzzSettings.targetCratePath.map { target -> + val target = createFuzzTarget(target, context.fileManifest, fuzzSettings, model) + FuzzTargetGenerator(target).generateFuzzTarget() + target + } + + createDriver(model, context.fileManifest, fuzzSettings) + + targets.forEach { + context.fileManifest.addAllFiles(it.finalize()) + } + } +} + +fun driverSettings( + service: ShapeId, + runtimeConfig: RuntimeConfig, +) = CoreRustSettings( + service, + moduleVersion = "0.1.0", + moduleName = "fuzz-driver", + moduleAuthors = listOf(), + codegenConfig = CoreCodegenConfig(), + license = null, + runtimeConfig = runtimeConfig, + moduleDescription = null, + moduleRepository = null, +) + +fun corpus( + model: Model, + fuzzSettings: FuzzSettings, +): ArrayNode { + val operations = TopDownIndex.of(model).getContainedOperations(fuzzSettings.service) + val protocolTests = operations.flatMap { it.getTrait()?.testCases ?: listOf() } + val out = ArrayNode.builder() + protocolTests.forEach { testCase -> + out.withValue( + ObjectNode.objectNode() + .withMember("uri", testCase.uri) + .withMember("method", testCase.method) + .withMember( + "headers", + ObjectNode.objectNode( + testCase.headers.map { (k, v) -> + (StringNode.from(k) to ArrayNode.fromStrings(v)) + }.toMap(), + ), + ) + .withMember("trailers", ObjectNode.objectNode()) + .withMember( + "body", + ArrayNode.fromNodes( + testCase.body.orNull()?.chars()?.toList()?.map { c -> NumberNode.from(c) } + ?: listOf(), + ), + ), + ) + } + return out.build() +} + +fun createDriver( + model: Model, + baseManifest: FileManifest, + fuzzSettings: FuzzSettings, +) { + val fuzzLexicon = + ObjectNode.objectNode() + .withMember("corpus", corpus(model, fuzzSettings)) + .withMember("dictionary", dictionary(model, fuzzSettings)) + baseManifest.writeFile("lexicon.json", Node.prettyPrintJson(fuzzLexicon)) +} + +fun dictionary( + model: Model, + fuzzSettings: FuzzSettings, +): ArrayNode { + val operations = TopDownIndex.of(model).getContainedOperations(fuzzSettings.service) + val walker = Walker(model) + val dictionary = mutableSetOf() + operations.forEach { + walker.iterateShapes(it).forEach { shape -> + dictionary.addAll(getTraitBasedNames(shape)) + dictionary.add(shape.id.name) + when (shape) { + is MemberShape -> dictionary.add(shape.memberName) + is OperationShape -> dictionary.add(shape.id.toString()) + else -> {} + } + } + } + return ArrayNode.fromStrings(dictionary.toList().sorted()) +} + +fun getTraitBasedNames(shape: Shape): List { + return listOfNotNull( + shape.getTrait()?.value, + shape.getTrait()?.value, + shape.getTrait()?.value, + shape.getTrait()?.method, + *( + shape.getTrait()?.uri?.queryLiterals?.flatMap { (k, v) -> listOf(k, v) } + ?: listOf() + ).toTypedArray(), + shape.getTrait()?.value, + ) +} + +fun createFuzzTarget( + target: FuzzTarget, + baseManifest: FileManifest, + fuzzSettings: FuzzSettings, + model: Model, +): FuzzTargetContext { + val newManifest = FileManifest.create(baseManifest.resolvePath(Path.of(target.name))) + val codegenConfig = CoreCodegenConfig() + val symbolProvider = + SymbolVisitor( + rustSettings(fuzzSettings, target), + model, + model.expectShape(fuzzSettings.service, ServiceShape::class.java), + RustSymbolProviderConfig( + fuzzSettings.runtimeConfig, + renameExceptions = false, + NullableIndex.CheckMode.SERVER, + ServerModuleProvider, + ), + ).let { PublicCrateSymbolProvider("rust_server_codegen", it) } + val crate = + BasicRustCrate( + newManifest, + symbolProvider, + codegenConfig, + DocProvider(), + ) + return FuzzTargetContext( + target = target, + fuzzSettings = fuzzSettings, + rustCrate = crate, + model = model, + manifest = newManifest, + symbolProvider = symbolProvider, + ) +} + +class DocProvider : ModuleDocProvider { + override fun docsWriter(module: RustModule.LeafModule): Writable? { + return null + } +} + +class NoOpVisitor : RustSymbolProvider { + override val model: Model + get() = TODO("Not yet implemented") + override val moduleProviderContext: ModuleProviderContext + get() = TODO("Not yet implemented") + override val config: RustSymbolProviderConfig + get() = TODO("Not yet implemented") + + override fun symbolForOperationError(operation: OperationShape): Symbol { + TODO("Not yet implemented") + } + + override fun symbolForEventStreamError(eventStream: UnionShape): Symbol { + TODO("Not yet implemented") + } + + override fun symbolForBuilder(shape: Shape): Symbol { + TODO("Not yet implemented") + } + + override fun toSymbol(shape: Shape?): Symbol { + TODO("Not yet implemented") + } +} + +class PublicCrateSymbolProvider(private val crateName: String, private val base: RustSymbolProvider) : + WrappingSymbolProvider(base) { + override fun toSymbol(shape: Shape): Symbol { + val base = base.toSymbol(shape) + return base.mapRustType { ty -> + when (ty) { + is RustType.Opaque -> RustType.Opaque(ty.name, ty.namespace?.replace("crate", crateName)) + else -> ty + } + } + } +} diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt new file mode 100644 index 00000000000..2d76b6d500a --- /dev/null +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt @@ -0,0 +1,168 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.fuzz + +import software.amazon.smithy.build.FileManifest +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.TopDownIndex +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.Local +import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.BasicRustCrate +import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig +import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.core.smithy.contextName +import software.amazon.smithy.rust.codegen.core.util.hasStreamingMember +import software.amazon.smithy.rust.codegen.core.util.inputShape +import software.amazon.smithy.rust.codegen.core.util.isEventStream +import software.amazon.smithy.rust.codegen.core.util.outputShape +import software.amazon.smithy.rust.codegen.core.util.toPascalCase +import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import software.amazon.smithy.rust.codegen.server.smithy.isDirectlyConstrained +import java.nio.file.Path +import kotlin.io.path.name + +fun rustSettings( + fuzzSettings: FuzzSettings, + target: FuzzTarget, +) = CoreRustSettings( + fuzzSettings.service, + moduleVersion = "0.1.0", + moduleName = "fuzz-target-${target.name}", + moduleAuthors = listOf(), + codegenConfig = CoreCodegenConfig(), + license = null, + runtimeConfig = fuzzSettings.runtimeConfig, + moduleDescription = null, + moduleRepository = null, +) + +data class FuzzTargetContext( + val target: FuzzTarget, + val fuzzSettings: FuzzSettings, + val rustCrate: BasicRustCrate, + val model: Model, + val symbolProvider: RustSymbolProvider, + private val manifest: FileManifest, +) { + fun finalize(): FileManifest { + val forceWorkspace = + mapOf("workspace" to listOf("_ignored" to "_ignored").toMap(), "lib" to mapOf("crate-type" to listOf("cdylib"))) + val rustSettings = rustSettings(fuzzSettings, target) + rustCrate.finalize(rustSettings, model, forceWorkspace, listOf(), requireDocs = false) + return manifest + } +} + +class FuzzTargetGenerator(private val context: FuzzTargetContext) { + private val model = context.model + private val serviceShape = context.model.expectShape(context.fuzzSettings.service, ServiceShape::class.java) + private val symbolProvider = context.symbolProvider + + private fun targetCrate(): RuntimeType { + val path = Path.of(context.target.relativePath).toAbsolutePath() + return CargoDependency( + path.name, + Local(path.parent?.toString() ?: ""), + `package` = context.target.targetPackage(), + ).toType() + } + + private val smithyFuzz = context.fuzzSettings.runtimeConfig.smithyRuntimeCrate("smithy-fuzz").toType() + private val ctx = + arrayOf( + "fuzz_harness" to smithyFuzz.resolve("fuzz_harness"), + "fuzz_service" to smithyFuzz.resolve("fuzz_service"), + "FuzzResult" to smithyFuzz.resolve("FuzzResult"), + "Body" to smithyFuzz.resolve("Body"), + "http" to CargoDependency.Http.toType(), + "target" to targetCrate(), + ) + + private val serviceName = context.fuzzSettings.service.name.toPascalCase() + + fun generateFuzzTarget() { + context.rustCrate.lib { + rustTemplate( + """ + #{fuzz_harness}!(|tx| { + let config = #{target}::${serviceName}Config::builder().build(); + #{tx_clones} + #{target}::$serviceName::builder::<#{Body}, _, _, _>(config)#{all_operations}.build_unchecked() + }); + + """, + *ctx, + "all_operations" to allOperations(), + "tx_clones" to allTxs(), + *preludeScope, + ) + } + } + + private fun operationsToImplement(): List { + val index = TopDownIndex.of(model) + return index.getContainedOperations(serviceShape).filter { operationShape -> + // TODO(fuzzing): consider if it is possible to support event streams + !operationShape.isEventStream(model) && + // TODO(fuzzing): it should be possible to support normal streaming operations + !( + operationShape.inputShape(model).hasStreamingMember(model) || + operationShape.outputShape(model) + .hasStreamingMember(model) + ) && + // TODO(fuzzing): it should be possible to work backwards from constraints to satisfy them in most cases. + !(operationShape.outputShape(model).isDirectlyConstrained(symbolProvider)) + }.toList() + } + + private fun allTxs(): Writable = + writable { + operationsToImplement().forEach { op -> + val operationName = op.contextName(serviceShape).toSnakeCase().let { RustReservedWords.escapeIfNeeded(it) } + rust("let tx_$operationName = tx.clone();") + } + } + + private fun allOperations(): Writable = + writable { + val operations = operationsToImplement() + operations.forEach { op -> + val operationName = op.contextName(serviceShape).toSnakeCase().let { RustReservedWords.escapeIfNeeded(it) } + val output = + writable { + val outputSymbol = symbolProvider.toSymbol(op.outputShape(model)) + if (op.errors.isEmpty()) { + rust("#T::builder().build()", outputSymbol) + } else { + rust("Ok(#T::builder().build())", outputSymbol) + } + } + rustTemplate( + """ + .$operationName(move |input: #{Input}| { + let tx = tx_$operationName.clone(); + async move { + tx.send(format!("{:?}", input)).await.unwrap(); + #{output} + } + })""", + "Input" to symbolProvider.toSymbol(op.inputShape(model)), + "output" to output, + *preludeScope, + ) + } + } +} diff --git a/fuzzgen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/fuzzgen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin new file mode 100644 index 00000000000..6dc89961745 --- /dev/null +++ b/fuzzgen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin @@ -0,0 +1,5 @@ +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# +software.amazon.smithy.rust.codegen.fuzz.FuzzHarnessBuildPlugin diff --git a/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt b/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt new file mode 100644 index 00000000000..124e221730e --- /dev/null +++ b/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.rust.codegen.fuzz + +import org.junit.jupiter.api.Test +import software.amazon.smithy.build.FileManifest +import software.amazon.smithy.build.PluginContext +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig +import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.printGeneratedFiles +import java.io.File + +class FuzzHarnessBuildPluginTest() { + fun actualServerCodegenPath( + model: String, + extra: String, + ) = + File("../../smithy-rs$extra/codegen-server-test/build/smithyprojections/codegen-server-test/$model/rust-server-codegen").absolutePath + + @Test + fun works() { + val model = "namespace empty".asSmithyModel() + val testDir = TestWorkspace.subproject() + val testPath = testDir.toPath() + val manifest = FileManifest.create(testPath) + val modelName = "rest_json" + val context = + PluginContext.builder() + .model(model) + .fileManifest(manifest) + .settings( + ObjectNode.objectNode() + .withMember("service", "aws.protocoltests.restjson#RestJson") + .withMember( + "targetCrates", + ArrayNode.arrayNode( + ObjectNode.objectNode().withMember("relativePath", actualServerCodegenPath(modelName, "2")) + .withMember("name", "a"), + ObjectNode.objectNode().withMember("relativePath", actualServerCodegenPath(modelName, "3")) + .withMember("name", "b"), + ), + ) + .withMember( + "runtimeConfig", + Node.objectNode().withMember( + "relativePath", + Node.from(((TestRuntimeConfig).runtimeCrateLocation).path), + ), + ), + ).build() + FuzzHarnessBuildPlugin().execute(context) + context.fileManifest.printGeneratedFiles() + print("cd ${context.fileManifest.baseDir.resolve("driver")} && cargo check") + // "cargo check".runCommand(context.fileManifest.baseDir.resolve("a")) + } +} diff --git a/gradle.properties b/gradle.properties index 4cfe1db1f78..3cd33b6172c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,3 +24,5 @@ ktlintVersion=1.0.1 kotestVersion=5.8.0 # Avoid registering dependencies/plugins/tasks that are only used for testing purposes isTestingEnabled=true + +smithyRsVersion=0.1.0 diff --git a/rust-runtime/Cargo.lock b/rust-runtime/Cargo.lock index e552821839f..5b1a36c7835 100644 --- a/rust-runtime/Cargo.lock +++ b/rust-runtime/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "afl" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b784d6332a6978dd29861676de9df37aa37ed8852341db6340bd75eb82bc7a69" +dependencies = [ + "home", + "libc", + "rustc_version", + "xdg", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -32,18 +44,76 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "approx" version = "0.5.1" @@ -58,6 +128,9 @@ name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "assert-json-diff" @@ -413,6 +486,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-smithy-fuzz" +version = "0.1.0" +dependencies = [ + "afl", + "arbitrary", + "aws-smithy-http-server", + "bincode", + "bytes", + "cargo_toml", + "clap 4.5.23", + "ffi-support", + "futures", + "glob", + "http 0.2.12", + "http-body 0.4.6", + "lazy_static", + "libloading", + "serde", + "serde_json", + "tera", + "termcolor", + "tokio", + "tower", + "tracing", + "tracing-subscriber", +] + [[package]] name = "aws-smithy-http" version = "0.60.11" @@ -485,7 +586,7 @@ dependencies = [ "pretty_assertions", "regex", "serde_urlencoded", - "thiserror", + "thiserror 1.0.69", "tokio", "tower", "tower-http", @@ -519,7 +620,7 @@ dependencies = [ "rustls-pemfile", "signal-hook", "socket2", - "thiserror", + "thiserror 1.0.69", "tls-listener", "tokio", "tokio-rustls 0.24.1", @@ -578,7 +679,7 @@ dependencies = [ "regex-lite", "roxmltree", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -597,7 +698,7 @@ dependencies = [ "regex-lite", "roxmltree", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -896,6 +997,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.69.5" @@ -964,6 +1074,16 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -995,6 +1115,16 @@ dependencies = [ "either", ] +[[package]] +name = "cargo_toml" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88da5a13c620b4ca0078845707ea9c3faf11edbc3ffd8497d11d686211cd1ac0" +dependencies = [ + "serde", + "toml", +] + [[package]] name = "cast" version = "0.3.0" @@ -1052,7 +1182,32 @@ version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ + "android-tzdata", + "iana-time-zone", "num-traits", + "windows-targets", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", ] [[package]] @@ -1103,7 +1258,7 @@ dependencies = [ "bitflags 1.3.2", "clap_lex 0.2.4", "indexmap 1.9.3", - "strsim", + "strsim 0.10.0", "termcolor", "textwrap", ] @@ -1115,6 +1270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -1123,8 +1279,22 @@ version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ + "anstream", "anstyle", "clap_lex 0.7.4", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.93", ] [[package]] @@ -1151,6 +1321,12 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1363,6 +1539,12 @@ dependencies = [ "syn 2.0.93", ] +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "diff" version = "0.1.13" @@ -1494,6 +1676,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ffi-support" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27838c6815cfe9de2d3aeb145ffd19e565f577414b33f3bdbf42fe040e9e0ff6" +dependencies = [ + "lazy_static", + "log", +] + [[package]] name = "flate2" version = "1.0.35" @@ -1653,6 +1845,30 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.6.0", + "ignore", + "walkdir", +] + [[package]] name = "group" version = "0.12.1" @@ -1729,6 +1945,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1858,6 +2080,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "0.14.32" @@ -1955,6 +2186,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -2094,6 +2348,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.9", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2169,6 +2439,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -2300,6 +2576,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2578,6 +2860,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.15" @@ -2599,6 +2890,89 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.10", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.7" @@ -3336,6 +3710,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3415,6 +3798,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -3424,6 +3813,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -3474,6 +3873,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3532,6 +3937,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3553,7 +3980,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3" +dependencies = [ + "thiserror-impl 2.0.10", ] [[package]] @@ -3567,6 +4003,17 @@ dependencies = [ "syn 2.0.93", ] +[[package]] +name = "thiserror-impl" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -3652,7 +4099,7 @@ dependencies = [ "futures-util", "hyper 0.14.32", "pin-project-lite", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-rustls 0.24.1", ] @@ -3743,6 +4190,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.7.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -3822,7 +4303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.69", "time", "tracing-subscriber", ] @@ -3923,12 +4404,68 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-ident" version = "1.0.14" @@ -3982,6 +4519,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.11.0" @@ -4160,6 +4703,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -4242,6 +4794,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.19.2" @@ -4263,6 +4824,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/rust-runtime/Cargo.toml b/rust-runtime/Cargo.toml index d7853b00996..b724af2c25b 100644 --- a/rust-runtime/Cargo.toml +++ b/rust-runtime/Cargo.toml @@ -8,6 +8,7 @@ members = [ "aws-smithy-compression", "aws-smithy-client", "aws-smithy-eventstream", + "aws-smithy-fuzz", "aws-smithy-http", "aws-smithy-http-auth", "aws-smithy-http-server", diff --git a/rust-runtime/aws-smithy-fuzz/Cargo.toml b/rust-runtime/aws-smithy-fuzz/Cargo.toml new file mode 100644 index 00000000000..315318c8996 --- /dev/null +++ b/rust-runtime/aws-smithy-fuzz/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "aws-smithy-fuzz" +version = "0.1.0" +authors = ["AWS Rust SDK Team "] +description = "Fuzzing utilities for smithy-rs servers" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/smithy-lang/smithy-rs" + +[dependencies] +afl = "0.15.10" +arbitrary = { version = "1.3.2", features = ["derive"] } +bincode = "1" +bytes = "1.7.1" +cargo_toml = "0.20.4" +clap = { version = "4.5.15", features = ["derive"] } +ffi-support = "0.4.4" +futures = "0.3.30" +glob = "0.3.1" +http = "0.2" +http-body = "0.4" +lazy_static = "1.5.0" +libloading = "0.8.5" +serde = { version = "1.0.204", features = ["derive"] } +serde_json = "1.0.124" +tera = "1.20.0" +termcolor = "1.4.1" +tokio = { version = "1.39.2", features = ["sync", "rt", "rt-multi-thread"] } +tower = { version = "0.4.13", features = ["util"] } +tracing = "0.1.40" +tracing-subscriber = "0.3.18" + +[dev-dependencies] +aws-smithy-http-server = { path = "../aws-smithy-http-server" } + + +[profile.release] +debug = true + +[package.metadata.docs.rs] +all-features = true +targets = ["x86_64-unknown-linux-gnu"] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +rustdoc-args = ["--cfg", "docsrs"] +# End of docs.rs metadata diff --git a/rust-runtime/aws-smithy-fuzz/LICENSE b/rust-runtime/aws-smithy-fuzz/LICENSE new file mode 100644 index 00000000000..67db8588217 --- /dev/null +++ b/rust-runtime/aws-smithy-fuzz/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/rust-runtime/aws-smithy-fuzz/README.md b/rust-runtime/aws-smithy-fuzz/README.md new file mode 100644 index 00000000000..60a83efeafc --- /dev/null +++ b/rust-runtime/aws-smithy-fuzz/README.md @@ -0,0 +1,166 @@ +# aws-smithy-fuzz + +AWS Smithy fuzz contains a set of utilities for writing fuzz tests against smithy-rs servers. This is part of our tooling to perform differential fuzzing against different versions of smithy-rs-server. + +## Usage +This contains a library + a CLI tool to fuzz smithy servers. The library allows setting up a given Smithy server implementation as a `cdylib`. This allows two different versions two by dynamically linked at runtime and executed by the fuzzer. + +Each of these components are meant to be usable independently: +1. The public APIs of `aws-smithy-fuzz` can be used to write your own fuzz targets without code generation. +2. The `lexicon.json` can be used outside of this project to seed a fuzzer from a smithy model +3. The fuzz driver can be used on other fuzz targets. + +### Setup +First, you'll need to generate the 1 (or more) versions of a smithy-rs server to test against. The best way to do this is by using the smithy CLI. **This process is fully automated with the `aws-smithy-fuzz setup-smithy`. The following docs are in place in case you want to alter the behavior.** + +There is nothing magic about what `setup-smithy` does, but it does save you some tedious setup. + +```bash +# Create a workspace just to keep track of everything +mkdir workspace && cd workspace +REVISION_1=main +REVISION_2=76d5afb42d545ca2f5cbe90a089681135da935d3 +rm -rf maven-locals && mkdir maven-locals +# Build two different versions of smithy-rs and publish them to two separate local directories +git clone git@github.com:smithy-lang/smithy-rs.git smithy-rs1 && (cd smithy-rs1 && git checkout $REVISION_1 && ./gradlew publishToMavenLocal -Dmaven.repo.local=$(cd ../maven-locals && pwd)/$REVISION_1) +git clone git@github.com:smithy-lang/smithy-rs.git smithy-rs2 && (cd smithy-rs2 && git checkout $REVISION_2 && ./gradlew publishToMavenLocal -Dmaven.repo.local=$(cd ../maven-locals && pwd)/$REVISION_2) +``` + +For each of these, use the smithy CLI to generate a server implementation using something like this: +``` +{ + "version": "1.0", + "maven": { + "dependencies": [ + "software.amazon.smithy.rust.codegen.server.smithy:codegen-server:0.1.0", + "software.amazon.smithy:smithy-aws-protocol-tests:1.50.0" + ], + "repositories": [ + { + "url": "file://maven-locals/" + }, + { + "url": "https://repo1.maven.org/maven2" + } + ] + }, + "projections": { + "server": { + "imports": [ + ], + "plugins": { + "rust-server-codegen": { + "runtimeConfig": { + "relativePath": "/Users/rcoh/code/smithy-rs/rust-runtime" + }, + "codegen": {}, + // PICK YOUR SERVICE + "service": "aws.protocoltests.restjson#RestJson", + "module": "rest_json", + "moduleVersion": "0.0.1", + "moduleDescription": "test", + "moduleAuthors": [ + "protocoltest@example.com" + ] + } + } + } + } +} +``` + +Next, you'll use the `fuzzgen` target to generate two things based on your target crates: +1. A `lexicon.json` file: This uses information from the smithy model to seed the fuzzer with some initial inputs and helps it get better code coverage. +2. Fuzz target shims for your generated servers. These each implement most of the operations available in the smithy model and wire up each target crate with the correct bindings to create a cdylib crate that can be used by the fuzzer. + +The easiest way to use `fuzzgen` is with the Smithy CLI: + +```json +{ + "version": "1.0", + "maven": { + "dependencies": [ + "software.amazon.smithy.rust.codegen.serde:fuzzgen:0.1.0", + "software.amazon.smithy:smithy-aws-protocol-tests:1.50.0" + ] + }, + "projections": { + "harness": { + "imports": [], + "plugins": { + "fuzz-harness": { + "service": "aws.protocoltests.restjson#RestJson", + "runtimeConfig": { + "relativePath": "/Users/rcoh/code/smithy-rs/rust-runtime" + }, + "targetCrates": [ + { + "relativePath": "target-mainline/build/smithy/server/rust-server-codegen/", + "name": "mainline" + }, + { + "relativePath": "target-previous-release/build/smithy/server/rust-server-codegen/", + "name": "previous-release" + } + ] + } + } + } + } +} +``` + +### Initialization and Fuzzing +``` +aws-smithy-fuzz initialize --lexicon --target-crate --target-crate +``` + +> **Important**: These are the crates generated by `fuzzgen`, not the crates you generated for the different smithy versions. + +This may take a couple of minutes as it builds each crate. + +To start the fuzz test use: +``` +aws-smithy-fuzz fuzz +``` + +The fuzz session should start (although AFL may prompt you to run some configuration commands.) + +You should see something like this: +``` + AFL ++4.21c {default} (/Users/rcoh/.cargo/bin/aws-smithy-fuzz) [explore] +┌─ process timing ────────────────────────────────────┬─ overall results ────┐ +│ run time : 0 days, 0 hrs, 36 min, 18 sec │ cycles done : 78 │ +│ last new find : 0 days, 0 hrs, 0 min, 17 sec │ corpus count : 1714 │ +│last saved crash : 0 days, 0 hrs, 19 min, 25 sec │saved crashes : 3 │ +│ last saved hang : none seen yet │ saved hangs : 0 │ +├─ cycle progress ─────────────────────┬─ map coverage┴──────────────────────┤ +│ now processing : 145.173 (8.5%) │ map density : 0.07% / 29.11% │ +│ runs timed out : 0 (0.00%) │ count coverage : 1.52 bits/tuple │ +├─ stage progress ─────────────────────┼─ findings in depth ─────────────────┤ +│ now trying : splice 12 │ favored items : 615 (35.88%) │ +│ stage execs : 11/12 (91.67%) │ new edges on : 802 (46.79%) │ +│ total execs : 38.3M │ total crashes : 6 (3 saved) │ +│ exec speed : 16.0k/sec │ total tmouts : 39 (0 saved) │ +├─ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ───────┤ +│ bit flips : 4/47.6k, 1/47.5k, 2/47.5k │ levels : 27 │ +│ byte flips : 0/5945, 0/5927, 0/5891 │ pending : 2 │ +│ arithmetics : 14/415k, 0/826k, 0/821k │ pend fav : 0 │ +│ known ints : 0/53.4k, 3/224k, 0/329k │ own finds : 1572 │ +│ dictionary : 4/4.38M, 0/4.40M, 4/1.48M, 0/1.48M │ imported : 0 │ +│havoc/splice : 756/13.1M, 184/24.5M │ stability : 96.92% │ +│py/custom/rq : unused, unused, 581/381k, 1/183 ├───────────────────────┘ +│ trim/eff : 32.14%/274k, 99.65% │ [cpu: 62%] +└─ strategy: explore ────────── state: in progress ──┘ +``` +(but with more pretty colors). + +## Replaying Crashes + +Run `aws-smithy-fuzz replay`. This will rerun all the crashes in the crashes folder. Other options exist, see: `aws-smithy-fuzz replay --help`. + +**You can run replay from another terminal while fuzzing is in process.** + + +This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/smithy-lang/smithy-rs) code generator. In most cases, it should not be used directly. + diff --git a/rust-runtime/aws-smithy-fuzz/src/lib.rs b/rust-runtime/aws-smithy-fuzz/src/lib.rs new file mode 100644 index 00000000000..4f8458b494f --- /dev/null +++ b/rust-runtime/aws-smithy-fuzz/src/lib.rs @@ -0,0 +1,203 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Automatically managed default lints */ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +/* End of automatically managed default lints */ +use libloading::{os, Library, Symbol}; +use std::error::Error; +use tokio::sync::mpsc::Sender; + +use futures::task::noop_waker; +use std::future::Future; +use std::path::Path; +use std::pin::pin; +use std::sync::Mutex; +use std::task::{Context, Poll}; +use tower::ServiceExt; +mod types; +pub use lazy_static; +pub use types::{Body, FuzzResult, HttpRequest, HttpResponse}; + +#[macro_export] +/// Defines an extern `process_request` method that can be invoked as a shared library +macro_rules! fuzz_harness { + ($test_function: expr) => { + $crate::lazy_static::lazy_static! { + static ref TARGET: std::sync::Mutex<$crate::LocalFuzzTarget> = $crate::make_target($test_function); + } + + #[no_mangle] + pub extern "C" fn process_request(input: *const u8, len: usize) -> $crate::ByteBuffer { + let slice = unsafe { std::slice::from_raw_parts(input, len) }; + let request = $crate::HttpRequest::from_bytes(slice); + let response = TARGET.lock().unwrap().invoke( + request + .into_http_request_04x() + .expect("input was not a valid HTTP request. Bug in driver."), + ); + $crate::ByteBuffer::from_vec(response.into_bytes()) + } + }; +} + +pub use ::ffi_support::ByteBuffer; +use bytes::Buf; +use tokio::runtime::{Builder, Handle}; +use tower::util::BoxService; + +#[derive(Clone)] +pub struct FuzzTarget(os::unix::Symbol ByteBuffer>); +impl FuzzTarget { + pub fn from_path(path: impl AsRef) -> Self { + let path = path.as_ref(); + eprintln!("loading library from {}", path.display()); + let library = unsafe { Library::new(path).expect("could not load library") }; + let func: Symbol ByteBuffer> = + unsafe { library.get(b"process_request").unwrap() }; + // ensure we never unload the library + let func = unsafe { func.into_raw() }; + std::mem::forget(library); + + Self(func) + } + + pub fn invoke_bytes(&self, input: &[u8]) -> FuzzResult { + let buffer = unsafe { (self.0)(input.as_ptr(), input.len()) }; + let data = buffer.destroy_into_vec(); + FuzzResult::from_bytes(&data) + } + + pub fn invoke(&self, request: &HttpRequest) -> FuzzResult { + let input = request.as_bytes(); + self.invoke_bytes(&input) + } +} + +pub struct LocalFuzzTarget { + service: BoxService, http::Response>, Box>, + rx: tokio::sync::mpsc::Receiver, +} + +impl LocalFuzzTarget { + pub fn invoke(&mut self, request: http::Request) -> FuzzResult { + assert_ready(async move { + let result = ServiceExt::oneshot(&mut self.service, request) + .await + .unwrap(); + let debug = self.rx.try_recv().ok(); + if result.status().is_success() && debug.is_none() { + panic!("success but no debug data received"); + } + let (parts, body) = result.into_parts(); + FuzzResult { + input: debug, + response: HttpResponse { + status: parts.status.as_u16(), + headers: parts + .headers + .iter() + .map(|(key, value)| (key.to_string(), value.to_str().unwrap().to_string())) + .collect(), + body, + }, + } + }) + } +} + +/// Create a target from a tower service. +/// +/// A `Sender` is passed in. The service should send a deterministic string +/// based on the parsed input. +pub fn make_target< + D: Send + Buf, + B: http_body::Body + Send + 'static, + F: Send + 'static, + E: Into>, + T: tower::Service, Response = http::Response, Future = F, Error = E> + + Send + + 'static, +>( + service: impl Fn(Sender) -> T, +) -> Mutex { + let (tx, rx) = tokio::sync::mpsc::channel(1); + let service = + service(tx) + .map_err(|e| e.into()) + .and_then(|resp: http::Response| async move { + let (parts, body) = resp.into_parts(); + let body = body.collect().await.ok().unwrap().to_bytes().to_vec(); + Ok::<_, Box>(http::Response::from_parts(parts, body)) + }); + let service = BoxService::new(service); + Mutex::new(LocalFuzzTarget { service, rx }) +} + +#[allow(unused)] +fn assert_ready_tokio(future: F) -> F::Output { + match Handle::try_current() { + Ok(handle) => handle.block_on(future), + Err(_) => { + let handle = Builder::new_multi_thread().build().unwrap(); + handle.block_on(future) + } + } +} + +/// Polls a future and panics if it isn't already ready. +fn assert_ready(mut future: F) -> F::Output { + // Create a waker that does nothing. + let waker = noop_waker(); + // Create a context from the waker. + let mut cx = Context::from_waker(&waker); + let mut future = pin!(future); + match future.as_mut().poll(&mut cx) { + Poll::Ready(output) => output, + Poll::Pending => { + panic!("poll pending...") + } + } +} + +#[cfg(test)] +mod test { + use crate::types::FuzzResult; + use crate::{make_target, Body}; + use http::Request; + use std::error::Error; + use std::future::Future; + use std::pin::Pin; + use std::task::{Context, Poll}; + use tokio::sync::mpsc::Sender; + + fuzz_harness!(|_tx| { TestService }); + + fn make_service(_sender: Sender) -> TestService { + TestService + } + + #[test] + fn test() { + make_target(make_service); + } + + struct TestService; + + impl tower::Service> for TestService { + type Response = http::Response; + type Error = Box; + type Future = + Pin> + Send + Sync>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + todo!() + } + + fn call(&mut self, _req: Request) -> Self::Future { + todo!() + } + } +} diff --git a/rust-runtime/aws-smithy-fuzz/src/main.rs b/rust-runtime/aws-smithy-fuzz/src/main.rs new file mode 100644 index 00000000000..f216a8f9857 --- /dev/null +++ b/rust-runtime/aws-smithy-fuzz/src/main.rs @@ -0,0 +1,737 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_fuzz::{FuzzResult, FuzzTarget, HttpRequest}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::env::current_dir; +use std::fmt::Display; +use std::io::{BufRead, BufWriter}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::SystemTime; +use std::{env, fs}; + +use clap::{Parser, Subcommand}; +use tera::{Context, Tera}; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Invoke the fuzz driver + Fuzz(FuzzArgs), + + /// Initialize the fuzz configuration + /// + /// This command will compile cdylib shared libraries and create a + Initialize(InitializeArgs), + + /// Replay subcommand + Replay(ReplayArgs), + + /// Setup smithy-rs targets + /// + /// This does all of the schlep of setting of smithy-rs copies at different revisions for fuzz testing + /// This command is not strictly necessary—It's pretty easy to wire this up yourself. But + /// this removes a lot of boilerplate. + SetupSmithy(SetupSmithyArgs), + + /// Invoke Testcase + /// + /// Directly invoke a test case against a given shared library target. Generally, replay will + /// be easier to use. + /// + /// This exists to facilitate invoking a cdylib that may panic without crashing the main + /// process + InvokeTestCase(InvokeArgs), +} + +#[derive(Parser)] +struct InvokeArgs { + #[arg(short, long)] + shared_library_path: PathBuf, + + #[arg(short, long)] + test_case: PathBuf, +} + +#[derive(Parser)] +struct SetupSmithyArgs { + #[arg(short, long)] + revision: Vec, + #[arg(short, long)] + service: String, + #[arg(short, long)] + workdir: PathBuf, + + #[arg(short, long)] + fuzz_runner_local_path: PathBuf, +} + +#[derive(Parser)] +struct FuzzArgs { + /// Custom path to the configuration file. + #[arg( + short, + long, + value_name = "PATH", + default_value = "smithy-fuzz-config.json" + )] + config_path: String, + + #[arg(long)] + enter_fuzzing_loop: bool, +} + +#[derive(Parser)] +struct InitializeArgs { + /// Path to the target crates to fuzz (repeat for multiple crates) + #[arg(short, long, value_name = "PATH")] + target_crate: Vec, + + /// Path to the `lexicon.json` defining the dictionary and corpus + /// + /// This file is typically produced by the fuzzgen smithy plugin. + #[arg(short, long, value_name = "PATH")] + lexicon: PathBuf, + + /// Custom path to the configuration file. + #[arg( + short, + long, + value_name = "PATH", + default_value = "smithy-fuzz-config.json" + )] + config_path: String, + + /// Force a rebuild of target artifacts. + /// + /// **NOTE**: You must use this if you change the target artifacts + #[arg(short, long)] + force_rebuild: bool, + + /// Compile the target artifacts in release mode + #[arg(short, long)] + release: bool, +} + +/// Replay crashes from a fuzz run +/// +/// By default, this will automatically discover all crashes during fuzzing sessions and rerun them. +/// To rerun a single crash, use `--invoke-only`. +#[derive(Parser)] +struct ReplayArgs { + /// Custom path to the configuration file. + #[arg( + short, + long, + value_name = "PATH", + default_value = "smithy-fuzz-config.json" + )] + config_path: String, + + /// Invoke only the specified path. + invoke_only: Option, +} + +#[derive(Deserialize)] +struct Lexicon { + corpus: Vec, + dictionary: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +struct FuzzConfig { + seed: PathBuf, + targets: Vec, + afl_input_dir: PathBuf, + afl_output_dir: PathBuf, + dictionaries: Vec, + lexicon: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Target { + package_name: String, + source: PathBuf, + shared_library: Option, +} + +fn main() { + tracing_subscriber::fmt::init(); + let cli = Cli::parse(); + match cli.command { + Commands::Fuzz(args) => fuzz(args), + Commands::Initialize(args) => initialize(args), + Commands::Replay(args) => replay(args), + Commands::SetupSmithy(args) => setup_smithy(args), + Commands::InvokeTestCase(InvokeArgs { + test_case, + shared_library_path, + }) => invoke_testcase(test_case, shared_library_path), + } +} + +fn setup_smithy(args: SetupSmithyArgs) { + let maven_locals = args.workdir.join("maven-locals"); + fs::create_dir_all(&maven_locals).unwrap(); + + let fuzz_driver_path = maven_locals.join("fuzz-driver"); + let local_path = current_dir().unwrap().join(&args.fuzz_runner_local_path); + build_revision(&fuzz_driver_path, &local_path); + + let rust_runtime_for_fuzzer = local_path.join("rust-runtime"); + + let mut context = Context::new(); + context.insert("service", &args.service); + context.insert("revisions", &args.revision); + context.insert("maven_local", &fuzz_driver_path); + context.insert("rust_runtime", &rust_runtime_for_fuzzer); + + let fuzzgen_smithy_build = Tera::one_off( + include_str!("../templates/smithy-build-fuzzer.jinja2"), + &context, + false, + ) + .unwrap(); + let fuzzgen_smithy_build_path = args.workdir.join("smithy-build-fuzzgen.json"); + fs::write( + &fuzzgen_smithy_build_path, + assert_valid_json(&fuzzgen_smithy_build), + ) + .unwrap(); + + for revision in args.revision { + let revision_dir = clone_smithyrs(&args.workdir, &revision); + + let maven_local_subpath = maven_locals.join(&revision); + build_revision(&maven_local_subpath, &revision_dir); + let mut context = Context::new(); + context.insert("maven_local", &maven_local_subpath); + context.insert("service", &args.service); + context.insert("revision", &revision); + context.insert("rust_runtime", &revision_dir.join("rust-runtime")); + + let fuzzgen_smithy_build = Tera::one_off( + include_str!("../templates/smithy-build-targetcrate.jinja2"), + &context, + false, + ) + .unwrap(); + let smithy_build_path = args.workdir.join(format!("smithy-build-{revision}.json")); + fs::write(&smithy_build_path, assert_valid_json(&fuzzgen_smithy_build)).unwrap(); + smithy_build(&args.workdir, &smithy_build_path); + } + println!("running smithy build for fuzz harness"); + smithy_build(&args.workdir, &fuzzgen_smithy_build_path); +} + +/// Run smithy build for a given file +/// +/// # Arguments +/// +/// * `workdir`: Path to the working directory (this is where the `build` directory) will be created / used +/// * `smithy_build_json`: Path to the smithy-build.json file +fn smithy_build(workdir: impl AsRef, smithy_build_json: impl AsRef) { + println!( + "running smithy build for {}", + smithy_build_json.as_ref().display() + ); + // Need to delete classpath.json if it exists to work around small bug in smithy CLI: + // https://github.com/smithy-lang/smithy/issues/2376 + let _ = fs::remove_file(workdir.as_ref().join("build/smithy").join("classpath.json")); + + exec( + Command::new("smithy") + .arg("build") + .arg("-c") + .arg(smithy_build_json.as_ref()) + .current_dir(workdir), + ); +} + +/// Creates a copy of the smithy-rs repository at a specific revision. +/// +/// - If it does not already exist, it will create a local clone of the entire repo. +/// - After that, it will make a local clone of that repo to facilitate checking out a specific +/// revision +/// +/// # Arguments +/// +/// * `workdir`: Working directory to clone into +/// * `revision`: Revision to check out +/// * `maven_local`: Path to a revisione-specific maven-local directory to build into +/// +/// returns: Path to the cloned directory +fn clone_smithyrs(workdir: impl AsRef, revision: &str) -> PathBuf { + let smithy_dir = workdir.as_ref().join("smithy-rs-src"); + if !smithy_dir.exists() { + exec( + Command::new("git") + .args(["clone", "git@github.com:smithy-lang/smithy-rs.git"]) + .arg(&smithy_dir) + .current_dir(&workdir), + ); + } + let copies_dir = workdir.as_ref().join("smithy-rs-copies"); + fs::create_dir_all(&copies_dir).unwrap(); + + let revision_dir = copies_dir.join(revision); + + exec( + Command::new("git") + .arg("clone") + .arg(smithy_dir) + .arg(&revision_dir), + ); + + exec( + Command::new("git") + .args(["checkout", &revision]) + .current_dir(&revision_dir), + ); + revision_dir +} + +fn exec(command: &mut Command) { + let status = command.spawn().unwrap().wait().unwrap(); + if !status.success() { + panic!("command failed: {:?}", command); + } +} + +/// Runs `./gradlew publishToMavenLocal` on a given smithy-rs directory +/// +/// # Arguments +/// +/// * `maven_local`: mavenLocal directory to publish into +/// * `smithy_dir`: smithy-rs source directory to use +fn build_revision(maven_local: impl AsRef, smithy_dir: impl AsRef) { + tracing::info!("building revision from {}", smithy_dir.as_ref().display()); + exec( + Command::new("./gradlew") + .args([ + "publishToMavenLocal", + &format!("-Dmaven.repo.local={}", maven_local.as_ref().display()), + ]) + .current_dir(&smithy_dir), + ) +} + +fn assert_valid_json(data: &str) -> &str { + serde_json::from_str::(data).unwrap(); + data +} + +fn force_load_libraries(libraries: &[Target]) -> Vec { + libraries + .iter() + .map(|t| { + t.shared_library + .as_ref() + .expect("shared library must be built! run `aws-smithy-fuzz initialize`") + }) + .map(FuzzTarget::from_path) + .collect::>() +} + +/// Starts a fuzz session +/// +/// This function is a little bit of an snake eating its tail. When it is initially run, +/// it ensures everything is set up properly, then it invokes AFL, passing through +/// all the relevant flags. AFL is actually going to come right back in here—(but with `enter_fuzzing_loop`) +/// set to true. In that case, we just prepare to start actually fuzzing the targets. +fn fuzz(args: FuzzArgs) { + let config = fs::read_to_string(&args.config_path).unwrap(); + let config: FuzzConfig = serde_json::from_str(&config).unwrap(); + if args.enter_fuzzing_loop { + let libraries = force_load_libraries(&config.targets); + let target = fs::File::create("log.txt").unwrap(); + let fuzzing_log = BufWriter::new(target); + enter_fuzz_loop(libraries, Some(fuzzing_log)) + } else { + eprintln!( + "Preparing to start fuzzing... {} targets.", + config.targets.len() + ); + let mut cmd = Command::new("cargo"); + cmd.args(["afl", "fuzz"]) + .arg("-i") + .arg(&config.lexicon) + .arg("-o") + .arg(&config.afl_output_dir); + + for dict in &config.dictionaries { + cmd.arg("-x").arg(dict); + } + + let current_binary = std::env::current_exe().expect("could not determine current target"); + cmd.arg(current_binary) + .arg("fuzz") + .arg("--config-path") + .arg(args.config_path) + .arg("--enter-fuzzing-loop"); + + eprintln!("Switching to AFL with the following command:\n{:?}", cmd); + cmd.spawn().unwrap().wait().unwrap(); + } +} + +fn yellow(text: impl Display) { + let mut stdout = StandardStream::stderr(ColorChoice::Auto); + use std::io::Write; + stdout + .set_color(ColorSpec::new().set_fg(Some(Color::Yellow))) + .unwrap(); + writeln!(&mut stdout, "{}", text).unwrap(); + stdout.reset().unwrap(); +} + +fn initialize( + InitializeArgs { + target_crate, + lexicon, + config_path, + force_rebuild, + release, + }: InitializeArgs, +) { + let mode = match release { + true => Mode::Release, + false => Mode::Debug, + }; + let current_config = if Path::new(&config_path).exists() { + let config_data = fs::read_to_string(&config_path).unwrap(); + let config: FuzzConfig = serde_json::from_str(&config_data).unwrap(); + Some(config) + } else { + None + }; + let current_targets = current_config.map(|c| c.targets).unwrap_or_default(); + let targets = if current_targets + .iter() + .map(|t| &t.source) + .collect::>() + != target_crate.iter().collect::>() + { + yellow("The target crates specified in the configuration file do not match the current target crates."); + eprintln!( + "Initializing the fuzzer with {} target crates.", + target_crate.len() + ); + target_crate + .into_iter() + .map(initialize_target) + .collect::>() + } else { + current_targets + }; + if targets.is_empty() { + yellow("No target crates specified, nothing to do."); + } + + let afl_input_dir = Path::new("afl-input"); + let afl_output_dir = Path::new("afl-output"); + + let mut config = FuzzConfig { + seed: lexicon, + targets, + afl_input_dir: afl_input_dir.into(), + afl_output_dir: afl_output_dir.into(), + lexicon: afl_input_dir.join("corpus"), + dictionaries: vec![afl_input_dir.join("dictionary")], + }; + + let seed_request = fs::read_to_string(&config.seed).unwrap(); + let seed: Lexicon = serde_json::from_str(&seed_request).unwrap(); + write_seed(&config.afl_input_dir, &seed); + + for target in &mut config.targets { + if target.shared_library.is_none() || force_rebuild { + build_target(target, mode); + } + check_library_health(&FuzzTarget::from_path( + target.shared_library.as_ref().unwrap(), + )); + } + eprintln!("Writing settings to {}", config_path); + fs::write(&config_path, serde_json::to_string_pretty(&config).unwrap()).unwrap(); +} + +fn initialize_target(source: PathBuf) -> Target { + let package_name = query_package_name(source.as_ref()); + Target { + package_name, + source, + shared_library: None, + } +} + +fn load_all_crashes(output_dir: &Path) -> Vec { + let pattern = output_dir.join("default/crashes*"); + + eprintln!("searching for crashes in {}", pattern.display()); + let pattern = format!("{}", pattern.display()); + let mut crash_directories = glob::glob(&pattern).unwrap(); + let mut crashes = vec![]; + while let Some(Ok(crash_dir)) = crash_directories.next() { + for entry in fs::read_dir(crash_dir).unwrap() { + let entry = entry.unwrap().path(); + match entry.file_name().and_then(|f| f.to_str()) { + None | Some("README.txt") => {} + Some(_other) => crashes.push(entry), + } + } + } + crashes +} + +/// Replay crashes +fn replay( + ReplayArgs { + config_path, + invoke_only, + }: ReplayArgs, +) { + let config = fs::read_to_string(config_path).unwrap(); + let config: FuzzConfig = serde_json::from_str(&config).unwrap(); + eprintln!("Replaying crashes."); + let crashes = if let Some(path) = invoke_only { + vec![path.into()] + } else { + load_all_crashes(&config.afl_output_dir) + }; + for crash in crashes { + println!("{}", crash.display()); + let data = fs::read(&crash).unwrap(); + let http_request = HttpRequest::from_unknown_bytes(&data); + println!("Test case: {:#?}", http_request); + let mut results = vec![]; + #[derive(Debug)] + enum CrashResult { + Panic(String), + FuzzResult(FuzzResult), + } + + for library in config + .targets + .iter() + .flat_map(|t| t.shared_library.as_ref()) + { + let result = Command::new(env::current_exe().unwrap()) + .arg("invoke-test-case") + .arg("--shared-library-path") + .arg(library) + .arg("--test-case") + .arg(&crash) + .output() + .unwrap(); + match serde_json::from_slice::(&result.stdout) { + Ok(result) => results.push(CrashResult::FuzzResult(result)), + Err(_err) => results.push(CrashResult::Panic( + String::from_utf8_lossy(&result.stderr).to_string(), + )), + } + } + println!("{:#?}", results); + } + println!("------------"); +} + +fn invoke_testcase(test_case: impl AsRef, shared_library_path: impl AsRef) { + let data = fs::read(test_case).unwrap(); + let library = FuzzTarget::from_path(shared_library_path); + let data = data.clone(); + let result = library.invoke_bytes(&data.clone()); + println!("{}", serde_json::to_string(&result).unwrap()); +} + +/// Enters the fuzzing loop. This method should only be entered when `afl` is driving the binary +fn enter_fuzz_loop(libraries: Vec, mut log: Option>) { + afl::fuzz(true, |data: &[u8]| { + use std::io::Write; + #[allow(clippy::disallowed_methods)] + let start = SystemTime::now(); + + let http_request = HttpRequest::from_unknown_bytes(data); + if let Some(request) = http_request { + if request.into_http_request_04x().is_some() { + let mut results = vec![]; + for library in &libraries { + results.push(library.invoke_bytes(data)); + } + log.iter_mut().for_each(|log| { + log.write_all( + #[allow(clippy::disallowed_methods)] + format!( + "[{:?}ms] {:?} {:?}\n", + start.elapsed().unwrap().as_millis(), + request, + &results[0] + ) + .as_bytes(), + ) + .unwrap(); + }); + for result in &results { + if result.response != results[0].response { + panic!("inconsistent results: {:#?}", results); + } + } + } + } + }); +} + +/// Converts a JSON formatted seed to the format expected by AFL +/// +/// - Dictionary items are written out into a file +/// - Corpus items are bincode serialized so that the format matches +fn write_seed(target_directory: &Path, seed: &Lexicon) { + fs::create_dir_all(target_directory.join("corpus")).unwrap(); + for (id, request) in seed.corpus.iter().enumerate() { + std::fs::write( + target_directory.join("corpus").join(&format!("{}", id)), + request.as_bytes(), + ) + .unwrap(); + } + use std::fmt::Write; + let mut dictionary = String::new(); + for word in &seed.dictionary { + writeln!(dictionary, "\"{word}\"").unwrap(); + } + std::fs::write(target_directory.join("dictionary"), dictionary.as_bytes()).unwrap(); +} + +fn check_library_health(library: &FuzzTarget) { + let input = HttpRequest { + uri: "/NoInputAndNoOutput".to_string(), + method: "POST".to_string(), + headers: [("Accept".to_string(), vec!["application/json".to_string()])] + .into_iter() + .collect::>(), + trailers: Default::default(), + body: "{}".into(), + }; + library.invoke(&input); +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +enum Mode { + Release, + Debug, +} + +impl AsRef for Mode { + fn as_ref(&self) -> &Path { + match self { + Mode::Release => Path::new("release"), + Mode::Debug => Path::new("debug"), + } + } +} + +fn query_package_name(path: &Path) -> String { + let metadata = Command::new("cargo") + .args(["metadata", "--format-version", "1"]) + .current_dir(path) + .output() + .unwrap(); + let metadata: Value = match serde_json::from_slice(&metadata.stdout) { + Ok(v) => v, + Err(e) => panic!( + "failed to parse metadata: {}\n{}", + e, + String::from_utf8_lossy(&metadata.stderr) + ), + }; + let package_name = &metadata["workspace_members"][0].as_str().unwrap(); + package_name.to_string() +} + +fn build_target(target: &mut Target, mode: Mode) { + let mut cmd = Command::new("cargo"); + cmd.env( + "CARGO_TARGET_DIR", + env::current_dir() + .unwrap() + .join("target/fuzz-target-target"), + ); + cmd.args(["afl", "build", "--message-format", "json"]); + cmd.stdout(Stdio::piped()); + if mode == Mode::Release { + cmd.arg("--release"); + } + let json_output = cmd + .current_dir(&target.source) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + + let shared_library = json_output + .stdout + .lines() + .filter_map(|line| cargo_output::find_shared_library(&line.unwrap(), target)) + .next() + .expect("failed to find shared library"); + target.shared_library = Some(PathBuf::from(shared_library)) +} + +mod cargo_output { + use crate::Target; + use serde::Deserialize; + + #[derive(Deserialize, Debug)] + struct DylibOutput { + reason: String, + package_id: String, + target: DyLibTarget, + filenames: Vec, + } + + #[derive(Deserialize, Debug)] + struct DyLibTarget { + kind: Vec, + } + + /// Reads a line of cargo output and look for the dylib + pub(super) fn find_shared_library(line: &str, target: &Target) -> Option { + tracing::trace!("{}", line); + let output: DylibOutput = match serde_json::from_str(line) { + Ok(output) => output, + Err(_e) => { + tracing::debug!("line does not match: {}", line); + return None; + } + }; + if output.reason != "compiler-artifact" { + return None; + } + if output.package_id != target.package_name { + tracing::debug!(expected = %target.package_name, actual = %output.package_id, "ignoring line—package was wrong"); + return None; + } + if output.target.kind != ["cdylib"] { + return None; + } + Some( + output + .filenames + .into_iter() + .next() + .expect("should be one dylib target"), + ) + } +} diff --git a/rust-runtime/aws-smithy-fuzz/src/types.rs b/rust-runtime/aws-smithy-fuzz/src/types.rs new file mode 100644 index 00000000000..c2989e21338 --- /dev/null +++ b/rust-runtime/aws-smithy-fuzz/src/types.rs @@ -0,0 +1,157 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use arbitrary::Arbitrary; +use std::convert::Infallible; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::{collections::HashMap, fmt::Debug}; + +use bytes::Bytes; +use http::{HeaderMap, HeaderName, Method}; +use serde::{Deserialize, Serialize}; + +pub struct Body { + body: Option>, + trailers: Option, +} + +impl Body { + pub fn from_bytes(bytes: Vec) -> Self { + Self { + body: Some(bytes), + trailers: None, + } + } + pub fn from_static(bytes: &'static [u8]) -> Self { + Self { + body: Some(bytes.into()), + trailers: None, + } + } +} + +impl http_body::Body for Body { + type Data = Bytes; + type Error = Infallible; + + fn poll_data( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + match self.as_mut().body.take() { + Some(data) => Poll::Ready(Some(Ok(data.into()))), + None => Poll::Ready(None), + } + } + + fn poll_trailers( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + match self.as_mut().trailers.take() { + Some(trailers) => Poll::Ready(Ok(Some(trailers))), + None => Poll::Ready(Ok(None)), + } + } +} + +#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, Arbitrary)] +pub struct HttpRequest { + pub uri: String, + pub method: String, + pub headers: HashMap>, + pub trailers: HashMap>, + pub body: Vec, +} + +#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq)] +pub struct HttpResponse { + pub status: u16, + pub headers: HashMap, + pub body: Vec, +} + +impl Debug for HttpResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HttpResponse") + .field("status", &self.status) + .field("headers", &self.headers) + .field("body", &TryString(&self.body)) + .finish() + } +} + +impl Debug for HttpRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HttpRequest") + .field("uri", &self.uri) + .field("method", &self.method) + .field("headers", &self.headers) + .field("body", &TryString(&self.body)) + .finish() + } +} + +struct TryString<'a>(&'a [u8]); +impl Debug for TryString<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", String::from_utf8_lossy(self.0)) + } +} + +#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)] +pub struct FuzzResult { + pub response: HttpResponse, + pub input: Option, +} + +impl FuzzResult { + pub fn into_bytes(self) -> Vec { + bincode::serialize(&self).unwrap() + } + + pub fn from_bytes(bytes: &[u8]) -> Self { + bincode::deserialize(bytes).unwrap() + } +} + +impl HttpRequest { + pub fn into_http_request_04x(&self) -> Option> { + let mut builder = http::Request::builder() + .uri(&self.uri) + .method(Method::from_bytes(self.method.as_bytes()).ok()?); + for (key, values) in &self.headers { + for value in values { + builder = builder.header(key, value); + } + } + let mut trailers = HeaderMap::new(); + for (k, v) in &self.trailers { + let header_name: HeaderName = k.parse().ok()?; + for v in v { + trailers.append(header_name.clone(), v.parse().ok()?); + } + } + builder + .body(Body { + body: Some(self.body.clone()), + trailers: Some(trailers), + }) + .ok() + } + + pub fn as_bytes(&self) -> Vec { + bincode::serialize(self).unwrap() + } + + pub fn from_bytes(bytes: &[u8]) -> Self { + bincode::deserialize(bytes).unwrap() + } + + pub fn from_unknown_bytes(bytes: &[u8]) -> Option { + bincode::deserialize(bytes).ok() + } +} diff --git a/rust-runtime/aws-smithy-fuzz/templates/smithy-build-fuzzer.jinja2 b/rust-runtime/aws-smithy-fuzz/templates/smithy-build-fuzzer.jinja2 new file mode 100644 index 00000000000..7c179a3bf4e --- /dev/null +++ b/rust-runtime/aws-smithy-fuzz/templates/smithy-build-fuzzer.jinja2 @@ -0,0 +1,39 @@ +{ + "version": "1.0", + "maven": { + "dependencies": [ + "software.amazon.smithy.rust.codegen.serde:fuzzgen:0.1.0", + "software.amazon.smithy:smithy-aws-protocol-tests:1.50.0" + ], + "repositories": [ + { + "url": "file://{{ maven_local }}" + }, + { + "url": "https://repo1.maven.org/maven2" + } + ] + }, + "projections": { + "harness": { + "imports": [ ], + "plugins": { + "fuzz-harness": { + "service": "{{ service }}", + "runtimeConfig": { + "relativePath": "{{ rust_runtime }}" + }, + "targetCrates": [ + {% for revision in revisions -%} + { + "relativePath": "build/smithy/{{ revision }}/rust-server-codegen/", + "name": "{{ revision }}" + } + {% if not loop.last %}, {% endif %} + {% endfor %} + ] + } + } + } + } +} diff --git a/rust-runtime/aws-smithy-fuzz/templates/smithy-build-targetcrate.jinja2 b/rust-runtime/aws-smithy-fuzz/templates/smithy-build-targetcrate.jinja2 new file mode 100644 index 00000000000..caf078b9b5d --- /dev/null +++ b/rust-runtime/aws-smithy-fuzz/templates/smithy-build-targetcrate.jinja2 @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "maven": { + "dependencies": [ + "software.amazon.smithy.rust.codegen.server.smithy:codegen-server:0.1.0", + "software.amazon.smithy:smithy-aws-protocol-tests:1.50.0" + ], + "repositories": [ + { + "url": "file://{{ maven_local }}" + }, + { + "url": "https://repo1.maven.org/maven2" + } + ] + }, + "projections": { + "{{ revision }}": { + "imports": [ ], + "plugins": { + "rust-server-codegen": { + "runtimeConfig": { + "relativePath": "{{ rust_runtime }}" + }, + "codegen": {}, + "service": "{{ service }}", + "module": "test_target", + "moduleVersion": "0.0.1", + "moduleDescription": "test-{{ revision }}", + "moduleAuthors": [ + "protocoltest@example.com" + ] + } + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ef481244ba2..bf89c0cfc7a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,7 @@ include(":aws:rust-runtime") include(":aws:sdk") include(":aws:sdk-adhoc-test") include(":aws:sdk-codegen") +include(":fuzzgen") pluginManagement { val smithyGradlePluginVersion: String by settings From 8b2746137cfaa0e3613405abca96dc79a59f65a3 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Fri, 23 Aug 2024 20:29:15 +0000 Subject: [PATCH 02/13] Fixes found from running on dev desktop --- rust-runtime/aws-smithy-fuzz/src/main.rs | 200 ++++++++++++++++------- 1 file changed, 144 insertions(+), 56 deletions(-) diff --git a/rust-runtime/aws-smithy-fuzz/src/main.rs b/rust-runtime/aws-smithy-fuzz/src/main.rs index f216a8f9857..6a787e85931 100644 --- a/rust-runtime/aws-smithy-fuzz/src/main.rs +++ b/rust-runtime/aws-smithy-fuzz/src/main.rs @@ -11,7 +11,7 @@ use std::env::current_dir; use std::fmt::Display; use std::io::{BufRead, BufWriter}; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::{Child, Command, Stdio}; use std::time::SystemTime; use std::{env, fs}; @@ -72,10 +72,14 @@ struct SetupSmithyArgs { #[arg(short, long)] service: String, #[arg(short, long)] - workdir: PathBuf, + workdir: Option, #[arg(short, long)] fuzz_runner_local_path: PathBuf, + + /// Rebuild the local clones of smithy-rs + #[arg(long)] + rebuild_local_targets: bool, } #[derive(Parser)] @@ -91,6 +95,10 @@ struct FuzzArgs { #[arg(long)] enter_fuzzing_loop: bool, + + /// The number of parallel fuzzers to run + #[arg(short, long)] + num_fuzzers: Option, } #[derive(Parser)] @@ -141,7 +149,12 @@ struct ReplayArgs { config_path: String, /// Invoke only the specified path. + #[arg(short, long)] invoke_only: Option, + + /// Output results in JSON + #[arg(short, long)] + json: bool, } #[derive(Deserialize)] @@ -167,6 +180,12 @@ struct Target { shared_library: Option, } +impl Target { + fn human_name(&self) -> String { + determine_package_name(&self.source.join("Cargo.toml")) + } +} + fn main() { tracing_subscriber::fmt::init(); let cli = Cli::parse(); @@ -183,7 +202,8 @@ fn main() { } fn setup_smithy(args: SetupSmithyArgs) { - let maven_locals = args.workdir.join("maven-locals"); + let workdir = args.workdir.unwrap_or(env::current_dir().unwrap()); + let maven_locals = workdir.join("maven-locals"); fs::create_dir_all(&maven_locals).unwrap(); let fuzz_driver_path = maven_locals.join("fuzz-driver"); @@ -204,7 +224,7 @@ fn setup_smithy(args: SetupSmithyArgs) { false, ) .unwrap(); - let fuzzgen_smithy_build_path = args.workdir.join("smithy-build-fuzzgen.json"); + let fuzzgen_smithy_build_path = workdir.join("smithy-build-fuzzgen.json"); fs::write( &fuzzgen_smithy_build_path, assert_valid_json(&fuzzgen_smithy_build), @@ -212,7 +232,7 @@ fn setup_smithy(args: SetupSmithyArgs) { .unwrap(); for revision in args.revision { - let revision_dir = clone_smithyrs(&args.workdir, &revision); + let revision_dir = clone_smithyrs(&workdir, &revision, args.rebuild_local_targets); let maven_local_subpath = maven_locals.join(&revision); build_revision(&maven_local_subpath, &revision_dir); @@ -228,12 +248,12 @@ fn setup_smithy(args: SetupSmithyArgs) { false, ) .unwrap(); - let smithy_build_path = args.workdir.join(format!("smithy-build-{revision}.json")); + let smithy_build_path = workdir.join(format!("smithy-build-{revision}.json")); fs::write(&smithy_build_path, assert_valid_json(&fuzzgen_smithy_build)).unwrap(); - smithy_build(&args.workdir, &smithy_build_path); + smithy_build(&workdir, &smithy_build_path); } println!("running smithy build for fuzz harness"); - smithy_build(&args.workdir, &fuzzgen_smithy_build_path); + smithy_build(&workdir, &fuzzgen_smithy_build_path); } /// Run smithy build for a given file @@ -273,7 +293,7 @@ fn smithy_build(workdir: impl AsRef, smithy_build_json: impl AsRef) /// * `maven_local`: Path to a revisione-specific maven-local directory to build into /// /// returns: Path to the cloned directory -fn clone_smithyrs(workdir: impl AsRef, revision: &str) -> PathBuf { +fn clone_smithyrs(workdir: impl AsRef, revision: &str, recreate: bool) -> PathBuf { let smithy_dir = workdir.as_ref().join("smithy-rs-src"); if !smithy_dir.exists() { exec( @@ -287,6 +307,10 @@ fn clone_smithyrs(workdir: impl AsRef, revision: &str) -> PathBuf { fs::create_dir_all(&copies_dir).unwrap(); let revision_dir = copies_dir.join(revision); + if revision_dir.exists() && !recreate { + return revision_dir; + } + exec(Command::new("rm").arg("-rf").arg(&revision_dir)); exec( Command::new("git") @@ -304,7 +328,12 @@ fn clone_smithyrs(workdir: impl AsRef, revision: &str) -> PathBuf { } fn exec(command: &mut Command) { - let status = command.spawn().unwrap().wait().unwrap(); + let status = match command.spawn().unwrap().wait() { + Ok(status) => status, + Err(e) => { + panic!("{:?} failed: {}", command, e) + } + }; if !status.success() { panic!("command failed: {:?}", command); } @@ -356,34 +385,60 @@ fn fuzz(args: FuzzArgs) { let config: FuzzConfig = serde_json::from_str(&config).unwrap(); if args.enter_fuzzing_loop { let libraries = force_load_libraries(&config.targets); - let target = fs::File::create("log.txt").unwrap(); - let fuzzing_log = BufWriter::new(target); - enter_fuzz_loop(libraries, Some(fuzzing_log)) + //let target = fs::File::create("log.txt").unwrap(); + //let fuzzing_log = BufWriter::new(target); + enter_fuzz_loop(libraries, None) } else { eprintln!( "Preparing to start fuzzing... {} targets.", config.targets.len() ); - let mut cmd = Command::new("cargo"); - cmd.args(["afl", "fuzz"]) - .arg("-i") - .arg(&config.lexicon) - .arg("-o") - .arg(&config.afl_output_dir); - - for dict in &config.dictionaries { - cmd.arg("-x").arg(dict); - } + let base_command = || { + let mut cmd = Command::new("cargo"); + cmd.args(["afl", "fuzz"]) + .arg("-i") + .arg(&config.lexicon) + .arg("-o") + .arg(&config.afl_output_dir); + + for dict in &config.dictionaries { + cmd.arg("-x").arg(dict); + } + cmd + }; - let current_binary = std::env::current_exe().expect("could not determine current target"); - cmd.arg(current_binary) - .arg("fuzz") - .arg("--config-path") - .arg(args.config_path) - .arg("--enter-fuzzing-loop"); + let apply_target = |mut cmd: Command| { + let current_binary = + std::env::current_exe().expect("could not determine current target"); + cmd.arg(current_binary) + .arg("fuzz") + .arg("--config-path") + .arg(&args.config_path) + .arg("--enter-fuzzing-loop"); + cmd + }; + let mut main_runner = base_command(); + main_runner.arg("-M").arg("fuzzer0"); - eprintln!("Switching to AFL with the following command:\n{:?}", cmd); - cmd.spawn().unwrap().wait().unwrap(); + eprintln!( + "Switching to AFL with the following command:\n{:?}", + main_runner + ); + let mut main = apply_target(main_runner).spawn().unwrap(); + let mut children = vec![]; + for idx in 0..args.num_fuzzers.unwrap_or_default() { + let mut runner = base_command(); + runner.arg("-S").arg(format!("fuzzer{}", idx)); + runner.stderr(Stdio::null()).stdout(Stdio::null()); + children.push(KillOnDrop(apply_target(runner).spawn().unwrap())); + } + main.wait().unwrap(); + } +} +struct KillOnDrop(Child); +impl Drop for KillOnDrop { + fn drop(&mut self) { + self.0.kill().unwrap(); } } @@ -469,16 +524,16 @@ fn initialize( } fn initialize_target(source: PathBuf) -> Target { - let package_name = query_package_name(source.as_ref()); + let package_id = determine_package_id(source.as_ref()); Target { - package_name, + package_name: package_id, source, shared_library: None, } } fn load_all_crashes(output_dir: &Path) -> Vec { - let pattern = output_dir.join("default/crashes*"); + let pattern = output_dir.join("fuzzer*/crashes*"); eprintln!("searching for crashes in {}", pattern.display()); let pattern = format!("{}", pattern.display()); @@ -501,51 +556,64 @@ fn replay( ReplayArgs { config_path, invoke_only, + json, }: ReplayArgs, ) { let config = fs::read_to_string(config_path).unwrap(); let config: FuzzConfig = serde_json::from_str(&config).unwrap(); - eprintln!("Replaying crashes."); let crashes = if let Some(path) = invoke_only { vec![path.into()] } else { load_all_crashes(&config.afl_output_dir) }; + eprintln!("Replaying {} crashes.", crashes.len()); for crash in crashes { - println!("{}", crash.display()); + eprintln!("{}", crash.display()); let data = fs::read(&crash).unwrap(); let http_request = HttpRequest::from_unknown_bytes(&data); - println!("Test case: {:#?}", http_request); - let mut results = vec![]; - #[derive(Debug)] + let mut results: HashMap = HashMap::new(); + #[derive(Debug, Serialize)] + #[serde(tag = "type")] enum CrashResult { - Panic(String), - FuzzResult(FuzzResult), + Panic { message: String }, + FuzzResult { result: String }, } - for library in config - .targets - .iter() - .flat_map(|t| t.shared_library.as_ref()) - { + for library in &config.targets { let result = Command::new(env::current_exe().unwrap()) .arg("invoke-test-case") .arg("--shared-library-path") - .arg(library) + .arg(library.shared_library.as_deref().unwrap()) .arg("--test-case") .arg(&crash) .output() .unwrap(); - match serde_json::from_slice::(&result.stdout) { - Ok(result) => results.push(CrashResult::FuzzResult(result)), - Err(_err) => results.push(CrashResult::Panic( - String::from_utf8_lossy(&result.stderr).to_string(), - )), - } + let result = match serde_json::from_slice::(&result.stdout) { + Ok(result) => CrashResult::FuzzResult { result: format!("{:?}", result) }, + Err(_err) => CrashResult::Panic { + message: String::from_utf8_lossy(&result.stderr).to_string(), + }, + }; + results.insert(library.human_name(), result); + } + #[derive(Serialize)] + struct Results { + test_case: String, + results: HashMap, + } + if json { + println!( + "{}", + serde_json::to_string(&Results { + test_case: format!("{:#?}", http_request.unwrap()), + results + }) + .unwrap() + ); + } else { + println!("{:#?}\n----", results); } - println!("{:#?}", results); } - println!("------------"); } fn invoke_testcase(test_case: impl AsRef, shared_library_path: impl AsRef) { @@ -585,6 +653,9 @@ fn enter_fuzz_loop(libraries: Vec, mut log: Option, mut log: Option bool { + for lib in libraries { + let sample = (0..10) + .map(|_idx| lib.invoke_bytes(&data)) + .collect::>(); + if sample.iter().any(|result| result != &sample[0]) { + return true; + } + } + return false; +} + /// Converts a JSON formatted seed to the format expected by AFL /// /// - Dictionary items are written out into a file @@ -642,7 +725,12 @@ impl AsRef for Mode { } } -fn query_package_name(path: &Path) -> String { +fn determine_package_name(path: &Path) -> String { + let cargo_toml_file = cargo_toml::Manifest::from_path(path).expect("invalid manifest"); + cargo_toml_file.package.unwrap().name +} + +fn determine_package_id(path: &Path) -> String { let metadata = Command::new("cargo") .args(["metadata", "--format-version", "1"]) .current_dir(path) From aaf239c00354298dabb3441e877dbf99fce848ac Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Mon, 26 Aug 2024 19:37:17 +0000 Subject: [PATCH 03/13] Fixes for dependencies, other bug fixes --- rust-runtime/aws-smithy-fuzz/Cargo.toml | 2 + rust-runtime/aws-smithy-fuzz/src/main.rs | 173 +++++++++++++----- rust-runtime/aws-smithy-fuzz/src/types.rs | 7 +- .../templates/smithy-build-fuzzer.jinja2 | 6 +- .../templates/smithy-build-targetcrate.jinja2 | 4 +- 5 files changed, 146 insertions(+), 46 deletions(-) diff --git a/rust-runtime/aws-smithy-fuzz/Cargo.toml b/rust-runtime/aws-smithy-fuzz/Cargo.toml index 315318c8996..5f8871c3c71 100644 --- a/rust-runtime/aws-smithy-fuzz/Cargo.toml +++ b/rust-runtime/aws-smithy-fuzz/Cargo.toml @@ -13,6 +13,8 @@ arbitrary = { version = "1.3.2", features = ["derive"] } bincode = "1" bytes = "1.7.1" cargo_toml = "0.20.4" +cbor = "0.4.1" +cbor-diag = "0.1.12" clap = { version = "4.5.15", features = ["derive"] } ffi-support = "0.4.4" futures = "0.3.30" diff --git a/rust-runtime/aws-smithy-fuzz/src/main.rs b/rust-runtime/aws-smithy-fuzz/src/main.rs index 6a787e85931..eebcc98ce0f 100644 --- a/rust-runtime/aws-smithy-fuzz/src/main.rs +++ b/rust-runtime/aws-smithy-fuzz/src/main.rs @@ -7,7 +7,6 @@ use aws_smithy_fuzz::{FuzzResult, FuzzTarget, HttpRequest}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::{HashMap, HashSet}; -use std::env::current_dir; use std::fmt::Display; use std::io::{BufRead, BufWriter}; use std::path::{Path, PathBuf}; @@ -80,6 +79,12 @@ struct SetupSmithyArgs { /// Rebuild the local clones of smithy-rs #[arg(long)] rebuild_local_targets: bool, + + /// Additional dependencies to use. Usually, these provide the model. + /// + /// Dependencies should be in the following format: `software.amazon.smithy:smithy-aws-protocol-tests:1.50.0` + #[arg(long)] + dependency: Vec, } #[derive(Parser)] @@ -155,6 +160,12 @@ struct ReplayArgs { /// Output results in JSON #[arg(short, long)] json: bool, + + /// Run against the input corpus instead of running against crashes + /// + /// This is helpful for sanity checking that everything is working properly + #[arg(long)] + corpus: bool, } #[derive(Deserialize)] @@ -202,52 +213,32 @@ fn main() { } fn setup_smithy(args: SetupSmithyArgs) { - let workdir = args.workdir.unwrap_or(env::current_dir().unwrap()); + let current_dir = env::current_dir().unwrap(); + let workdir = match &args.workdir { + Some(relative_workdir) => current_dir.join(relative_workdir), + None => current_dir.clone(), + }; let maven_locals = workdir.join("maven-locals"); fs::create_dir_all(&maven_locals).unwrap(); let fuzz_driver_path = maven_locals.join("fuzz-driver"); - let local_path = current_dir().unwrap().join(&args.fuzz_runner_local_path); + let local_path = current_dir.join(&args.fuzz_runner_local_path); build_revision(&fuzz_driver_path, &local_path); let rust_runtime_for_fuzzer = local_path.join("rust-runtime"); - let mut context = Context::new(); - context.insert("service", &args.service); - context.insert("revisions", &args.revision); - context.insert("maven_local", &fuzz_driver_path); - context.insert("rust_runtime", &rust_runtime_for_fuzzer); - - let fuzzgen_smithy_build = Tera::one_off( - include_str!("../templates/smithy-build-fuzzer.jinja2"), - &context, - false, - ) - .unwrap(); + let fuzzgen_smithy_build = + generate_smithy_build_json_for_fuzzer(&args, &fuzz_driver_path, &rust_runtime_for_fuzzer); let fuzzgen_smithy_build_path = workdir.join("smithy-build-fuzzgen.json"); - fs::write( - &fuzzgen_smithy_build_path, - assert_valid_json(&fuzzgen_smithy_build), - ) - .unwrap(); + fs::write(&fuzzgen_smithy_build_path, &fuzzgen_smithy_build).unwrap(); - for revision in args.revision { + for revision in &args.revision { let revision_dir = clone_smithyrs(&workdir, &revision, args.rebuild_local_targets); let maven_local_subpath = maven_locals.join(&revision); build_revision(&maven_local_subpath, &revision_dir); - let mut context = Context::new(); - context.insert("maven_local", &maven_local_subpath); - context.insert("service", &args.service); - context.insert("revision", &revision); - context.insert("rust_runtime", &revision_dir.join("rust-runtime")); - - let fuzzgen_smithy_build = Tera::one_off( - include_str!("../templates/smithy-build-targetcrate.jinja2"), - &context, - false, - ) - .unwrap(); + let fuzzgen_smithy_build = + generate_smithy_build_for_target(&maven_local_subpath, &args, &revision, &revision_dir); let smithy_build_path = workdir.join(format!("smithy-build-{revision}.json")); fs::write(&smithy_build_path, assert_valid_json(&fuzzgen_smithy_build)).unwrap(); smithy_build(&workdir, &smithy_build_path); @@ -256,6 +247,49 @@ fn setup_smithy(args: SetupSmithyArgs) { smithy_build(&workdir, &fuzzgen_smithy_build_path); } +fn generate_smithy_build_for_target( + maven_local_subpath: &Path, + args: &SetupSmithyArgs, + revision: &str, + revision_dir: &Path, +) -> String { + let mut context = Context::new(); + context.insert("maven_local", &maven_local_subpath); + context.insert("service", &args.service); + context.insert("revision", revision); + context.insert("rust_runtime", &revision_dir.join("rust-runtime")); + context.insert("dependencies", &args.dependency); + + let fuzzgen_smithy_build = Tera::one_off( + include_str!("../templates/smithy-build-targetcrate.jinja2"), + &context, + false, + ) + .unwrap(); + assert_valid_json(fuzzgen_smithy_build) +} + +fn generate_smithy_build_json_for_fuzzer( + args: &SetupSmithyArgs, + fuzz_driver_path: &Path, + rust_runtime_for_fuzzer: &Path, +) -> String { + let mut context = Context::new(); + context.insert("service", &args.service); + context.insert("revisions", &args.revision); + context.insert("maven_local", &fuzz_driver_path); + context.insert("rust_runtime", &rust_runtime_for_fuzzer); + context.insert("dependencies", &args.dependency); + + let fuzzgen_smithy_build = Tera::one_off( + include_str!("../templates/smithy-build-fuzzer.jinja2"), + &context, + false, + ) + .unwrap(); + assert_valid_json(fuzzgen_smithy_build) +} + /// Run smithy build for a given file /// /// # Arguments @@ -310,13 +344,19 @@ fn clone_smithyrs(workdir: impl AsRef, revision: &str, recreate: bool) -> if revision_dir.exists() && !recreate { return revision_dir; } - exec(Command::new("rm").arg("-rf").arg(&revision_dir)); + exec( + Command::new("rm") + .arg("-rf") + .arg(&revision_dir) + .current_dir(&workdir), + ); exec( Command::new("git") .arg("clone") .arg(smithy_dir) - .arg(&revision_dir), + .arg(&revision_dir) + .current_dir(&workdir), ); exec( @@ -328,6 +368,11 @@ fn clone_smithyrs(workdir: impl AsRef, revision: &str, recreate: bool) -> } fn exec(command: &mut Command) { + match command.get_current_dir() { + None => panic!("BUG: all commands should set a working directory"), + Some(dir) if !dir.is_absolute() => panic!("bug: absolute directory should be set"), + _ => {} + }; let status = match command.spawn().unwrap().wait() { Ok(status) => status, Err(e) => { @@ -357,8 +402,15 @@ fn build_revision(maven_local: impl AsRef, smithy_dir: impl AsRef) { ) } -fn assert_valid_json(data: &str) -> &str { - serde_json::from_str::(data).unwrap(); +fn assert_valid_json>(data: T) -> T { + match serde_json::from_str::(data.as_ref()) { + Err(e) => panic!( + "failed to generate valid JSON. this is a bug. {}\n\n{}", + e, + data.as_ref() + ), + Ok(_) => {} + }; data } @@ -426,7 +478,7 @@ fn fuzz(args: FuzzArgs) { ); let mut main = apply_target(main_runner).spawn().unwrap(); let mut children = vec![]; - for idx in 0..args.num_fuzzers.unwrap_or_default() { + for idx in 1..args.num_fuzzers.unwrap_or_default() { let mut runner = base_command(); runner.arg("-S").arg(format!("fuzzer{}", idx)); runner.stderr(Stdio::null()).stdout(Stdio::null()); @@ -534,8 +586,16 @@ fn initialize_target(source: PathBuf) -> Target { fn load_all_crashes(output_dir: &Path) -> Vec { let pattern = output_dir.join("fuzzer*/crashes*"); + load_inputs_at_pattern(&pattern) +} + +fn load_corpus(input_dir: &Path) -> Vec { + let pattern = input_dir.join("corpus"); + load_inputs_at_pattern(&pattern) +} - eprintln!("searching for crashes in {}", pattern.display()); +fn load_inputs_at_pattern(pattern: &Path) -> Vec { + eprintln!("searching for test cases in {}", pattern.display()); let pattern = format!("{}", pattern.display()); let mut crash_directories = glob::glob(&pattern).unwrap(); let mut crashes = vec![]; @@ -557,6 +617,7 @@ fn replay( config_path, invoke_only, json, + corpus, }: ReplayArgs, ) { let config = fs::read_to_string(config_path).unwrap(); @@ -564,7 +625,10 @@ fn replay( let crashes = if let Some(path) = invoke_only { vec![path.into()] } else { - load_all_crashes(&config.afl_output_dir) + match corpus { + true => load_corpus(&config.afl_input_dir), + false => load_all_crashes(&config.afl_output_dir), + } }; eprintln!("Replaying {} crashes.", crashes.len()); for crash in crashes { @@ -589,7 +653,9 @@ fn replay( .output() .unwrap(); let result = match serde_json::from_slice::(&result.stdout) { - Ok(result) => CrashResult::FuzzResult { result: format!("{:?}", result) }, + Ok(result) => CrashResult::FuzzResult { + result: format!("{:?}", result), + }, Err(_err) => CrashResult::Panic { message: String::from_utf8_lossy(&result.stderr).to_string(), }, @@ -823,3 +889,28 @@ mod cargo_output { ) } } + +#[cfg(test)] +mod test { + use std::{env::temp_dir, path::PathBuf}; + + use crate::{ + generate_smithy_build_for_target, generate_smithy_build_json_for_fuzzer, setup_smithy, + SetupSmithyArgs, + }; + + #[test] + fn does_this_work() { + let path = PathBuf::new(); + let args = SetupSmithyArgs { + revision: vec!["main".into()], + service: "test-service".to_string(), + workdir: Some(temp_dir()), + fuzz_runner_local_path: "../".into(), + rebuild_local_targets: true, + dependency: vec![], + }; + generate_smithy_build_json_for_fuzzer(&args, &path, &path); + generate_smithy_build_for_target(&path, &args, "revision", &path); + } +} diff --git a/rust-runtime/aws-smithy-fuzz/src/types.rs b/rust-runtime/aws-smithy-fuzz/src/types.rs index c2989e21338..fe25638ebb4 100644 --- a/rust-runtime/aws-smithy-fuzz/src/types.rs +++ b/rust-runtime/aws-smithy-fuzz/src/types.rs @@ -98,7 +98,12 @@ impl Debug for HttpRequest { struct TryString<'a>(&'a [u8]); impl Debug for TryString<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "\"{}\"", String::from_utf8_lossy(self.0)) + let try_cbor = cbor_diag::parse_bytes(self.0); + let str_rep = match try_cbor { + Ok(repr) => repr.to_diag_pretty(), + Err(e) => String::from_utf8_lossy(self.0).to_string(), + }; + write!(f, "\"{}\"", str_rep) } } diff --git a/rust-runtime/aws-smithy-fuzz/templates/smithy-build-fuzzer.jinja2 b/rust-runtime/aws-smithy-fuzz/templates/smithy-build-fuzzer.jinja2 index 7c179a3bf4e..e355acfd601 100644 --- a/rust-runtime/aws-smithy-fuzz/templates/smithy-build-fuzzer.jinja2 +++ b/rust-runtime/aws-smithy-fuzz/templates/smithy-build-fuzzer.jinja2 @@ -2,8 +2,10 @@ "version": "1.0", "maven": { "dependencies": [ - "software.amazon.smithy.rust.codegen.serde:fuzzgen:0.1.0", - "software.amazon.smithy:smithy-aws-protocol-tests:1.50.0" + {% for dependency in dependencies -%} + "{{ dependency }}", + {% endfor %} + "software.amazon.smithy.rust.codegen.serde:fuzzgen:0.1.0" ], "repositories": [ { diff --git a/rust-runtime/aws-smithy-fuzz/templates/smithy-build-targetcrate.jinja2 b/rust-runtime/aws-smithy-fuzz/templates/smithy-build-targetcrate.jinja2 index caf078b9b5d..01e4daa01e2 100644 --- a/rust-runtime/aws-smithy-fuzz/templates/smithy-build-targetcrate.jinja2 +++ b/rust-runtime/aws-smithy-fuzz/templates/smithy-build-targetcrate.jinja2 @@ -2,8 +2,8 @@ "version": "1.0", "maven": { "dependencies": [ - "software.amazon.smithy.rust.codegen.server.smithy:codegen-server:0.1.0", - "software.amazon.smithy:smithy-aws-protocol-tests:1.50.0" + {% for dependency in dependencies -%} "{{ dependency }}", {% endfor %} + "software.amazon.smithy.rust.codegen.server.smithy:codegen-server:0.1.0" ], "repositories": [ { From e1001133c37ba65c5d322f07b5cc88f59e3c8a7a Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Mon, 26 Aug 2024 15:36:36 -0400 Subject: [PATCH 04/13] base64 decode automatically --- .../rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt | 15 ++++++++++----- rust-runtime/aws-smithy-fuzz/README.md | 8 ++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt index 27defb93fe5..fd804e811e0 100644 --- a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt @@ -50,6 +50,7 @@ import software.amazon.smithy.rust.codegen.core.util.orNull import software.amazon.smithy.rust.codegen.server.smithy.ServerModuleProvider import software.amazon.smithy.rust.codegen.server.smithy.transformers.AttachValidationExceptionToConstrainedOperationInputsInAllowList import java.nio.file.Path +import java.util.Base64 import kotlin.streams.toList data class FuzzTarget(val name: String, val relativePath: String) { @@ -91,7 +92,6 @@ class FuzzHarnessBuildPlugin : SmithyBuildPlugin { override fun execute(context: PluginContext) { val fuzzSettings = FuzzSettings.fromNode(context.settings) - val subdir = FileManifest.create(context.fileManifest.resolvePath(Path.of("driver"))) val model = context.model.let(OperationNormalizer::transform) .let(AttachValidationExceptionToConstrainedOperationInputsInAllowList::transform) @@ -133,6 +133,14 @@ fun corpus( val protocolTests = operations.flatMap { it.getTrait()?.testCases ?: listOf() } val out = ArrayNode.builder() protocolTests.forEach { testCase -> + val body: List = + when (testCase.bodyMediaType.orNull()) { + "application/cbor" -> { + println("base64 decoding first") + Base64.getDecoder().decode(testCase.body.orNull())?.map { NumberNode.from(it) } + } + else -> testCase.body.orNull()?.chars()?.toList()?.map { c -> NumberNode.from(c) } + } ?: listOf() out.withValue( ObjectNode.objectNode() .withMember("uri", testCase.uri) @@ -148,10 +156,7 @@ fun corpus( .withMember("trailers", ObjectNode.objectNode()) .withMember( "body", - ArrayNode.fromNodes( - testCase.body.orNull()?.chars()?.toList()?.map { c -> NumberNode.from(c) } - ?: listOf(), - ), + ArrayNode.fromNodes(body), ), ) } diff --git a/rust-runtime/aws-smithy-fuzz/README.md b/rust-runtime/aws-smithy-fuzz/README.md index 60a83efeafc..aba273d17f2 100644 --- a/rust-runtime/aws-smithy-fuzz/README.md +++ b/rust-runtime/aws-smithy-fuzz/README.md @@ -2,6 +2,14 @@ AWS Smithy fuzz contains a set of utilities for writing fuzz tests against smithy-rs servers. This is part of our tooling to perform differential fuzzing against different versions of smithy-rs-server. +## Installation +1. Install `cargo afl`: `cargo install cargo-afl` +2. Install the AFL runtime: `cargo afl config --build` +2. Install the smithy CLI: +2. Install aws-smithy-fuzz: + 3. Locally: `cargo afl install --path .` + 4. From crates.io: cargo afl install aws-smithy-fuzz + ## Usage This contains a library + a CLI tool to fuzz smithy servers. The library allows setting up a given Smithy server implementation as a `cdylib`. This allows two different versions two by dynamically linked at runtime and executed by the fuzzer. From 36447a88b0b37564cc80824aa32d05b957faddef Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 27 Aug 2024 15:23:48 +0000 Subject: [PATCH 05/13] lots more fixes (some more cleanup still required) --- .../codegen/fuzz/FuzzHarnessBuildPlugin.kt | 6 ++- rust-runtime/aws-smithy-fuzz/README.md | 4 +- rust-runtime/aws-smithy-fuzz/src/lib.rs | 1 - rust-runtime/aws-smithy-fuzz/src/main.rs | 40 ++++++++++++++----- rust-runtime/aws-smithy-fuzz/src/types.rs | 5 ++- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt index fd804e811e0..d098e30dcfc 100644 --- a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt @@ -102,6 +102,7 @@ class FuzzHarnessBuildPlugin : SmithyBuildPlugin { target } + println("creating the driver...") createDriver(model, context.fileManifest, fuzzSettings) targets.forEach { @@ -133,11 +134,12 @@ fun corpus( val protocolTests = operations.flatMap { it.getTrait()?.testCases ?: listOf() } val out = ArrayNode.builder() protocolTests.forEach { testCase -> + println(testCase.bodyMediaType) val body: List = when (testCase.bodyMediaType.orNull()) { "application/cbor" -> { - println("base64 decoding first") - Base64.getDecoder().decode(testCase.body.orNull())?.map { NumberNode.from(it) } + println("base64 decoding first (v2)") + Base64.getDecoder().decode(testCase.body.orNull())?.map { NumberNode.from(it.toUByte().toInt()) } } else -> testCase.body.orNull()?.chars()?.toList()?.map { c -> NumberNode.from(c) } } ?: listOf() diff --git a/rust-runtime/aws-smithy-fuzz/README.md b/rust-runtime/aws-smithy-fuzz/README.md index aba273d17f2..46dbc7970cf 100644 --- a/rust-runtime/aws-smithy-fuzz/README.md +++ b/rust-runtime/aws-smithy-fuzz/README.md @@ -7,8 +7,8 @@ AWS Smithy fuzz contains a set of utilities for writing fuzz tests against smith 2. Install the AFL runtime: `cargo afl config --build` 2. Install the smithy CLI: 2. Install aws-smithy-fuzz: - 3. Locally: `cargo afl install --path .` - 4. From crates.io: cargo afl install aws-smithy-fuzz + - Locally: `cargo afl install --path .` + - From crates.io: cargo afl install aws-smithy-fuzz ## Usage This contains a library + a CLI tool to fuzz smithy servers. The library allows setting up a given Smithy server implementation as a `cdylib`. This allows two different versions two by dynamically linked at runtime and executed by the fuzzer. diff --git a/rust-runtime/aws-smithy-fuzz/src/lib.rs b/rust-runtime/aws-smithy-fuzz/src/lib.rs index 4f8458b494f..e23864fc2cc 100644 --- a/rust-runtime/aws-smithy-fuzz/src/lib.rs +++ b/rust-runtime/aws-smithy-fuzz/src/lib.rs @@ -164,7 +164,6 @@ fn assert_ready(mut future: F) -> F::Output { #[cfg(test)] mod test { - use crate::types::FuzzResult; use crate::{make_target, Body}; use http::Request; use std::error::Error; diff --git a/rust-runtime/aws-smithy-fuzz/src/main.rs b/rust-runtime/aws-smithy-fuzz/src/main.rs index eebcc98ce0f..7e9c14a1d28 100644 --- a/rust-runtime/aws-smithy-fuzz/src/main.rs +++ b/rust-runtime/aws-smithy-fuzz/src/main.rs @@ -643,6 +643,21 @@ fn replay( FuzzResult { result: String }, } + impl Display for CrashResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CrashResult::Panic { message } => { + f.pad("The process paniced!\n")?; + for line in message.lines() { + write!(f, " {}\n", line)?; + } + Ok(()) + } + CrashResult::FuzzResult { result } => f.pad(result), + } + } + } + for library in &config.targets { let result = Command::new(env::current_exe().unwrap()) .arg("invoke-test-case") @@ -667,17 +682,24 @@ fn replay( test_case: String, results: HashMap, } + impl Display for Results { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let test_case = &self.test_case; + write!(f, "Test case: {test_case}\n")?; + for (target, result) in &self.results { + write!(f, " target: {target}\n{:>2}", result)?; + } + Ok(()) + } + } + let results = Results { + test_case: format!("{:#?}", http_request.unwrap()), + results, + }; if json { - println!( - "{}", - serde_json::to_string(&Results { - test_case: format!("{:#?}", http_request.unwrap()), - results - }) - .unwrap() - ); + println!("{}", serde_json::to_string(&results).unwrap()); } else { - println!("{:#?}\n----", results); + println!("{}\n----", results); } } } diff --git a/rust-runtime/aws-smithy-fuzz/src/types.rs b/rust-runtime/aws-smithy-fuzz/src/types.rs index fe25638ebb4..64b4dfcec87 100644 --- a/rust-runtime/aws-smithy-fuzz/src/types.rs +++ b/rust-runtime/aws-smithy-fuzz/src/types.rs @@ -101,7 +101,10 @@ impl Debug for TryString<'_> { let try_cbor = cbor_diag::parse_bytes(self.0); let str_rep = match try_cbor { Ok(repr) => repr.to_diag_pretty(), - Err(e) => String::from_utf8_lossy(self.0).to_string(), + Err(e) => { + eprintln!("not cbor: {}", e); + String::from_utf8_lossy(self.0).to_string() + } }; write!(f, "\"{}\"", str_rep) } From b3b7de570e84364175e236e81a2596731c53772f Mon Sep 17 00:00:00 2001 From: david-perez Date: Thu, 17 Oct 2024 13:57:21 +0000 Subject: [PATCH 06/13] `git clone` using HTTPs protocol --- rust-runtime/aws-smithy-fuzz/README.md | 4 ++-- rust-runtime/aws-smithy-fuzz/src/main.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust-runtime/aws-smithy-fuzz/README.md b/rust-runtime/aws-smithy-fuzz/README.md index 46dbc7970cf..02e32194df5 100644 --- a/rust-runtime/aws-smithy-fuzz/README.md +++ b/rust-runtime/aws-smithy-fuzz/README.md @@ -30,8 +30,8 @@ REVISION_1=main REVISION_2=76d5afb42d545ca2f5cbe90a089681135da935d3 rm -rf maven-locals && mkdir maven-locals # Build two different versions of smithy-rs and publish them to two separate local directories -git clone git@github.com:smithy-lang/smithy-rs.git smithy-rs1 && (cd smithy-rs1 && git checkout $REVISION_1 && ./gradlew publishToMavenLocal -Dmaven.repo.local=$(cd ../maven-locals && pwd)/$REVISION_1) -git clone git@github.com:smithy-lang/smithy-rs.git smithy-rs2 && (cd smithy-rs2 && git checkout $REVISION_2 && ./gradlew publishToMavenLocal -Dmaven.repo.local=$(cd ../maven-locals && pwd)/$REVISION_2) +git clone https://github.com/smithy-lang/smithy-rs.git smithy-rs1 && (cd smithy-rs1 && git checkout $REVISION_1 && ./gradlew publishToMavenLocal -Dmaven.repo.local=$(cd ../maven-locals && pwd)/$REVISION_1) +git clone https://github.com/smithy-lang/smithy-rs.git smithy-rs2 && (cd smithy-rs2 && git checkout $REVISION_2 && ./gradlew publishToMavenLocal -Dmaven.repo.local=$(cd ../maven-locals && pwd)/$REVISION_2) ``` For each of these, use the smithy CLI to generate a server implementation using something like this: diff --git a/rust-runtime/aws-smithy-fuzz/src/main.rs b/rust-runtime/aws-smithy-fuzz/src/main.rs index 7e9c14a1d28..8ef9450efa0 100644 --- a/rust-runtime/aws-smithy-fuzz/src/main.rs +++ b/rust-runtime/aws-smithy-fuzz/src/main.rs @@ -332,7 +332,7 @@ fn clone_smithyrs(workdir: impl AsRef, revision: &str, recreate: bool) -> if !smithy_dir.exists() { exec( Command::new("git") - .args(["clone", "git@github.com:smithy-lang/smithy-rs.git"]) + .args(["clone", "https://github.com/smithy-lang/smithy-rs.git"]) .arg(&smithy_dir) .current_dir(&workdir), ); From 0f8a496b8329cd2848a3a6ab842cffc2a23337e9 Mon Sep 17 00:00:00 2001 From: david-perez Date: Thu, 17 Oct 2024 14:00:37 +0000 Subject: [PATCH 07/13] `rm -r ~/.m2` before invoking `smithy build` --- rust-runtime/aws-smithy-fuzz/Cargo.toml | 1 + rust-runtime/aws-smithy-fuzz/src/main.rs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/rust-runtime/aws-smithy-fuzz/Cargo.toml b/rust-runtime/aws-smithy-fuzz/Cargo.toml index 5f8871c3c71..a9f092865f9 100644 --- a/rust-runtime/aws-smithy-fuzz/Cargo.toml +++ b/rust-runtime/aws-smithy-fuzz/Cargo.toml @@ -19,6 +19,7 @@ clap = { version = "4.5.15", features = ["derive"] } ffi-support = "0.4.4" futures = "0.3.30" glob = "0.3.1" +homedir = "0.3" http = "0.2" http-body = "0.4" lazy_static = "1.5.0" diff --git a/rust-runtime/aws-smithy-fuzz/src/main.rs b/rust-runtime/aws-smithy-fuzz/src/main.rs index 8ef9450efa0..c62a84f4ed0 100644 --- a/rust-runtime/aws-smithy-fuzz/src/main.rs +++ b/rust-runtime/aws-smithy-fuzz/src/main.rs @@ -305,6 +305,13 @@ fn smithy_build(workdir: impl AsRef, smithy_build_json: impl AsRef) // https://github.com/smithy-lang/smithy/issues/2376 let _ = fs::remove_file(workdir.as_ref().join("build/smithy").join("classpath.json")); + let home_dir = homedir::my_home().unwrap().unwrap(); + exec( + Command::new("rm") + .arg("-r") + .arg(format!("{}/.m2", home_dir.display())) + .current_dir(&workdir), + ); exec( Command::new("smithy") .arg("build") From 702252cd946370a59c7f7b75c7d0f6995922ff66 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Thu, 26 Dec 2024 15:49:55 -0500 Subject: [PATCH 08/13] Rollback changes to RustCrate --- .../codegen/core/smithy/CodegenDelegator.kt | 44 +++++++------------ .../codegen/fuzz/FuzzHarnessBuildPlugin.kt | 4 +- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt index f921cf717cf..8ce5d8888dd 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt @@ -66,29 +66,6 @@ interface ModuleDocProvider { fun docsWriter(module: RustModule.LeafModule): Writable? } -open class RustCrate( - fileManifest: FileManifest, - private val symbolProvider: RustSymbolProvider, - coreCodegenConfig: CoreCodegenConfig, - moduleDocProvider: ModuleDocProvider, -): BasicRustCrate(fileManifest, symbolProvider, coreCodegenConfig, moduleDocProvider) { - /** - * Write into the module that this shape is [locatedIn] - */ - fun useShapeWriter( - shape: Shape, - f: Writable, - ) { - val module = symbolProvider.toSymbol(shape).module() - check(!module.isInline()) { - "Cannot use useShapeWriter with inline modules—use [RustWriter.withInlineModule] instead" - } - withModule(symbolProvider.toSymbol(shape).module(), f) - } - -} - - /** * RustCrate abstraction. * @@ -106,7 +83,7 @@ open class RustCrate( * shape locations are determined. * 2. [finalize]: Write the crate out to the file system, generating a lib.rs and Cargo.toml */ -open class BasicRustCrate( +open class RustCrate( fileManifest: FileManifest, private val symbolProvider: SymbolProvider, coreCodegenConfig: CoreCodegenConfig, @@ -118,6 +95,19 @@ open class BasicRustCrate( // used to ensure we never create accidentally discard docs / incorrectly create modules with incorrect visibility private var duplicateModuleWarningSystem: MutableMap = mutableMapOf() + /** + * Write into the module that this shape is [locatedIn] + */ + fun useShapeWriter( + shape: Shape, + f: Writable, + ) { + val module = symbolProvider.toSymbol(shape).module() + check(!module.isInline()) { + "Cannot use useShapeWriter with inline modules—use [RustWriter.withInlineModule] instead" + } + withModule(symbolProvider.toSymbol(shape).module(), f) + } /** * Write directly into lib.rs @@ -199,7 +189,7 @@ open class BasicRustCrate( fun withModule( module: RustModule, moduleWriter: Writable, - ) { + ): RustCrate { when (module) { is RustModule.LibRs -> lib { moduleWriter(this) } is RustModule.LeafModule -> { @@ -224,7 +214,7 @@ open class BasicRustCrate( } } } - //return this + // return this } /** @@ -233,7 +223,7 @@ open class BasicRustCrate( fun moduleFor( shape: Shape, moduleWriter: Writable, - ) = withModule((symbolProvider as RustSymbolProvider).moduleForShape(shape), moduleWriter) + ): RustCrate = withModule((symbolProvider as RustSymbolProvider).moduleForShape(shape), moduleWriter) /** * Create a new file directly diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt index d098e30dcfc..fdbb8d61e7e 100644 --- a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt @@ -33,12 +33,12 @@ import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.Writable -import software.amazon.smithy.rust.codegen.core.smithy.BasicRustCrate import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings import software.amazon.smithy.rust.codegen.core.smithy.ModuleDocProvider import software.amazon.smithy.rust.codegen.core.smithy.ModuleProviderContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProviderConfig import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor @@ -233,7 +233,7 @@ fun createFuzzTarget( ), ).let { PublicCrateSymbolProvider("rust_server_codegen", it) } val crate = - BasicRustCrate( + RustCrate( newManifest, symbolProvider, codegenConfig, From be16fc43e162fa6f9b2cb60939f4230870c06a58 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Thu, 26 Dec 2024 16:16:14 -0500 Subject: [PATCH 09/13] First pass at cleanups --- .../codegen/core/smithy/CodegenDelegator.kt | 2 +- .../smithy/rust/codegen/core/util/Exec.kt | 3 +- fuzzgen/build.gradle.kts | 3 +- .../codegen/fuzz/FuzzHarnessBuildPlugin.kt | 31 ++----------------- .../rust/codegen/fuzz/FuzzTargetContext.kt | 4 +-- .../fuzz/FuzzHarnessBuildPluginTest.kt | 9 +++--- gradle.properties | 2 -- 7 files changed, 12 insertions(+), 42 deletions(-) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt index 8ce5d8888dd..9c72dd2185c 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt @@ -214,7 +214,7 @@ open class RustCrate( } } } - // return this + return this } /** diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt index 0e2039d115e..399e4fd1a1e 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt @@ -16,11 +16,10 @@ fun String.runCommand( workdir: Path? = null, environment: Map = mapOf(), timeout: Long = 3600, - redirect: ProcessBuilder.Redirect = ProcessBuilder.Redirect.PIPE + redirect: ProcessBuilder.Redirect = ProcessBuilder.Redirect.PIPE, ): String { val logger = Logger.getLogger("RunCommand") logger.fine("Invoking comment $this in `$workdir` with env $environment") - println("Invoking comment $this in `$workdir` with env $environment") val start = System.currentTimeMillis() val parts = this.split("\\s".toRegex()) val builder = diff --git a/fuzzgen/build.gradle.kts b/fuzzgen/build.gradle.kts index 910e874da79..e025831a51e 100644 --- a/fuzzgen/build.gradle.kts +++ b/fuzzgen/build.gradle.kts @@ -65,7 +65,7 @@ if (isTestingEnabled.toBoolean()) { runtimeOnly(project(":rust-runtime")) testImplementation("org.junit.jupiter:junit-jupiter:5.6.1") testImplementation("software.amazon.smithy:smithy-validation-model:$smithyVersion") - testImplementation("software.amazon.smithy:smithy-aws-protocol-tests:1.48.0") + testImplementation("software.amazon.smithy:smithy-aws-protocol-tests:$smithyVersion") testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") } @@ -84,7 +84,6 @@ if (isTestingEnabled.toBoolean()) { // Convert to classpath string val classpath = fullClasspath.asPath - println(classpath) } } diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt index fdbb8d61e7e..84cf343f87a 100644 --- a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt @@ -23,7 +23,6 @@ import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait import software.amazon.smithy.model.traits.HttpQueryTrait import software.amazon.smithy.model.traits.HttpTrait @@ -36,7 +35,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings import software.amazon.smithy.rust.codegen.core.smithy.ModuleDocProvider -import software.amazon.smithy.rust.codegen.core.smithy.ModuleProviderContext +import software.amazon.smithy.rust.codegen.core.smithy.PublicImportSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider @@ -134,7 +133,6 @@ fun corpus( val protocolTests = operations.flatMap { it.getTrait()?.testCases ?: listOf() } val out = ArrayNode.builder() protocolTests.forEach { testCase -> - println(testCase.bodyMediaType) val body: List = when (testCase.bodyMediaType.orNull()) { "application/cbor" -> { @@ -231,7 +229,7 @@ fun createFuzzTarget( NullableIndex.CheckMode.SERVER, ServerModuleProvider, ), - ).let { PublicCrateSymbolProvider("rust_server_codegen", it) } + ).let { PublicImportSymbolProvider(it, "rust_server_codegen") } val crate = RustCrate( newManifest, @@ -255,31 +253,6 @@ class DocProvider : ModuleDocProvider { } } -class NoOpVisitor : RustSymbolProvider { - override val model: Model - get() = TODO("Not yet implemented") - override val moduleProviderContext: ModuleProviderContext - get() = TODO("Not yet implemented") - override val config: RustSymbolProviderConfig - get() = TODO("Not yet implemented") - - override fun symbolForOperationError(operation: OperationShape): Symbol { - TODO("Not yet implemented") - } - - override fun symbolForEventStreamError(eventStream: UnionShape): Symbol { - TODO("Not yet implemented") - } - - override fun symbolForBuilder(shape: Shape): Symbol { - TODO("Not yet implemented") - } - - override fun toSymbol(shape: Shape?): Symbol { - TODO("Not yet implemented") - } -} - class PublicCrateSymbolProvider(private val crateName: String, private val base: RustSymbolProvider) : WrappingSymbolProvider(base) { override fun toSymbol(shape: Shape): Symbol { diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt index 2d76b6d500a..ed022ffed28 100644 --- a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt @@ -17,11 +17,11 @@ import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable -import software.amazon.smithy.rust.codegen.core.smithy.BasicRustCrate import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.contextName import software.amazon.smithy.rust.codegen.core.util.hasStreamingMember @@ -52,7 +52,7 @@ fun rustSettings( data class FuzzTargetContext( val target: FuzzTarget, val fuzzSettings: FuzzSettings, - val rustCrate: BasicRustCrate, + val rustCrate: RustCrate, val model: Model, val symbolProvider: RustSymbolProvider, private val manifest: FileManifest, diff --git a/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt b/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt index 124e221730e..21c115b713d 100644 --- a/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt +++ b/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt @@ -14,6 +14,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.printGeneratedFiles +import software.amazon.smithy.rust.codegen.core.util.runCommand import java.io.File class FuzzHarnessBuildPluginTest() { @@ -40,9 +41,9 @@ class FuzzHarnessBuildPluginTest() { .withMember( "targetCrates", ArrayNode.arrayNode( - ObjectNode.objectNode().withMember("relativePath", actualServerCodegenPath(modelName, "2")) + ObjectNode.objectNode().withMember("relativePath", actualServerCodegenPath(modelName, "")) .withMember("name", "a"), - ObjectNode.objectNode().withMember("relativePath", actualServerCodegenPath(modelName, "3")) + ObjectNode.objectNode().withMember("relativePath", actualServerCodegenPath(modelName, "")) .withMember("name", "b"), ), ) @@ -56,7 +57,7 @@ class FuzzHarnessBuildPluginTest() { ).build() FuzzHarnessBuildPlugin().execute(context) context.fileManifest.printGeneratedFiles() - print("cd ${context.fileManifest.baseDir.resolve("driver")} && cargo check") - // "cargo check".runCommand(context.fileManifest.baseDir.resolve("a")) + "cargo check".runCommand(context.fileManifest.baseDir.resolve("a")) + "cargo check".runCommand(context.fileManifest.baseDir.resolve("b")) } } diff --git a/gradle.properties b/gradle.properties index 3cd33b6172c..4cfe1db1f78 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,5 +24,3 @@ ktlintVersion=1.0.1 kotestVersion=5.8.0 # Avoid registering dependencies/plugins/tasks that are only used for testing purposes isTestingEnabled=true - -smithyRsVersion=0.1.0 From d510c80352aaca2e4e2a53e8d3cc066f5023f286 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Thu, 9 Jan 2025 11:27:57 -0500 Subject: [PATCH 10/13] more cleanups --- .../codegen/fuzz/FuzzHarnessBuildPlugin.kt | 123 +++++------------- ...argetContext.kt => FuzzTargetGenerator.kt} | 65 +++++++-- .../fuzz/FuzzHarnessBuildPluginTest.kt | 48 ++++--- rust-runtime/aws-smithy-fuzz/README.md | 12 ++ 4 files changed, 135 insertions(+), 113 deletions(-) rename fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/{FuzzTargetContext.kt => FuzzTargetGenerator.kt} (75%) diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt index 84cf343f87a..80c7e1ef690 100644 --- a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt @@ -8,9 +8,7 @@ package software.amazon.smithy.rust.codegen.fuzz import software.amazon.smithy.build.FileManifest import software.amazon.smithy.build.PluginContext import software.amazon.smithy.build.SmithyBuildPlugin -import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.Model -import software.amazon.smithy.model.knowledge.NullableIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.neighbor.Walker import software.amazon.smithy.model.node.ArrayNode @@ -20,7 +18,6 @@ import software.amazon.smithy.model.node.ObjectNode import software.amazon.smithy.model.node.StringNode import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait @@ -30,37 +27,38 @@ import software.amazon.smithy.model.traits.JsonNameTrait import software.amazon.smithy.model.traits.XmlNameTrait import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait import software.amazon.smithy.rust.codegen.core.rustlang.RustModule -import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.Writable -import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig -import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings import software.amazon.smithy.rust.codegen.core.smithy.ModuleDocProvider -import software.amazon.smithy.rust.codegen.core.smithy.PublicImportSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig -import software.amazon.smithy.rust.codegen.core.smithy.RustCrate -import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider -import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProviderConfig -import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor -import software.amazon.smithy.rust.codegen.core.smithy.WrappingSymbolProvider -import software.amazon.smithy.rust.codegen.core.smithy.mapRustType import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.orNull -import software.amazon.smithy.rust.codegen.server.smithy.ServerModuleProvider import software.amazon.smithy.rust.codegen.server.smithy.transformers.AttachValidationExceptionToConstrainedOperationInputsInAllowList import java.nio.file.Path import java.util.Base64 import kotlin.streams.toList -data class FuzzTarget(val name: String, val relativePath: String) { +/** + * Metadata for a TargetCrate: A code generated smithy-rs server for a given model + */ +data class TargetCrate( + /** The name of the Fuzz target */ + val name: String, + /** Where the server implementation of this target is */ + val relativePath: String, +) { companion object { - fun fromNode(node: ObjectNode): FuzzTarget { + fun fromNode(node: ObjectNode): TargetCrate { val name = node.expectStringMember("name").value val relativePath = node.expectStringMember("relativePath").value - return FuzzTarget(name, relativePath) + return TargetCrate(name, relativePath) } } + /** The name of the actual `package` from Cargo's perspective. + * + * We need this to make a dependency on it + * */ fun targetPackage(): String { val path = Path.of(relativePath) val cargoToml = path.resolve("Cargo.toml").toFile() @@ -71,13 +69,15 @@ data class FuzzTarget(val name: String, val relativePath: String) { } data class FuzzSettings( - val targetCratePath: List, + val targetServers: List, val service: ShapeId, val runtimeConfig: RuntimeConfig, ) { companion object { fun fromNode(node: ObjectNode): FuzzSettings { - val targetCrates = node.expectArrayMember("targetCrates").map { FuzzTarget.fromNode(it.expectObjectNode()) } + val targetCrates = + node.expectArrayMember("targetCrates") + .map { TargetCrate.fromNode(it.expectObjectNode()) } val service = ShapeId.fromNode(node.expectStringMember("service")) val runtimeConfig = RuntimeConfig.fromNode(node.getObjectMember("runtimeConfig")) return FuzzSettings(targetCrates, service, runtimeConfig) @@ -85,6 +85,11 @@ data class FuzzSettings( } } +/** + * Build plugin for generating a fuzz harness and lexicon from a smithy model and a set of smithy-rs versions + * + * This is used by `aws-smithy-fuzz` which contains most of the usage docs + */ class FuzzHarnessBuildPlugin : SmithyBuildPlugin { override fun getName(): String = "fuzz-harness" @@ -95,10 +100,11 @@ class FuzzHarnessBuildPlugin : SmithyBuildPlugin { context.model.let(OperationNormalizer::transform) .let(AttachValidationExceptionToConstrainedOperationInputsInAllowList::transform) val targets = - fuzzSettings.targetCratePath.map { target -> - val target = createFuzzTarget(target, context.fileManifest, fuzzSettings, model) - FuzzTargetGenerator(target).generateFuzzTarget() - target + fuzzSettings.targetServers.map { target -> + val targetContext = createFuzzTarget(target, context.fileManifest, fuzzSettings, model) + println("Creating a fuzz targret for $targetContext") + FuzzTargetGenerator(targetContext).generateFuzzTarget() + targetContext } println("creating the driver...") @@ -110,21 +116,9 @@ class FuzzHarnessBuildPlugin : SmithyBuildPlugin { } } -fun driverSettings( - service: ShapeId, - runtimeConfig: RuntimeConfig, -) = CoreRustSettings( - service, - moduleVersion = "0.1.0", - moduleName = "fuzz-driver", - moduleAuthors = listOf(), - codegenConfig = CoreCodegenConfig(), - license = null, - runtimeConfig = runtimeConfig, - moduleDescription = null, - moduleRepository = null, -) - +/** + * Generate a corpus of words used within the model to see the dictionary + */ fun corpus( model: Model, fuzzSettings: FuzzSettings, @@ -139,6 +133,7 @@ fun corpus( println("base64 decoding first (v2)") Base64.getDecoder().decode(testCase.body.orNull())?.map { NumberNode.from(it.toUByte().toInt()) } } + else -> testCase.body.orNull()?.chars()?.toList()?.map { c -> NumberNode.from(c) } } ?: listOf() out.withValue( @@ -210,58 +205,8 @@ fun getTraitBasedNames(shape: Shape): List { ) } -fun createFuzzTarget( - target: FuzzTarget, - baseManifest: FileManifest, - fuzzSettings: FuzzSettings, - model: Model, -): FuzzTargetContext { - val newManifest = FileManifest.create(baseManifest.resolvePath(Path.of(target.name))) - val codegenConfig = CoreCodegenConfig() - val symbolProvider = - SymbolVisitor( - rustSettings(fuzzSettings, target), - model, - model.expectShape(fuzzSettings.service, ServiceShape::class.java), - RustSymbolProviderConfig( - fuzzSettings.runtimeConfig, - renameExceptions = false, - NullableIndex.CheckMode.SERVER, - ServerModuleProvider, - ), - ).let { PublicImportSymbolProvider(it, "rust_server_codegen") } - val crate = - RustCrate( - newManifest, - symbolProvider, - codegenConfig, - DocProvider(), - ) - return FuzzTargetContext( - target = target, - fuzzSettings = fuzzSettings, - rustCrate = crate, - model = model, - manifest = newManifest, - symbolProvider = symbolProvider, - ) -} - -class DocProvider : ModuleDocProvider { +class NoOpDocProvider : ModuleDocProvider { override fun docsWriter(module: RustModule.LeafModule): Writable? { return null } } - -class PublicCrateSymbolProvider(private val crateName: String, private val base: RustSymbolProvider) : - WrappingSymbolProvider(base) { - override fun toSymbol(shape: Shape): Symbol { - val base = base.toSymbol(shape) - return base.mapRustType { ty -> - when (ty) { - is RustType.Opaque -> RustType.Opaque(ty.name, ty.namespace?.replace("crate", crateName)) - else -> ty - } - } - } -} diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetGenerator.kt similarity index 75% rename from fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt rename to fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetGenerator.kt index ed022ffed28..b1894ffeb61 100644 --- a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetContext.kt +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzTargetGenerator.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rust.codegen.fuzz import software.amazon.smithy.build.FileManifest import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.NullableIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape @@ -19,10 +20,13 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings +import software.amazon.smithy.rust.codegen.core.smithy.PublicImportSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProviderConfig +import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor import software.amazon.smithy.rust.codegen.core.smithy.contextName import software.amazon.smithy.rust.codegen.core.util.hasStreamingMember import software.amazon.smithy.rust.codegen.core.util.inputShape @@ -30,13 +34,14 @@ import software.amazon.smithy.rust.codegen.core.util.isEventStream import software.amazon.smithy.rust.codegen.core.util.outputShape import software.amazon.smithy.rust.codegen.core.util.toPascalCase import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import software.amazon.smithy.rust.codegen.server.smithy.ServerModuleProvider import software.amazon.smithy.rust.codegen.server.smithy.isDirectlyConstrained import java.nio.file.Path import kotlin.io.path.name -fun rustSettings( +private fun rustSettings( fuzzSettings: FuzzSettings, - target: FuzzTarget, + target: TargetCrate, ) = CoreRustSettings( fuzzSettings.service, moduleVersion = "0.1.0", @@ -50,7 +55,7 @@ fun rustSettings( ) data class FuzzTargetContext( - val target: FuzzTarget, + val target: TargetCrate, val fuzzSettings: FuzzSettings, val rustCrate: RustCrate, val model: Model, @@ -59,7 +64,10 @@ data class FuzzTargetContext( ) { fun finalize(): FileManifest { val forceWorkspace = - mapOf("workspace" to listOf("_ignored" to "_ignored").toMap(), "lib" to mapOf("crate-type" to listOf("cdylib"))) + mapOf( + "workspace" to listOf("_ignored" to "_ignored").toMap(), + "lib" to mapOf("crate-type" to listOf("cdylib")), + ) val rustSettings = rustSettings(fuzzSettings, target) rustCrate.finalize(rustSettings, model, forceWorkspace, listOf(), requireDocs = false) return manifest @@ -69,13 +77,13 @@ data class FuzzTargetContext( class FuzzTargetGenerator(private val context: FuzzTargetContext) { private val model = context.model private val serviceShape = context.model.expectShape(context.fuzzSettings.service, ServiceShape::class.java) - private val symbolProvider = context.symbolProvider + private val symbolProvider = PublicImportSymbolProvider(context.symbolProvider, targetCrate().name) private fun targetCrate(): RuntimeType { val path = Path.of(context.target.relativePath).toAbsolutePath() return CargoDependency( - path.name, - Local(path.parent?.toString() ?: ""), + name = path.name, + location = Local(path.parent?.toString() ?: ""), `package` = context.target.targetPackage(), ).toType() } @@ -131,7 +139,8 @@ class FuzzTargetGenerator(private val context: FuzzTargetContext) { private fun allTxs(): Writable = writable { operationsToImplement().forEach { op -> - val operationName = op.contextName(serviceShape).toSnakeCase().let { RustReservedWords.escapeIfNeeded(it) } + val operationName = + op.contextName(serviceShape).toSnakeCase().let { RustReservedWords.escapeIfNeeded(it) } rust("let tx_$operationName = tx.clone();") } } @@ -140,7 +149,8 @@ class FuzzTargetGenerator(private val context: FuzzTargetContext) { writable { val operations = operationsToImplement() operations.forEach { op -> - val operationName = op.contextName(serviceShape).toSnakeCase().let { RustReservedWords.escapeIfNeeded(it) } + val operationName = + op.contextName(serviceShape).toSnakeCase().let { RustReservedWords.escapeIfNeeded(it) } val output = writable { val outputSymbol = symbolProvider.toSymbol(op.outputShape(model)) @@ -166,3 +176,40 @@ class FuzzTargetGenerator(private val context: FuzzTargetContext) { } } } + +fun createFuzzTarget( + target: TargetCrate, + baseManifest: FileManifest, + fuzzSettings: FuzzSettings, + model: Model, +): FuzzTargetContext { + val newManifest = FileManifest.create(baseManifest.resolvePath(Path.of(target.name))) + val codegenConfig = CoreCodegenConfig() + val symbolProvider = + SymbolVisitor( + rustSettings(fuzzSettings, target), + model, + model.expectShape(fuzzSettings.service, ServiceShape::class.java), + RustSymbolProviderConfig( + fuzzSettings.runtimeConfig, + renameExceptions = false, + NullableIndex.CheckMode.SERVER, + ServerModuleProvider, + ), + ) + val crate = + RustCrate( + newManifest, + symbolProvider, + codegenConfig, + NoOpDocProvider(), + ) + return FuzzTargetContext( + target = target, + fuzzSettings = fuzzSettings, + rustCrate = crate, + model = model, + manifest = newManifest, + symbolProvider = symbolProvider, + ) +} diff --git a/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt b/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt index 21c115b713d..5522c13fee3 100644 --- a/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt +++ b/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt @@ -4,47 +4,65 @@ */ package software.amazon.smithy.rust.codegen.fuzz +import io.kotest.matchers.collections.shouldContain import org.junit.jupiter.api.Test import software.amazon.smithy.build.FileManifest import software.amazon.smithy.build.PluginContext import software.amazon.smithy.model.node.ArrayNode import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.printGeneratedFiles import software.amazon.smithy.rust.codegen.core.util.runCommand -import java.io.File +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest class FuzzHarnessBuildPluginTest() { - fun actualServerCodegenPath( - model: String, - extra: String, - ) = - File("../../smithy-rs$extra/codegen-server-test/build/smithyprojections/codegen-server-test/$model/rust-server-codegen").absolutePath + private val minimalModel = + """ + namespace com.example + use aws.protocols#awsJson1_0 + @awsJson1_0 + service HelloService { + operations: [SayHello], + version: "1" + } + @optionalAuth + operation SayHello { input: TestInput } + structure TestInput { + foo: String, + } + """.asSmithyModel() + /** + * Smoke test that generates a lexicon and target crate for the trivial service above + */ @Test - fun works() { - val model = "namespace empty".asSmithyModel() + fun smokeTest() { val testDir = TestWorkspace.subproject() val testPath = testDir.toPath() val manifest = FileManifest.create(testPath) - val modelName = "rest_json" + val service = "com.example#HelloService" + val generatedServer = + serverIntegrationTest( + minimalModel, + IntegrationTestParams(service = service, command = { dir -> println("generated $dir") }), + ) { _, _ -> + } val context = PluginContext.builder() - .model(model) + .model(minimalModel) .fileManifest(manifest) .settings( ObjectNode.objectNode() - .withMember("service", "aws.protocoltests.restjson#RestJson") + .withMember("service", "com.example#HelloService") .withMember( "targetCrates", ArrayNode.arrayNode( - ObjectNode.objectNode().withMember("relativePath", actualServerCodegenPath(modelName, "")) + ObjectNode.objectNode().withMember("relativePath", generatedServer.toString()) .withMember("name", "a"), - ObjectNode.objectNode().withMember("relativePath", actualServerCodegenPath(modelName, "")) - .withMember("name", "b"), ), ) .withMember( @@ -57,7 +75,7 @@ class FuzzHarnessBuildPluginTest() { ).build() FuzzHarnessBuildPlugin().execute(context) context.fileManifest.printGeneratedFiles() + context.fileManifest.files.map { it.fileName.toString() } shouldContain "lexicon.json" "cargo check".runCommand(context.fileManifest.baseDir.resolve("a")) - "cargo check".runCommand(context.fileManifest.baseDir.resolve("b")) } } diff --git a/rust-runtime/aws-smithy-fuzz/README.md b/rust-runtime/aws-smithy-fuzz/README.md index 02e32194df5..4bbcc662286 100644 --- a/rust-runtime/aws-smithy-fuzz/README.md +++ b/rust-runtime/aws-smithy-fuzz/README.md @@ -9,6 +9,8 @@ AWS Smithy fuzz contains a set of utilities for writing fuzz tests against smith 2. Install aws-smithy-fuzz: - Locally: `cargo afl install --path .` - From crates.io: cargo afl install aws-smithy-fuzz + > **IMPORTANT**: This package MUST be installed with `cargo afl install` (instead of `cargo install`). If you do not use `afl`, + > you will get linking errors. ## Usage This contains a library + a CLI tool to fuzz smithy servers. The library allows setting up a given Smithy server implementation as a `cdylib`. This allows two different versions two by dynamically linked at runtime and executed by the fuzzer. @@ -23,6 +25,14 @@ First, you'll need to generate the 1 (or more) versions of a smithy-rs server to There is nothing magic about what `setup-smithy` does, but it does save you some tedious setup. +```bash +aws-smithy-fuzz setup-smithy --revision fix-timestamp-from-f64 --service smithy.protocoltests.rpcv2Cbor#RpcV2Protocol --workdir fuzz-workspace-cbor2 --fuzz-runner-local-path smithy-rs --dependency software.amazon.smithy:smithy-protocol-tests:1.50.0 --rebuild-local-targets +``` +
+ +Details of functionality of `setup-smithy`. This can be helpful if you need to do something slightly different. + + ```bash # Create a workspace just to keep track of everything mkdir workspace && cd workspace @@ -117,8 +127,10 @@ The easiest way to use `fuzzgen` is with the Smithy CLI: } } ``` +
### Initialization and Fuzzing +After `setup-smithy` creates the target shims, use `aws-smithy initialize` to setup ceremony required for `AFL` to function: ``` aws-smithy-fuzz initialize --lexicon --target-crate --target-crate ``` From c945a7429e1e6ef31bf65f1681d20a22d9ef261c Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Thu, 9 Jan 2025 13:15:10 -0500 Subject: [PATCH 11/13] CI cleanups --- rust-runtime/Cargo.lock | 118 +++++++++++++++++++++- rust-runtime/Cargo.toml | 1 - rust-runtime/aws-smithy-fuzz/Cargo.toml | 6 +- rust-runtime/aws-smithy-fuzz/src/lib.rs | 1 + rust-runtime/aws-smithy-fuzz/src/main.rs | 2 + rust-runtime/aws-smithy-fuzz/src/types.rs | 5 +- 6 files changed, 121 insertions(+), 12 deletions(-) diff --git a/rust-runtime/Cargo.lock b/rust-runtime/Cargo.lock index 5b1a36c7835..febd144a444 100644 --- a/rust-runtime/Cargo.lock +++ b/rust-runtime/Cargo.lock @@ -496,10 +496,13 @@ dependencies = [ "bincode", "bytes", "cargo_toml", + "cbor", + "cbor-diag", "clap 4.5.23", "ffi-support", "futures", "glob", + "homedir", "http 0.2.12", "http-body 0.4.6", "lazy_static", @@ -1090,6 +1093,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byteorder" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" + [[package]] name = "byteorder" version = "1.5.0" @@ -1131,6 +1140,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbor" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56053652b4b5c0ded5ae6183c7cd547ad2dd6bcce149658bef052a4995533bd" +dependencies = [ + "byteorder 0.5.3", + "rustc-serialize", +] + [[package]] name = "cbor-diag" version = "0.1.12" @@ -1176,6 +1195,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.39" @@ -1996,6 +2021,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "homedir" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2" +dependencies = [ + "cfg-if", + "nix", + "widestring", + "windows", +] + [[package]] name = "http" version = "0.2.12" @@ -2197,7 +2234,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -2706,6 +2743,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -3437,6 +3486,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-serialize" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4672,6 +4727,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -4703,6 +4764,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -4712,6 +4783,49 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -4881,7 +4995,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", + "byteorder 1.5.0", "zerocopy-derive", ] diff --git a/rust-runtime/Cargo.toml b/rust-runtime/Cargo.toml index b724af2c25b..d7853b00996 100644 --- a/rust-runtime/Cargo.toml +++ b/rust-runtime/Cargo.toml @@ -8,7 +8,6 @@ members = [ "aws-smithy-compression", "aws-smithy-client", "aws-smithy-eventstream", - "aws-smithy-fuzz", "aws-smithy-http", "aws-smithy-http-auth", "aws-smithy-http-server", diff --git a/rust-runtime/aws-smithy-fuzz/Cargo.toml b/rust-runtime/aws-smithy-fuzz/Cargo.toml index a9f092865f9..faec250e611 100644 --- a/rust-runtime/aws-smithy-fuzz/Cargo.toml +++ b/rust-runtime/aws-smithy-fuzz/Cargo.toml @@ -1,3 +1,4 @@ +[workspace] [package] name = "aws-smithy-fuzz" version = "0.1.0" @@ -13,7 +14,6 @@ arbitrary = { version = "1.3.2", features = ["derive"] } bincode = "1" bytes = "1.7.1" cargo_toml = "0.20.4" -cbor = "0.4.1" cbor-diag = "0.1.12" clap = { version = "4.5.15", features = ["derive"] } ffi-support = "0.4.4" @@ -33,10 +33,6 @@ tower = { version = "0.4.13", features = ["util"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" -[dev-dependencies] -aws-smithy-http-server = { path = "../aws-smithy-http-server" } - - [profile.release] debug = true diff --git a/rust-runtime/aws-smithy-fuzz/src/lib.rs b/rust-runtime/aws-smithy-fuzz/src/lib.rs index e23864fc2cc..4f1287df75d 100644 --- a/rust-runtime/aws-smithy-fuzz/src/lib.rs +++ b/rust-runtime/aws-smithy-fuzz/src/lib.rs @@ -6,6 +6,7 @@ /* Automatically managed default lints */ #![cfg_attr(docsrs, feature(doc_auto_cfg))] /* End of automatically managed default lints */ +#![cfg(not(windows))] use libloading::{os, Library, Symbol}; use std::error::Error; use tokio::sync::mpsc::Sender; diff --git a/rust-runtime/aws-smithy-fuzz/src/main.rs b/rust-runtime/aws-smithy-fuzz/src/main.rs index c62a84f4ed0..edc088f2daf 100644 --- a/rust-runtime/aws-smithy-fuzz/src/main.rs +++ b/rust-runtime/aws-smithy-fuzz/src/main.rs @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +#![cfg(not(windows))] + use aws_smithy_fuzz::{FuzzResult, FuzzTarget, HttpRequest}; use serde::{Deserialize, Serialize}; use serde_json::Value; diff --git a/rust-runtime/aws-smithy-fuzz/src/types.rs b/rust-runtime/aws-smithy-fuzz/src/types.rs index 64b4dfcec87..220a6e1b668 100644 --- a/rust-runtime/aws-smithy-fuzz/src/types.rs +++ b/rust-runtime/aws-smithy-fuzz/src/types.rs @@ -101,10 +101,7 @@ impl Debug for TryString<'_> { let try_cbor = cbor_diag::parse_bytes(self.0); let str_rep = match try_cbor { Ok(repr) => repr.to_diag_pretty(), - Err(e) => { - eprintln!("not cbor: {}", e); - String::from_utf8_lossy(self.0).to_string() - } + Err(_e) => String::from_utf8_lossy(self.0).to_string(), }; write!(f, "\"{}\"", str_rep) } From e3bf7b3e2c4e485f618f3f7433f5b2530b87b191 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Mon, 10 Feb 2025 15:16:04 -0500 Subject: [PATCH 12/13] Apply suggestions from code review Co-authored-by: david-perez --- .../smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt | 2 +- rust-runtime/aws-smithy-fuzz/README.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt index 80c7e1ef690..cb4ba107f7e 100644 --- a/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt +++ b/fuzzgen/src/main/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPlugin.kt @@ -117,7 +117,7 @@ class FuzzHarnessBuildPlugin : SmithyBuildPlugin { } /** - * Generate a corpus of words used within the model to see the dictionary + * Generate a corpus of words used within the model to seed the dictionary */ fun corpus( model: Model, diff --git a/rust-runtime/aws-smithy-fuzz/README.md b/rust-runtime/aws-smithy-fuzz/README.md index 4bbcc662286..52b716bf6ef 100644 --- a/rust-runtime/aws-smithy-fuzz/README.md +++ b/rust-runtime/aws-smithy-fuzz/README.md @@ -1,23 +1,23 @@ # aws-smithy-fuzz -AWS Smithy fuzz contains a set of utilities for writing fuzz tests against smithy-rs servers. This is part of our tooling to perform differential fuzzing against different versions of smithy-rs-server. +AWS Smithy fuzz contains a set of utilities for writing fuzz tests against smithy-rs servers. This is part of our tooling to perform differential fuzzing against different versions of smithy-rs servers. ## Installation 1. Install `cargo afl`: `cargo install cargo-afl` 2. Install the AFL runtime: `cargo afl config --build` 2. Install the smithy CLI: -2. Install aws-smithy-fuzz: +2. Install `aws-smithy-fuzz`: - Locally: `cargo afl install --path .` - From crates.io: cargo afl install aws-smithy-fuzz > **IMPORTANT**: This package MUST be installed with `cargo afl install` (instead of `cargo install`). If you do not use `afl`, > you will get linking errors. ## Usage -This contains a library + a CLI tool to fuzz smithy servers. The library allows setting up a given Smithy server implementation as a `cdylib`. This allows two different versions two by dynamically linked at runtime and executed by the fuzzer. +This contains a library + a CLI tool to fuzz smithy-rs servers. The library allows setting up a given smithy-rs server implementation as a `cdylib`. This allows two different versions two by dynamically linked at runtime and executed by the fuzzer. Each of these components are meant to be usable independently: 1. The public APIs of `aws-smithy-fuzz` can be used to write your own fuzz targets without code generation. -2. The `lexicon.json` can be used outside of this project to seed a fuzzer from a smithy model +2. The `lexicon.json` can be used outside of this project to seed a fuzzer from a Smithy model. 3. The fuzz driver can be used on other fuzz targets. ### Setup From 3bd3159efecf6021264089dbb50c8ebd06973c07 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Mon, 10 Feb 2025 15:18:59 -0500 Subject: [PATCH 13/13] Ensure fuzz unit tests run in CI --- .github/workflows/ci.yml | 2 ++ .../core/smithy/generators/CargoTomlGenerator.kt | 1 - .../rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt | 1 - rust-runtime/aws-smithy-fuzz/src/main.rs | 2 -- tools/ci-scripts/check-fuzzgen | 10 ++++++++++ 5 files changed, 12 insertions(+), 4 deletions(-) create mode 100755 tools/ci-scripts/check-fuzzgen diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0723f9c20f5..a6ba237182d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,8 @@ jobs: fetch-depth: 0 - action: check-sdk-codegen-unit-tests runner: ubuntu-latest + - action: check-fuzzgen + runner: ubuntu-latest - action: check-server-codegen-integration-tests runner: smithy_ubuntu-latest_8-core - action: check-server-codegen-integration-tests-python diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt index cebdfbfab7d..e5f49163489 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CargoTomlGenerator.kt @@ -113,7 +113,6 @@ class CargoTomlGenerator( "features" to cargoFeatures.toMap(), ).deepMergeWith(manifestCustomizations) - println(cargoToml) writer.writeWithNoFormatting(TomlWriter().write(cargoToml)) } } diff --git a/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt b/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt index 5522c13fee3..f90e801de3c 100644 --- a/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt +++ b/fuzzgen/src/test/kotlin/software/amazon/smithy/rust/codegen/fuzz/FuzzHarnessBuildPluginTest.kt @@ -29,7 +29,6 @@ class FuzzHarnessBuildPluginTest() { operations: [SayHello], version: "1" } - @optionalAuth operation SayHello { input: TestInput } structure TestInput { foo: String, diff --git a/rust-runtime/aws-smithy-fuzz/src/main.rs b/rust-runtime/aws-smithy-fuzz/src/main.rs index edc088f2daf..0f2101974e1 100644 --- a/rust-runtime/aws-smithy-fuzz/src/main.rs +++ b/rust-runtime/aws-smithy-fuzz/src/main.rs @@ -446,8 +446,6 @@ fn fuzz(args: FuzzArgs) { let config: FuzzConfig = serde_json::from_str(&config).unwrap(); if args.enter_fuzzing_loop { let libraries = force_load_libraries(&config.targets); - //let target = fs::File::create("log.txt").unwrap(); - //let fuzzing_log = BufWriter::new(target); enter_fuzz_loop(libraries, None) } else { eprintln!( diff --git a/tools/ci-scripts/check-fuzzgen b/tools/ci-scripts/check-fuzzgen new file mode 100755 index 00000000000..3a4a167608a --- /dev/null +++ b/tools/ci-scripts/check-fuzzgen @@ -0,0 +1,10 @@ +#!/bin/bash +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# + +set -eux +cd smithy-rs + +./gradlew fuzzgen:test