diff --git a/Crashlytics/Crashlytics/Components/FIRCLSContext.m b/Crashlytics/Crashlytics/Components/FIRCLSContext.m index 3242a08e884..c38a541ced1 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSContext.m +++ b/Crashlytics/Crashlytics/Components/FIRCLSContext.m @@ -33,6 +33,17 @@ #include "Crashlytics/Crashlytics/Helpers/FIRCLSFeatures.h" #include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h" +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#elif __has_include() +#import +#elif __has_include("FirebaseCrashlytics-Swift.h") +// If frameworks are not available, fall back to importing the header as it +// should be findable from a header search path pointing to the build +// directory. See #12611 for more context. +#import "FirebaseCrashlytics-Swift.h" +#endif + // The writable size is our handler stack plus whatever scratch we need. We have to use this space // extremely carefully, however, because thread stacks always needs to be page-aligned. Only the // first allocation is guaranteed to be page-aligned. @@ -182,10 +193,12 @@ #if CLS_MACH_EXCEPTION_SUPPORTED dispatch_group_async(group, queue, ^{ - _firclsContext.readonly->machException.path = - FIRCLSContextAppendToRoot(rootPath, FIRCLSReportMachExceptionFile); - - FIRCLSMachExceptionInit(&_firclsContext.readonly->machException); + FIRCLSEntitlementsReader* entitlementReader = [[FIRCLSEntitlementsReader alloc] init]; + NSNumber* enableEnhancedSecurity = + [[entitlementReader readEntitlements] platformRestrictions]; + if (!enableEnhancedSecurity) { + FIRCLSMachExceptionInit(&_firclsContext.readonly->machException); + } }); #endif diff --git a/Crashlytics/Crashlytics/Entitlements/Entitlements.swift b/Crashlytics/Crashlytics/Entitlements/Entitlements.swift new file mode 100644 index 00000000000..1a46f767157 --- /dev/null +++ b/Crashlytics/Crashlytics/Entitlements/Entitlements.swift @@ -0,0 +1,33 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +@objc(FIRCLSEntitlements) +public class Entitlements: NSObject { + @objc public var platformRestrictions: NSNumber? + + enum Key: String { + case platformRestrictions = "com.apple.security.hardened-process.platform-restrictions" + } + + init(rawValue: [String: Any]) { + for key in [Key.platformRestrictions] { + switch key { + case .platformRestrictions: + platformRestrictions = rawValue[key.rawValue] as? NSNumber + } + } + } +} diff --git a/Crashlytics/Crashlytics/Entitlements/EntitlementsReader.swift b/Crashlytics/Crashlytics/Entitlements/EntitlementsReader.swift new file mode 100644 index 00000000000..1bde7cc0634 --- /dev/null +++ b/Crashlytics/Crashlytics/Entitlements/EntitlementsReader.swift @@ -0,0 +1,137 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import MachO + +@objc(FIRCLSEntitlementsReader) +public class EntitlementsReader: NSObject { + private struct CSMultiBlob { + var magic: UInt32 + var lentgh: UInt32 + var count: UInt32 + } + + private struct CSBlob { + var type: UInt32 + var offset: UInt32 + } + + private enum CSMagic { + static let embeddedSignature: UInt32 = 0xFADE_0CC0 + static let embededEntitlements: UInt32 = 0xFADE_7171 + } + + @objc public func readEntitlements() -> Entitlements? { + var executableHeader: UnsafePointer? = nil + + for i in 0 ..< _dyld_image_count() { + let imageHeader = _dyld_get_image_header(i) + + if let filetype = imageHeader?.pointee.filetype, filetype == MH_EXECUTE { + executableHeader = imageHeader + break + } + } + + guard let executableHeader = executableHeader else { + print("Application image not found") + return nil + } + + let is64bit = executableHeader.pointee.magic == MH_MAGIC_64 || executableHeader.pointee + .magic == MH_CIGAM_64 + + var curLoadCommandIterator = Int(bitPattern: executableHeader) + + (is64bit ? MemoryLayout.size : MemoryLayout.size) + + for _ in 0 ..< executableHeader.pointee.ncmds { + guard let segmentCommand = UnsafePointer( + bitPattern: curLoadCommandIterator + )?.pointee else { + return nil + } + + if segmentCommand.cmd == LC_CODE_SIGNATURE { + break + } + curLoadCommandIterator = curLoadCommandIterator + Int(segmentCommand.cmdsize) + } + + guard let dataCommand = + UnsafePointer(bitPattern: curLoadCommandIterator)?.pointee else { + return nil + } + + let dataStart = Int(bitPattern: executableHeader) + Int(dataCommand.dataoff) + + var signatureCursor: Int = dataStart + + // This segement is not always has signature start + // When I test on debug build the this segment also contains symbol table + // need to iterate addresses and find the magic first + while signatureCursor < dataStart + Int(dataCommand.datasize) { + guard let multiBlob = UnsafePointer(bitPattern: signatureCursor)?.pointee else { + return nil + } + print(String(format: "Finding Magic Number: 0x%08X", CFSwapInt32(multiBlob.magic))) + if CFSwapInt32(multiBlob.magic) == CSMagic.embeddedSignature { + return readEntitlementsFromSignature(startingAt: signatureCursor, multiBlob: multiBlob) + } + signatureCursor = signatureCursor + 4 + } + + print("Cannot find the magic number") + return nil + } + + private func readEntitlementsFromSignature(startingAt offset: Int, + multiBlob: CSMultiBlob) -> Entitlements? { + let multiBlobSize = MemoryLayout.size + let blobSize = MemoryLayout.size + let itemCount = Int(CFSwapInt32(multiBlob.count)) + + for index in 0 ..< itemCount { + let blobOffset = offset + multiBlobSize + index * blobSize + let blob = UnsafePointer(bitPattern: blobOffset)?.pointee + // the first 4 bytes are the magic + // the next 4 are the length + // after that is the encoded plist + if + let blob = UnsafePointer(bitPattern: blobOffset)?.pointee, + let magic = UnsafePointer(bitPattern: offset + Int(CFSwapInt32(blob.offset)))? + .pointee, CFSwapInt32(magic) == CSMagic.embededEntitlements { + if let signatureLength = + UnsafePointer(bitPattern: offset + Int(CFSwapInt32(blob.offset)) + 4)?.pointee, + let entitlementDataPointer = + UnsafeRawPointer(bitPattern: offset + Int(CFSwapInt32(blob.offset)) + 8) { + let data = Data( + bytes: entitlementDataPointer, + count: Int(CFSwapInt32(signatureLength)) - 8 + ) + + guard let rawValues = try? PropertyListSerialization.propertyList( + from: data, + options: [], + format: nil + ) as? [String: Any] else { + return nil + } + return Entitlements(rawValue: rawValues) + } + } + } + return nil + } +} diff --git a/Package.swift b/Package.swift index b5e4b2b2881..fdf47eecbeb 100644 --- a/Package.swift +++ b/Package.swift @@ -581,6 +581,7 @@ let package = Package( "CrashlyticsInputFiles.xcfilelist", "third_party/libunwind/LICENSE", "Crashlytics/Rollouts/", + "Crashlytics/Entitlements/", ], sources: [ "Crashlytics/", @@ -616,6 +617,7 @@ let package = Package( path: "Crashlytics", sources: [ "Crashlytics/Rollouts/", + "Crashlytics/Entitlements/", ] ), .testTarget(