diff --git a/Cargo.lock b/Cargo.lock index ffbf65631..16a2fe2a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2598,6 +2598,14 @@ dependencies = [ "tracing", ] +[[package]] +name = "swift-demangler" +version = "0.1.0" +source = "git+https://github.com/Vector35/swift-demangler.git#b0b98bfa446284632eafe23eddcc93e5098534e2" +dependencies = [ + "cmake", +] + [[package]] name = "syn" version = "2.0.104" @@ -3454,6 +3462,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "workflow_swift" +version = "0.1.0" +dependencies = [ + "binaryninja", + "binaryninjacore-sys", + "swift-demangler", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "workflow_swift-static" +version = "0.1.0" +dependencies = [ + "binaryninja", + "binaryninjacore-sys", + "swift-demangler", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index ca260b9db..5cf7ac75e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ members = [ "plugins/workflow_objc/demo", "plugins/bntl_utils", "plugins/bntl_utils/cli", + "plugins/workflow_swift", + "plugins/workflow_swift/demo", ] [workspace.dependencies] diff --git a/arch/arm64/arch_arm64.cpp b/arch/arm64/arch_arm64.cpp index b71e79e82..a56ed35b7 100644 --- a/arch/arm64/arch_arm64.cpp +++ b/arch/arm64/arch_arm64.cpp @@ -2765,6 +2765,115 @@ class AppleArm64SystemCallConvention : public CallingConvention virtual bool IsEligibleForHeuristics() override { return false; } }; + +// Swift calling convention for ARM64. +// +// The Swift ABI repurposes three callee-saved registers for implicit parameters: +// x20 - self (swiftself): context/self parameter, passed as the last integer argument +// x21 - error (swifterror): caller initializes to zero, callee sets to error pointer on throw +// x22 - async context (swiftasync): implicit async context for async functions +// +// Each combination of these three properties requires a distinct calling convention, +// since it changes which registers are arguments, callee-saved, or implicitly defined. +// The naming scheme is "swift" with optional suffixes: "-self", "-throws", "-async". +class SwiftArm64CallingConvention : public CallingConvention +{ + bool m_hasSelf; + bool m_throws; + bool m_isAsync; + +public: + SwiftArm64CallingConvention(Architecture* arch, const string& name, bool hasSelf, bool throws, bool isAsync) + : CallingConvention(arch, name), m_hasSelf(hasSelf), m_throws(throws), m_isAsync(isAsync) + { + } + + virtual vector GetIntegerArgumentRegisters() override + { + // Self goes first because our function types list self as the first + // parameter. Parameters are assigned to registers sequentially, so + // this ensures self -> x20 and remaining args -> x0-x7. + // Async context and error go last as implicit trailing registers. + vector regs; + if (m_hasSelf) + regs.push_back(REG_X20); + regs.insert(regs.end(), {REG_X0, REG_X1, REG_X2, REG_X3, REG_X4, REG_X5, REG_X6, REG_X7}); + if (m_isAsync) + regs.push_back(REG_X22); + if (m_throws) + regs.push_back(REG_X21); + return regs; + } + + virtual vector GetFloatArgumentRegisters() override + { + return vector {REG_V0, REG_V1, REG_V2, REG_V3, REG_V4, REG_V5, REG_V6, REG_V7}; + } + + virtual vector GetCallerSavedRegisters() override + { + vector regs {REG_X0, REG_X1, REG_X2, REG_X3, REG_X4, REG_X5, REG_X6, REG_X7, REG_X8, + REG_X9, REG_X10, REG_X11, REG_X12, REG_X13, REG_X14, REG_X15, REG_X16, REG_X17, REG_X18, + REG_X30, REG_V0, REG_V1, REG_V2, REG_V3, REG_V4, REG_V5, REG_V6, REG_V7, REG_V16, REG_V17, + REG_V18, REG_V19, REG_V20, REG_V21, REG_V22, REG_V23, REG_V24, REG_V25, REG_V26, REG_V27, + REG_V28, REG_V29, REG_V30, REG_V31}; + // When used as special registers, they are no longer callee-saved. + if (m_hasSelf) + regs.push_back(REG_X20); + if (m_throws) + regs.push_back(REG_X21); + if (m_isAsync) + regs.push_back(REG_X22); + return regs; + } + + virtual vector GetCalleeSavedRegisters() override + { + vector regs {REG_X19, REG_X23, REG_X24, REG_X25, REG_X26, REG_X27, REG_X28, REG_X29}; + // Only include x20/x21/x22 as callee-saved when they are NOT repurposed. + if (!m_hasSelf) + regs.push_back(REG_X20); + if (!m_throws) + regs.push_back(REG_X21); + if (!m_isAsync) + regs.push_back(REG_X22); + return regs; + } + + virtual vector GetImplicitlyDefinedRegisters() override + { + vector regs; + // Throwing functions implicitly define x21 (the error register) on return. + if (m_throws) + regs.push_back(REG_X21); + return regs; + } + + virtual uint32_t GetIntegerReturnValueRegister() override { return REG_X0; } + + virtual uint32_t GetFloatReturnValueRegister() override { return REG_V0; } + + virtual vector GetRequiredArgumentRegisters() override + { + vector regs; + if (m_hasSelf) + regs.push_back(REG_X20); + if (m_throws) + regs.push_back(REG_X21); + if (m_isAsync) + regs.push_back(REG_X22); + return regs; + } + + virtual bool AreArgumentRegistersUsedForVarArgs() override { return false; } + + virtual bool IsEligibleForHeuristics() override + { + return m_hasSelf || m_throws || m_isAsync; + } +}; + + #define PAGE(x) (uint32_t)((x) >> 12) #define PAGE_OFF(x) (uint32_t)((x)&0xfff) #define PAGE_NO_OFF(x) (uint32_t)((x)&0xFFFFF000) @@ -3550,6 +3659,20 @@ extern "C" conv = new AppleArm64CallingConvention(arm64); arm64->RegisterCallingConvention(conv); + // Register Swift calling conventions (all combinations of self/throws/async). + for (int swiftFlags = 0; swiftFlags < 8; swiftFlags++) + { + bool hasSelf = (swiftFlags & 1) != 0; + bool throws = (swiftFlags & 2) != 0; + bool isAsync = (swiftFlags & 4) != 0; + string name = "swift"; + if (hasSelf) name += "-self"; + if (throws) name += "-throws"; + if (isAsync) name += "-async"; + conv = new SwiftArm64CallingConvention(arm64, name, hasSelf, throws, isAsync); + arm64->RegisterCallingConvention(conv); + } + for (uint32_t i = REG_X0; i <= REG_X28; i++) { if (i == REG_X16 || i == REG_X17 || i == REG_X18) // reserved by os. diff --git a/plugins/workflow_swift/CMakeLists.txt b/plugins/workflow_swift/CMakeLists.txt new file mode 100644 index 000000000..d9b027f32 --- /dev/null +++ b/plugins/workflow_swift/CMakeLists.txt @@ -0,0 +1,171 @@ +cmake_minimum_required(VERSION 3.15 FATAL_ERROR) + +project(workflow_swift) + +if(NOT BN_API_BUILD_EXAMPLES AND NOT BN_INTERNAL_BUILD) + if(NOT BN_API_PATH) + # If we have not already defined the API source directory try and find it. + find_path( + BN_API_PATH + NAMES binaryninjaapi.h + # List of paths to search for the clone of the api + HINTS ../../.. ../../binaryninja/api/ binaryninjaapi binaryninja-api $ENV{BN_API_PATH} + REQUIRED + ) + endif() + set(CARGO_STABLE_VERSION 1.91.1) + add_subdirectory(${BN_API_PATH} binaryninjaapi) +endif() + +file(GLOB_RECURSE PLUGIN_SOURCES CONFIGURE_DEPENDS + ${PROJECT_SOURCE_DIR}/Cargo.toml + ${PROJECT_SOURCE_DIR}/src/*.rs) + +if(CMAKE_BUILD_TYPE MATCHES Debug) + if(DEMO) + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/dev-demo) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --profile=dev-demo) + else() + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/debug) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target) + endif() +else() + if(DEMO) + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/release-demo) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --profile=release-demo) + else() + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/release) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --release) + endif() +endif() + +if(FORCE_COLORED_OUTPUT) + set(CARGO_OPTS ${CARGO_OPTS} --color always) +endif() + +if(DEMO) + set(CARGO_FEATURES --features demo --manifest-path ${PROJECT_SOURCE_DIR}/demo/Cargo.toml) + + set(OUTPUT_FILE_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}${PROJECT_NAME}_static${CMAKE_STATIC_LIBRARY_SUFFIX}) + set(OUTPUT_PDB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}${PROJECT_NAME}.pdb) + set(OUTPUT_FILE_PATH ${CMAKE_BINARY_DIR}/${OUTPUT_FILE_NAME}) + set(OUTPUT_PDB_PATH ${CMAKE_BINARY_DIR}/${OUTPUT_PDB_NAME}) + + set(BINJA_LIB_DIR $) +else() + # NOTE: --no-default-features is set to disable building artifacts used for testing + # NOTE: the linker is looking in the target dir and linking on it apparently. + set(CARGO_FEATURES "--no-default-features") + + set(OUTPUT_FILE_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}) + set(OUTPUT_PDB_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}${PROJECT_NAME}.pdb) + set(OUTPUT_FILE_PATH ${BN_CORE_PLUGIN_DIR}/${OUTPUT_FILE_NAME}) + set(OUTPUT_PDB_PATH ${BN_CORE_PLUGIN_DIR}/${OUTPUT_PDB_NAME}) + + set(BINJA_LIB_DIR ${BN_INSTALL_BIN_DIR}) +endif() + + +add_custom_target(${PROJECT_NAME} ALL DEPENDS ${OUTPUT_FILE_PATH}) +add_dependencies(${PROJECT_NAME} binaryninjaapi) +get_target_property(BN_API_SOURCE_DIR binaryninjaapi SOURCE_DIR) +list(APPEND CMAKE_MODULE_PATH "${BN_API_SOURCE_DIR}/cmake") +find_package(BinaryNinjaCore REQUIRED) + +set_property(TARGET ${PROJECT_NAME} PROPERTY OUTPUT_FILE_PATH ${OUTPUT_FILE_PATH}) + +# Add the whole api to the depends too +file(GLOB_RECURSE API_SOURCES CONFIGURE_DEPENDS + ${PROJECT_SOURCE_DIR}/../../binaryninjacore.h + ${PROJECT_SOURCE_DIR}/../../rust/binaryninjacore-sys/build.rs + ${PROJECT_SOURCE_DIR}/../../rust/binaryninjacore-sys/Cargo.toml + ${PROJECT_SOURCE_DIR}/../../rust/binaryninjacore-sys/src/*.rs + ${PROJECT_SOURCE_DIR}/../../rust/Cargo.toml + ${PROJECT_SOURCE_DIR}/../../rust/src/*/*.rs) + +find_program(RUSTUP_PATH rustup REQUIRED HINTS ~/.cargo/bin) +set(RUSTUP_COMMAND ${RUSTUP_PATH} run ${CARGO_STABLE_VERSION} cargo) + +if(APPLE) + if(UNIVERSAL) + if(CMAKE_BUILD_TYPE MATCHES Debug) + if(DEMO) + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/dev-demo/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/dev-demo/${OUTPUT_FILE_NAME}) + else() + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/debug/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/debug/${OUTPUT_FILE_NAME}) + endif() + else() + if(DEMO) + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/release-demo/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/release-demo/${OUTPUT_FILE_NAME}) + else() + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/release/${OUTPUT_FILE_NAME}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/release/${OUTPUT_FILE_NAME}) + endif() + endif() + + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean --target=aarch64-apple-darwin ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean --target=x86_64-apple-darwin ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build --target=aarch64-apple-darwin ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build --target=x86_64-apple-darwin ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND lipo -create ${AARCH64_LIB_PATH} ${X86_64_LIB_PATH} -output ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BINJA_LIB_DIR} + ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + endif() +elseif(WIN32) + if(DEMO) + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_PDB_NAME} ${OUTPUT_PDB_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) + endif() +else() + add_custom_command( + OUTPUT ${OUTPUT_FILE_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} clean ${CARGO_OPTS} --package binaryninjacore-sys + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BINJA_LIB_DIR} ${RUSTUP_COMMAND} build ${CARGO_OPTS} ${CARGO_FEATURES} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_FILE_NAME} ${OUTPUT_FILE_PATH} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES} + ) +endif() diff --git a/plugins/workflow_swift/Cargo.toml b/plugins/workflow_swift/Cargo.toml new file mode 100644 index 000000000..8ed4f8c46 --- /dev/null +++ b/plugins/workflow_swift/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "workflow_swift" +version = "0.1.0" +edition = "2021" +license = "BSD-3-Clause" +publish = false + +[lib] +crate-type = ["staticlib", "cdylib"] + +[features] +demo = [] + +[dependencies] +binaryninja = { workspace = true } +binaryninjacore-sys.workspace = true +tracing = "0.1" +thiserror = "2.0" +swift-demangler = { git = "https://github.com/Vector35/swift-demangler.git" } diff --git a/plugins/workflow_swift/build.rs b/plugins/workflow_swift/build.rs new file mode 100644 index 000000000..9006f16a6 --- /dev/null +++ b/plugins/workflow_swift/build.rs @@ -0,0 +1,25 @@ +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(target_os = "linux")] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } + + #[cfg(target_os = "macos")] + { + let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME not set"); + let lib_name = crate_name.replace('-', "_"); + println!( + "cargo::rustc-link-arg=-Wl,-install_name,@rpath/lib{}.dylib", + lib_name + ); + } +} diff --git a/plugins/workflow_swift/demo/Cargo.toml b/plugins/workflow_swift/demo/Cargo.toml new file mode 100644 index 000000000..a1e46891f --- /dev/null +++ b/plugins/workflow_swift/demo/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "workflow_swift-static" +version = "0.1.0" +edition = "2021" +license = "BSD-3-Clause" +publish = false + +[lib] +crate-type = ["staticlib"] +path = "../src/lib.rs" + +[features] +demo = [] + +[dependencies] +binaryninja = { workspace = true, features = ["demo"] } +binaryninjacore-sys.workspace = true +tracing = "0.1" +thiserror = "2.0" +swift-demangler = { git = "https://github.com/Vector35/swift-demangler.git" } diff --git a/plugins/workflow_swift/demo/build.rs b/plugins/workflow_swift/demo/build.rs new file mode 100644 index 000000000..ed6cec7d2 --- /dev/null +++ b/plugins/workflow_swift/demo/build.rs @@ -0,0 +1,15 @@ +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(not(target_os = "windows"))] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } +} diff --git a/plugins/workflow_swift/src/demangler/function_type.rs b/plugins/workflow_swift/src/demangler/function_type.rs new file mode 100644 index 000000000..908f5940c --- /dev/null +++ b/plugins/workflow_swift/src/demangler/function_type.rs @@ -0,0 +1,330 @@ +use binaryninja::architecture::{ArchitectureExt, CoreArchitecture, Register}; +use binaryninja::confidence::Conf; +use binaryninja::rc::Ref; +use binaryninja::types::{FunctionParameter, Type}; +use binaryninja::variable::{Variable, VariableSourceType}; +use swift_demangler::{ + Accessor, AccessorKind, ConstructorKind, HasFunctionSignature, HasModule, Metadata, + MetadataKind, Symbol, +}; + +use super::type_reconstruction::{make_named_type_ref, TypeRefExt}; + +/// Architecture-specific register assignments for Swift implicit parameters. +struct PlatformAbi { + arch: CoreArchitecture, + error_reg: &'static str, + async_context_reg: &'static str, +} + +impl PlatformAbi { + fn for_arch(arch: &CoreArchitecture) -> Option { + match arch.name().as_ref() { + "aarch64" => Some(Self { + arch: *arch, + error_reg: "x21", + async_context_reg: "x22", + }), + _ => None, + } + } + + fn error_location(&self) -> Option { + self.register_variable(self.error_reg) + } + + fn async_context_location(&self) -> Option { + self.register_variable(self.async_context_reg) + } + + fn register_variable(&self, name: &str) -> Option { + let reg = self.arch.register_by_name(name)?; + Some(Variable::new( + VariableSourceType::RegisterVariableSourceType, + 0, + reg.id().0 as i64, + )) + } +} + +/// Swift calling convention builder. +/// +/// Tracks which implicit parameters (self, error, async context) are present, +/// constructs the corresponding `FunctionParameter`s, and resolves the correct +/// architecture-specific calling convention when building the final function type. +struct CallingConvention { + arch: CoreArchitecture, + abi: Option, + flags: u8, + leading_params: Vec, + trailing_params: Vec, +} + +impl CallingConvention { + const SELF: u8 = 1; + const THROWS: u8 = 2; + const ASYNC: u8 = 4; + + fn for_arch(arch: &CoreArchitecture) -> Self { + Self { + arch: *arch, + abi: PlatformAbi::for_arch(arch), + flags: 0, + leading_params: Vec::new(), + trailing_params: Vec::new(), + } + } + + /// Mark this as a concrete instance method with self in x20. + fn set_self(&mut self, module: Option<&str>, containing_type: &str) { + self.flags |= Self::SELF; + let named_ty = make_named_type_ref(module, containing_type); + let self_ty = Type::pointer(&self.arch, &named_ty); + self.leading_params.push(FunctionParameter { + ty: self_ty.into(), + name: "self".to_string(), + location: None, + }); + } + + /// Optionally mark as a concrete instance method if `containing_type` is `Some`. + fn set_self_if(&mut self, module: Option<&str>, containing_type: Option<&str>) { + if let Some(ct) = containing_type { + self.set_self(module, ct); + } + } + + /// Mark this as a protocol witness method. Self is in x20 (swift-self) + /// like concrete methods. The self type metadata and witness table are + /// appended as trailing arguments after the explicit params. + fn set_protocol_self(&mut self, module: Option<&str>, containing_type: &str) { + self.set_self(module, containing_type); + let void_ptr = Type::pointer(&self.arch, &Type::void()); + self.trailing_params.push(FunctionParameter { + ty: void_ptr.clone().into(), + name: "selfMetadata".to_string(), + location: None, + }); + self.trailing_params.push(FunctionParameter { + ty: void_ptr.into(), + name: "selfWitnessTable".to_string(), + location: None, + }); + } + + fn set_protocol_self_if(&mut self, module: Option<&str>, containing_type: Option<&str>) { + if let Some(ct) = containing_type { + self.set_protocol_self(module, ct); + } + } + + /// Mark this as a throwing function. + fn set_throws(&mut self) { + self.flags |= Self::THROWS; + } + + /// Mark this as an async function. + fn set_async(&mut self) { + self.flags |= Self::ASYNC; + } + + /// Build the final function type. + /// + /// Prepends `self` before `params` and appends error/async context after, + /// then looks up the appropriate calling convention by name on the architecture. + fn build_type(self, ret_type: &Type, params: Vec) -> Ref { + let cc_name = self.cc_name(); + + let mut all_params = self.leading_params; + all_params.extend(params); + all_params.extend(self.trailing_params); + + if self.flags & Self::THROWS != 0 { + let error_ty = Type::pointer(&self.arch, &make_named_type_ref(Some("Swift"), "Error")); + all_params.push(FunctionParameter { + ty: error_ty.into(), + name: "error".to_string(), + location: self.abi.as_ref().and_then(|a| a.error_location()), + }); + } + + if self.flags & Self::ASYNC != 0 { + let ptr_ty = Type::pointer(&self.arch, &Type::void()); + all_params.push(FunctionParameter { + ty: ptr_ty.into(), + name: "asyncContext".to_string(), + location: self.abi.as_ref().and_then(|a| a.async_context_location()), + }); + } + + if let Some(cc) = self.arch.calling_convention_by_name(&cc_name) { + Type::function_with_opts(ret_type, &all_params, false, cc, Conf::new(0, 0)) + } else { + Type::function(ret_type, all_params, false) + } + } + + fn cc_name(&self) -> String { + let mut name = String::from("swift"); + if self.flags & Self::SELF != 0 { + name.push_str("-self"); + } + if self.flags & Self::THROWS != 0 { + name.push_str("-throws"); + } + if self.flags & Self::ASYNC != 0 { + name.push_str("-async"); + } + name + } +} + +/// Build a Binary Ninja function type from a parsed Swift symbol. +/// +/// Returns `None` if the symbol does not have a function signature (e.g. metadata, variables). +/// Thunks are intentionally excluded for now. +pub fn build_function_type(symbol: &Symbol, arch: &CoreArchitecture) -> Option> { + match symbol { + Symbol::Accessor(a) => return build_accessor_type(a, arch), + Symbol::Metadata(m) => return build_metadata_function_type(m, arch), + Symbol::Attributed(a) => return build_function_type(&a.inner, arch), + Symbol::Specialization(s) => return build_function_type(&s.inner, arch), + Symbol::Suffixed(s) => return build_function_type(&s.inner, arch), + _ => {} + } + + let mut cc = CallingConvention::for_arch(arch); + + // Determine implicit `self` parameter for instance methods/constructors. + match symbol { + Symbol::Function(f) if f.is_method() && !f.is_static() => { + if f.containing_type_is_protocol() { + cc.set_protocol_self_if(f.module(), f.containing_type()); + } else { + cc.set_self_if(f.module(), f.containing_type()); + } + } + Symbol::Constructor(c) if c.kind() != ConstructorKind::Allocating => { + cc.set_self_if(c.module(), c.containing_type()); + } + Symbol::Destructor(d) => { + cc.set_self_if(d.module(), d.containing_type()); + return Some(cc.build_type(&Type::void(), vec![])); + } + _ => {} + }; + + let (sig, labels) = match symbol { + Symbol::Function(f) => (f.signature(), f.labels()), + Symbol::Constructor(c) => (c.signature(), c.labels()), + Symbol::Closure(c) => (c.signature(), vec![]), + _ => (None, vec![]), + }; + let sig = sig?; + + if sig.is_throwing() { + cc.set_throws(); + } + if sig.is_async() { + cc.set_async(); + } + + let params: Vec = sig + .parameters() + .iter() + .enumerate() + .filter_map(|(i, p)| { + let ty = p.type_ref.to_bn_type(arch)?; + let name = labels + .get(i) + .copied() + .flatten() + .or(p.label) + .unwrap_or("") + .to_string(); + Some(FunctionParameter { + ty: ty.into(), + name, + location: None, + }) + }) + .collect(); + + let ret_type = sig + .return_type() + .and_then(|rt| rt.to_bn_type(arch)) + .unwrap_or_else(Type::void); + + Some(cc.build_type(&ret_type, params)) +} + +/// Build a function type for a property accessor from its property type. +fn build_accessor_type(accessor: &Accessor, arch: &CoreArchitecture) -> Option> { + let prop_ty = accessor + .property_type() + .and_then(|pt| pt.to_bn_type(arch))?; + + let mut cc = CallingConvention::for_arch(arch); + + // Non-static instance accessors take `self` as a parameter. + if !accessor.is_static() { + if let Some(ct) = accessor.containing_type() { + cc.set_self(accessor.module(), &ct); + } + } + + match accessor.kind() { + // Getter-like: (self) -> PropertyType + AccessorKind::Getter | AccessorKind::GlobalGetter | AccessorKind::Read => { + Some(cc.build_type(&prop_ty, vec![])) + } + + // Setter-like: (self, newValue: PropertyType) -> Void + AccessorKind::Setter + | AccessorKind::WillSet + | AccessorKind::DidSet + | AccessorKind::Modify + | AccessorKind::Init => { + let params = vec![FunctionParameter { + ty: prop_ty.into(), + name: "newValue".to_string(), + location: None, + }]; + Some(cc.build_type(&Type::void(), params)) + } + + _ => None, + } +} + +/// Build a function type for Swift runtime metadata functions. +fn build_metadata_function_type(metadata: &Metadata, arch: &CoreArchitecture) -> Option> { + let void_ptr = Type::pointer(arch, &Type::void()); + + match metadata.kind() { + // Type metadata accessor: void* fn() + // Returns a pointer to the type metadata singleton. + MetadataKind::AccessFunction | MetadataKind::CanonicalSpecializedGenericAccessFunction => { + Some(Type::function(&void_ptr, vec![], false)) + } + + // Method lookup function: void* fn(void* metadata, void* method) + MetadataKind::MethodLookupFunction => { + let params = vec![ + FunctionParameter { + ty: void_ptr.clone().into(), + name: "metadata".to_string(), + location: None, + }, + FunctionParameter { + ty: void_ptr.clone().into(), + name: "method".to_string(), + location: None, + }, + ]; + Some(Type::function(&void_ptr, params, false)) + } + + _ => None, + } +} diff --git a/plugins/workflow_swift/src/demangler/mod.rs b/plugins/workflow_swift/src/demangler/mod.rs new file mode 100644 index 000000000..657c62c34 --- /dev/null +++ b/plugins/workflow_swift/src/demangler/mod.rs @@ -0,0 +1,56 @@ +mod function_type; +mod name; +mod type_reconstruction; + +use binaryninja::architecture::CoreArchitecture; +use binaryninja::binary_view::BinaryView; +use binaryninja::demangle::CustomDemangler; +use binaryninja::rc::Ref; +use binaryninja::settings::{QueryOptions, Settings}; +use binaryninja::types::{QualifiedName, Type}; + +fn should_extract_types(view: Option<&BinaryView>) -> bool { + let mut opts = match view { + Some(v) => QueryOptions::new_with_view(v), + None => QueryOptions::new(), + }; + Settings::new().get_bool_with_opts(crate::SETTING_EXTRACT_TYPES, &mut opts) +} + +pub struct SwiftDemangler; + +impl CustomDemangler for SwiftDemangler { + fn is_mangled_string(&self, name: &str) -> bool { + name.starts_with("$s") + || name.starts_with("_$s") + || name.starts_with("$S") + || name.starts_with("_$S") + || name.starts_with("$e") + || name.starts_with("_$e") + || name.starts_with("_T") + } + + fn demangle( + &self, + arch: &CoreArchitecture, + name: &str, + view: Option>, + ) -> Option<(QualifiedName, Option>)> { + let ctx = swift_demangler::Context::new(); + let symbol = swift_demangler::Symbol::parse(&ctx, name)?; + + if should_extract_types(view.as_deref()) { + let ty = function_type::build_function_type(&symbol, arch); + let qname = if ty.is_some() { + name::build_short_name(&symbol) + } else { + None + } + .unwrap_or_else(|| QualifiedName::from(symbol.display())); + Some((qname, ty)) + } else { + let qname = QualifiedName::from(symbol.display()); + Some((qname, None)) + } + } +} diff --git a/plugins/workflow_swift/src/demangler/name.rs b/plugins/workflow_swift/src/demangler/name.rs new file mode 100644 index 000000000..fd39d8b7c --- /dev/null +++ b/plugins/workflow_swift/src/demangler/name.rs @@ -0,0 +1,84 @@ +use binaryninja::types::QualifiedName; +use swift_demangler::{ConstructorKind, DestructorKind, HasModule, Symbol}; + +/// Push a module name into the name parts, skipping `__C` (Swift's internal +/// module for C/Objective-C imports). +fn push_module(parts: &mut Vec, module: Option<&str>) { + if let Some(m) = module { + if m != "__C" { + parts.push(m.to_string()); + } + } +} + +/// Build a qualified name from a symbol's components, omitting parameter types +/// and return type since those are represented directly in the function's type +/// +/// Returns `None` for symbol kinds that don't benefit from shortening (e.g. +/// variables, thunks), in which case the caller should fall back to `symbol.display()`. +pub fn build_short_name(symbol: &swift_demangler::Symbol) -> Option { + let mut parts: Vec = Vec::new(); + + match symbol { + Symbol::Function(f) => { + push_module(&mut parts, f.module()); + if let Some(ct) = f.containing_type() { + parts.push(ct.to_string()); + } + parts.push(f.full_name()); + } + Symbol::Constructor(c) => { + push_module(&mut parts, c.module()); + if let Some(ct) = c.containing_type() { + parts.push(ct.to_string()); + } + let init_name = match c.kind() { + ConstructorKind::Allocating => "__allocating_init", + ConstructorKind::Regular => "init", + }; + let labels: Vec = c + .labels() + .iter() + .map(|l| { + l.map(|s| format!("{s}:")) + .unwrap_or_else(|| "_:".to_string()) + }) + .collect(); + parts.push(format!("{init_name}({})", labels.join(""))); + } + Symbol::Destructor(d) => { + push_module(&mut parts, d.module()); + if let Some(ct) = d.containing_type() { + parts.push(ct.to_string()); + } + let deinit_name = match d.kind() { + DestructorKind::Deallocating => "__deallocating_deinit", + DestructorKind::IsolatedDeallocating => "__isolated_deallocating_deinit", + DestructorKind::Regular => "deinit", + }; + parts.push(deinit_name.to_string()); + } + // Wrappers: combine the wrapper's display with the inner function's short name. + Symbol::Attributed(_) | Symbol::Specialization(_) => { + let inner = match symbol { + Symbol::Attributed(a) => &*a.inner, + Symbol::Specialization(s) => &*s.inner, + _ => unreachable!(), + }; + return build_short_name(inner).map(|inner_name| { + QualifiedName::from(format!("{}{}", symbol.display(), inner_name)) + }); + } + Symbol::Suffixed(s) => { + return build_short_name(&s.inner) + .map(|inner_name| QualifiedName::from(format!("{} {}", inner_name, s.suffix))); + } + _ => return None, + } + + if parts.is_empty() { + return None; + } + + Some(QualifiedName::from(parts.join("."))) +} diff --git a/plugins/workflow_swift/src/demangler/type_reconstruction.rs b/plugins/workflow_swift/src/demangler/type_reconstruction.rs new file mode 100644 index 000000000..3a3ba3e74 --- /dev/null +++ b/plugins/workflow_swift/src/demangler/type_reconstruction.rs @@ -0,0 +1,204 @@ +use binaryninja::architecture::{Architecture, CoreArchitecture}; +use binaryninja::rc::Ref; +use binaryninja::types::{NamedTypeReference, NamedTypeReferenceClass, QualifiedName, Type}; +use swift_demangler::{TypeKind, TypeRef}; + +pub(crate) trait TypeRefExt { + fn to_bn_type(&self, arch: &CoreArchitecture) -> Option>; +} + +impl TypeRefExt for TypeRef<'_> { + fn to_bn_type(&self, arch: &CoreArchitecture) -> Option> { + match self.kind() { + TypeKind::Named(named) => { + let name = named.name()?; + let module = named.module(); + + // __C is Swift's internal module for C/Objective-C imports; drop it. + let module = match module { + Some("__C") => None, + other => other, + }; + + // Map Swift standard library primitive types to BN primitives. + // Bound generic types (e.g., Optional) are never primitives. + if module == Some("Swift") && !named.is_generic() { + if let Some(ty) = swift_primitive(name, arch) { + return Some(ty); + } + } + + let ntr = if named.is_generic() { + // Include generic arguments in the type name. + let args: Vec = + named.generic_args().iter().map(|a| a.display()).collect(); + let full_name = format!("{}<{}>", name, args.join(", ")); + make_named_type_ref(module, &full_name) + } else { + make_named_type_ref(module, name) + }; + + // Class types (including ObjC classes) are reference types — + // always a pointer at the ABI level. + if named.is_class() { + Some(Type::pointer(arch, &ntr)) + } else { + Some(ntr) + } + } + + TypeKind::Function(func_type) => { + let params: Vec<_> = func_type + .parameters() + .iter() + .filter_map(|p| { + let ty = p.type_ref.to_bn_type(arch)?; + let name = p.label.unwrap_or("").to_string(); + Some(binaryninja::types::FunctionParameter { + ty: ty.into(), + name, + location: None, + }) + }) + .collect(); + + let ret_type = func_type + .return_type() + .and_then(|rt| rt.to_bn_type(arch)) + .unwrap_or_else(Type::void); + + Some(Type::function(&ret_type, params, false)) + } + + TypeKind::Tuple(elements) => { + if elements.is_empty() { + // () is Swift.Void + Some(Type::void()) + } else { + let display = self.display(); + Some(make_named_type_ref(Some("Swift"), &display)) + } + } + + TypeKind::Optional(inner) => { + let display = inner.display(); + Some(make_named_type_ref(Some("Swift"), &display)) + } + + TypeKind::Array(inner) => { + let display = inner.display(); + let label = format!("[{display}]"); + Some(make_named_type_ref(Some("Swift"), &label)) + } + + TypeKind::Dictionary { key, value } => { + let key_display = key.display(); + let value_display = value.display(); + let label = format!("[{key_display} : {value_display}]"); + Some(make_named_type_ref(Some("Swift"), &label)) + } + + TypeKind::InOut(inner) => { + let inner_ty = inner.to_bn_type(arch)?; + Some(Type::pointer(arch, &inner_ty)) + } + + TypeKind::Metatype(_) => { + // Metatype is an opaque pointer-sized value at runtime. + Some(Type::pointer_of_width( + &Type::void(), + arch.address_size(), + false, + false, + None, + )) + } + + TypeKind::GenericParam { .. } => { + // Generic parameters are opaque pointer-sized values at runtime. + Some(Type::pointer_of_width( + &Type::int(1, false), + arch.address_size(), + false, + false, + None, + )) + } + + // Ownership wrappers: unwrap and recurse. + TypeKind::Shared(inner) + | TypeKind::Owned(inner) + | TypeKind::Sending(inner) + | TypeKind::Isolated(inner) + | TypeKind::NoDerivative(inner) => inner.to_bn_type(arch), + + TypeKind::Weak(inner) | TypeKind::Unowned(inner) => inner.to_bn_type(arch), + + TypeKind::DynamicSelf(inner) => inner.to_bn_type(arch), + + TypeKind::ConstrainedExistential(inner) => inner.to_bn_type(arch), + + TypeKind::Any => { + // Swift.Any is an existential container (pointer-sized at the ABI level). + Some(make_named_type_ref(Some("Swift"), "Any")) + } + + TypeKind::Existential(protocols) => { + if protocols.len() == 1 { + protocols[0].to_bn_type(arch) + } else { + None + } + } + + TypeKind::Generic { inner, .. } => inner.to_bn_type(arch), + + // Types we can't meaningfully represent. + TypeKind::Error + | TypeKind::Builtin(_) + | TypeKind::BuiltinFixedArray { .. } + | TypeKind::ImplFunction(_) + | TypeKind::Pack(_) + | TypeKind::ValueGeneric(_) + | TypeKind::CompileTimeLiteral(_) + | TypeKind::AssociatedType { .. } + | TypeKind::Opaque { .. } + | TypeKind::SILBox { .. } + | TypeKind::Other(_) => None, + } + } +} + +/// Map a Swift standard library type name to a primitive type. +fn swift_primitive(name: &str, arch: &CoreArchitecture) -> Option> { + match name { + "Int" => Some(Type::int(arch.address_size(), true)), + "UInt" => Some(Type::int(arch.address_size(), false)), + "Int8" => Some(Type::int(1, true)), + "Int16" => Some(Type::int(2, true)), + "Int32" => Some(Type::int(4, true)), + "Int64" => Some(Type::int(8, true)), + "UInt8" => Some(Type::int(1, false)), + "UInt16" => Some(Type::int(2, false)), + "UInt32" => Some(Type::int(4, false)), + "UInt64" => Some(Type::int(8, false)), + "Float" => Some(Type::float(4)), + "Double" => Some(Type::float(8)), + "Float80" => Some(Type::float(10)), + "Bool" => Some(Type::bool()), + _ => None, + } +} + +/// Create a named type reference from an optional module and a type name. +/// +/// `__C` (Swift's internal module for C/Objective-C imports) is dropped since +/// it is not meaningful to users. +pub(crate) fn make_named_type_ref(module: Option<&str>, name: &str) -> Ref { + let qname = match module { + Some("__C") | None => QualifiedName::from(name), + Some(module) => QualifiedName::from(format!("{module}.{name}")), + }; + let ntr = NamedTypeReference::new(NamedTypeReferenceClass::UnknownNamedTypeClass, qname); + Type::named_type(&ntr) +} diff --git a/plugins/workflow_swift/src/lib.rs b/plugins/workflow_swift/src/lib.rs new file mode 100644 index 000000000..a04f36599 --- /dev/null +++ b/plugins/workflow_swift/src/lib.rs @@ -0,0 +1,36 @@ +mod demangler; + +use binaryninja::add_optional_plugin_dependency; +use binaryninja::demangle::Demangler; +use binaryninja::settings::Settings; +use demangler::SwiftDemangler; + +pub const SETTING_EXTRACT_TYPES: &str = "analysis.swift.extractTypesFromMangledNames"; + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn CorePluginDependencies() { + add_optional_plugin_dependency("arch_x86"); + add_optional_plugin_dependency("arch_arm64"); +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn CorePluginInit() -> bool { + binaryninja::tracing_init!("Plugin.Swift"); + + let settings = Settings::new(); + settings.register_setting_json( + SETTING_EXTRACT_TYPES, + r#"{ + "title" : "Extract Types from Mangled Names", + "type" : "boolean", + "default" : true, + "description" : "Extract parameter and return type information from Swift mangled names and apply them to function signatures. When disabled, only the demangled name is applied." + }"#, + ); + + Demangler::register("Swift", SwiftDemangler); + + true +} diff --git a/rust/src/architecture.rs b/rust/src/architecture.rs index 6b644e88e..e854d53b8 100644 --- a/rust/src/architecture.rs +++ b/rust/src/architecture.rs @@ -1262,6 +1262,20 @@ pub trait ArchitectureExt: Architecture { } } + fn calling_convention_by_name(&self, name: &str) -> Option> { + let name = name.to_cstr(); + unsafe { + let result = NonNull::new(BNGetArchitectureCallingConventionByName( + self.as_ref().handle, + name.as_ptr(), + ))?; + Some(CoreCallingConvention::ref_from_raw( + result.as_ptr(), + self.as_ref().handle(), + )) + } + } + fn calling_conventions(&self) -> Array { unsafe { let mut count = 0;