diff options
| author | Mark Rowe <mark@vector35.com> | 2026-02-18 22:03:29 -0800 |
|---|---|---|
| committer | Mark Rowe <mark@vector35.com> | 2026-03-23 16:08:45 -0700 |
| commit | 1fb9828d1e12f242fa6a53aa43603ae5e556b69d (patch) | |
| tree | 40fd4fa7ca487ca8cce5a8eca8e18d5e4a15da83 /plugins/workflow_swift | |
| parent | 7aa5bd46fc2343458dff6a24ec2c67169fcac34c (diff) | |
[Swift] Add support for applying parameter and return types during demangling
This is disabled by default due to a current limitation where core is
not able to represent parameters that are small structs being passed
across multiple registers. `analysis.swift.extractTypesFromMangledNames`
can be enabled to test this.
Diffstat (limited to 'plugins/workflow_swift')
| -rw-r--r-- | plugins/workflow_swift/src/demangler/function_type.rs | 271 | ||||
| -rw-r--r-- | plugins/workflow_swift/src/demangler/mod.rs | 36 | ||||
| -rw-r--r-- | plugins/workflow_swift/src/demangler/name.rs | 84 | ||||
| -rw-r--r-- | plugins/workflow_swift/src/demangler/type_reconstruction.rs | 204 | ||||
| -rw-r--r-- | plugins/workflow_swift/src/lib.rs | 14 |
5 files changed, 602 insertions, 7 deletions
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 00000000..3313143e --- /dev/null +++ b/plugins/workflow_swift/src/demangler/function_type.rs @@ -0,0 +1,271 @@ +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}; + +/// 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, + flags: u8, + leading_params: Vec<FunctionParameter>, + trailing_params: Vec<FunctionParameter>, +} + +impl CallingConvention { + const SELF: u8 = 1; + const THROWS: u8 = 2; + const ASYNC: u8 = 4; + + fn for_arch(arch: &CoreArchitecture) -> Self { + Self { + 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<FunctionParameter>) -> Ref<Type> { + 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: None, + }); + } + + 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: None, + }); + } + + Type::function(ret_type, all_params, false) + } +} + +/// 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<Ref<Type>> { + 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<FunctionParameter> = 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<Ref<Type>> { + 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<Ref<Type>> { + 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 index e1f22893..657c62c3 100644 --- a/plugins/workflow_swift/src/demangler/mod.rs +++ b/plugins/workflow_swift/src/demangler/mod.rs @@ -1,9 +1,22 @@ +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 { @@ -19,16 +32,25 @@ impl CustomDemangler for SwiftDemangler { fn demangle( &self, - _arch: &CoreArchitecture, + arch: &CoreArchitecture, name: &str, - _view: Option<Ref<BinaryView>>, + view: Option<Ref<BinaryView>>, ) -> Option<(QualifiedName, Option<Ref<Type>>)> { let ctx = swift_demangler::Context::new(); let symbol = swift_demangler::Symbol::parse(&ctx, name)?; - // Use the canonical demangled form from the parsed node tree. - // This matches what `xcrun swift-demangle` produces. - // TODO: Use the structured Symbol API to also reconstruct BN Types. - let demangled = symbol.display(); - Some((QualifiedName::from(demangled), None)) + + 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 00000000..fd39d8b7 --- /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<String>, 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<QualifiedName> { + let mut parts: Vec<String> = 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<String> = 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 00000000..3a3ba3e7 --- /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<Ref<Type>>; +} + +impl TypeRefExt for TypeRef<'_> { + fn to_bn_type(&self, arch: &CoreArchitecture) -> Option<Ref<Type>> { + 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<T>) 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<String> = + 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<Ref<Type>> { + 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<Type> { + 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 index 2e97a43b..a04f3659 100644 --- a/plugins/workflow_swift/src/lib.rs +++ b/plugins/workflow_swift/src/lib.rs @@ -2,8 +2,11 @@ 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() { @@ -16,6 +19,17 @@ pub extern "C" fn CorePluginDependencies() { 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 |
