From bd7784b83861eb6d613a057b4dd4c184893700bf Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 30 Oct 2026 09:04:06 -0700 Subject: [PATCH] fix(crashlytics): read SDK versions from LC_BUILD_VERSION (#16830) --- Crashlytics/CHANGELOG.md | 2 + .../Components/FIRCLSBinaryImage.m | 1 + .../Models/FIRCLSApplicationIdentifierModel.m | 3 +- Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m | 8 +++ .../Shared/FIRCLSMachO/FIRCLSMachOSlice.m | 1 + .../FIRCLSApplicationIdentifierModelTests.m | 35 +++++++++++ .../UnitTests/FIRCLSBinaryImageTests.m | 61 +++++++++++++++++++ .../UnitTests/FIRCLSMachO/FIRCLSMachOTests.m | 30 +++++++++ 8 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 Crashlytics/UnitTests/FIRCLSApplicationIdentifierModelTests.m create mode 100644 Crashlytics/UnitTests/FIRCLSBinaryImageTests.m diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index f76c0c2bbb..01086dc158 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -6,6 +6,8 @@ - [fixed] Improved robustness in Firebase Sessions when parsing malformed settings from the server, including `null` values that could cause a crash when caching settings. (#16728) +- [fixed] Fixed the app executable's minimum OS and SDK versions being recorded as `0.0.0` in + report metadata for apps targeting iOS 12, macOS 10.14, tvOS 12, watchOS 5, or later. (#16810) # 13.0.0 - [fixed] Safely validate memory reads when executing `DW_OP_deref_size` diff --git a/Crashlytics/Crashlytics/Components/FIRCLSBinaryImage.m b/Crashlytics/Crashlytics/Components/FIRCLSBinaryImage.m index d19beb84f4..4a72bd01a6 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSBinaryImage.m +++ b/Crashlytics/Crashlytics/Components/FIRCLSBinaryImage.m @@ -311,6 +311,7 @@ static void FIRCLSPopulateImageDetailWithLoadCommand(uint32_t type, case LC_VERSION_MIN_IPHONEOS: case LC_VERSION_MIN_TVOS: case LC_VERSION_MIN_WATCHOS: + case LC_BUILD_VERSION: details->minSDK = FIRCLSMachOGetMinimumOSVersion(cmd); details->builtSDK = FIRCLSMachOGetLinkedSDKVersion(cmd); break; diff --git a/Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.m b/Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.m index 975cf2b2d6..53f03c8daf 100644 --- a/Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.m +++ b/Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.m @@ -104,7 +104,8 @@ uuid = FIRCLSMachOGetUUID(cmd); [executables setObject:FIRCLSUUIDToNSString(uuid) forKey:arch]; - } else if (type == LC_VERSION_MIN_MACOSX || type == LC_VERSION_MIN_IPHONEOS) { + } else if (type == LC_VERSION_MIN_MACOSX || type == LC_VERSION_MIN_IPHONEOS || + type == LC_BUILD_VERSION) { self->_minimumSDK = FIRCLSMachOGetMinimumOSVersion(cmd); self->_builtSDK = FIRCLSMachOGetLinkedSDKVersion(cmd); } diff --git a/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m b/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m index c6cdde3247..318ec7dc4f 100644 --- a/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m +++ b/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m @@ -514,10 +514,18 @@ static FIRCLSMachOVersion FIRCLSMachOVersionFromEncoded(uint32_t encoded) { } FIRCLSMachOVersion FIRCLSMachOGetMinimumOSVersion(const struct load_command* cmd) { + if (cmd->cmd == LC_BUILD_VERSION) { + return FIRCLSMachOVersionFromEncoded(((const struct build_version_command*)cmd)->minos); + } + return FIRCLSMachOVersionFromEncoded(((const struct version_min_command*)cmd)->version); } FIRCLSMachOVersion FIRCLSMachOGetLinkedSDKVersion(const struct load_command* cmd) { + if (cmd->cmd == LC_BUILD_VERSION) { + return FIRCLSMachOVersionFromEncoded(((const struct build_version_command*)cmd)->sdk); + } + return FIRCLSMachOVersionFromEncoded(((const struct version_min_command*)cmd)->sdk); } diff --git a/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachOSlice.m b/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachOSlice.m index d28626b8ce..ee7198734c 100644 --- a/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachOSlice.m +++ b/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachOSlice.m @@ -66,6 +66,7 @@ case LC_VERSION_MIN_MACOSX: case LC_VERSION_MIN_WATCHOS: case LC_VERSION_MIN_TVOS: + case LC_BUILD_VERSION: self->_minimumOSVersion = FIRCLSMachOGetMinimumOSVersion(cmd); self->_linkedSDKVersion = FIRCLSMachOGetLinkedSDKVersion(cmd); break; diff --git a/Crashlytics/UnitTests/FIRCLSApplicationIdentifierModelTests.m b/Crashlytics/UnitTests/FIRCLSApplicationIdentifierModelTests.m new file mode 100644 index 0000000000..4b4b764422 --- /dev/null +++ b/Crashlytics/UnitTests/FIRCLSApplicationIdentifierModelTests.m @@ -0,0 +1,35 @@ +// Copyright 2026 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 "Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.h" + +#import + +@interface FIRCLSApplicationIdentifierModelTests : XCTestCase + +@end + +@implementation FIRCLSApplicationIdentifierModelTests + +- (void)testReadsMinimumAndBuiltSDKVersionsOfExecutable { + // The test host is built with a current toolchain and deployment target, so its versions are + // described by LC_BUILD_VERSION rather than LC_VERSION_MIN_*. + FIRCLSApplicationIdentifierModel *model = [[FIRCLSApplicationIdentifierModel alloc] init]; + XCTAssertNotNil(model); + + XCTAssertNotEqualObjects(model.minimumSDKString, @"0.0.0"); + XCTAssertNotEqualObjects(model.builtSDKString, @"0.0.0"); +} + +@end diff --git a/Crashlytics/UnitTests/FIRCLSBinaryImageTests.m b/Crashlytics/UnitTests/FIRCLSBinaryImageTests.m new file mode 100644 index 0000000000..51ccc3162e --- /dev/null +++ b/Crashlytics/UnitTests/FIRCLSBinaryImageTests.m @@ -0,0 +1,61 @@ +// Copyright 2026 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. + +#include "Crashlytics/Crashlytics/Components/FIRCLSBinaryImage.h" + +#import +#import + +#include "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h" + +@interface FIRCLSBinaryImageTests : XCTestCase + +@end + +@implementation FIRCLSBinaryImageTests + +- (void)testRecordMainExecutableIncludesMinimumAndBuiltSDKVersions { + // The test host is built with a current toolchain and deployment target, so its versions are + // described by LC_BUILD_VERSION rather than LC_VERSION_MIN_*. + NSString *path = + [NSTemporaryDirectory() stringByAppendingPathComponent:@"executable_test.clsrecord"]; + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + + FIRCLSFile file; + if (!FIRCLSFileInitWithPath(&file, [path fileSystemRepresentation], false)) { + XCTFail(@"Failed to open %@", path); + return; + } + XCTAssertTrue(FIRCLSBinaryImageRecordMainExecutable(&file)); + FIRCLSFileClose(&file); + + NSData *data = [NSData dataWithContentsOfFile:path]; + XCTAssertNotNil(data); + if (!data) { + return; + } + + NSDictionary *record = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSDictionary *executable = record[@"executable"]; + XCTAssertNotNil(executable); + + XCTAssertNotNil(executable[@"minimum_sdk_version"]); + XCTAssertNotNil(executable[@"built_sdk_version"]); + XCTAssertNotEqualObjects(executable[@"minimum_sdk_version"], @"0.0.0"); + XCTAssertNotEqualObjects(executable[@"built_sdk_version"], @"0.0.0"); + + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; +} + +@end diff --git a/Crashlytics/UnitTests/FIRCLSMachO/FIRCLSMachOTests.m b/Crashlytics/UnitTests/FIRCLSMachO/FIRCLSMachOTests.m index 723a08d1cf..39f4a753e9 100644 --- a/Crashlytics/UnitTests/FIRCLSMachO/FIRCLSMachOTests.m +++ b/Crashlytics/UnitTests/FIRCLSMachO/FIRCLSMachOTests.m @@ -277,6 +277,36 @@ XCTAssertEqual((uint32_t)0, version.bugfix, @""); } +- (void)testReadVersionsFromBuildVersionLoadCommand { + // Binaries built for iOS 12, macOS 10.14, tvOS 12, watchOS 5 or later carry LC_BUILD_VERSION + // instead of LC_VERSION_MIN_*. + struct build_version_command command = {0}; + command.cmd = LC_BUILD_VERSION; + command.cmdsize = sizeof(command); + command.platform = PLATFORM_IOS; + command.minos = 0x000F0200; // 15.2.0 + command.sdk = 0x001A0501; // 26.5.1 + + FIRCLSMachOVersion version = FIRCLSMachOGetMinimumOSVersion((const struct load_command*)&command); + XCTAssertEqual((uint32_t)15, version.major); + XCTAssertEqual((uint32_t)2, version.minor); + XCTAssertEqual((uint32_t)0, version.bugfix); + + version = FIRCLSMachOGetLinkedSDKVersion((const struct load_command*)&command); + XCTAssertEqual((uint32_t)26, version.major); + XCTAssertEqual((uint32_t)5, version.minor); + XCTAssertEqual((uint32_t)1, version.bugfix); +} + +- (void)testRunningSliceReadsMinimumOSAndSDKVersions { + // The test host is built with a current toolchain and deployment target, so its versions are + // described by LC_BUILD_VERSION. + FIRCLSMachOSlice* slice = [FIRCLSMachOSlice runningSlice]; + + XCTAssertGreaterThan(slice.minimumOSVersion.major, (uint32_t)0); + XCTAssertGreaterThan(slice.linkedSDKVersion.major, (uint32_t)0); +} + - (void)testReadx86_64Section { NSString* path = [[self resourcePath] stringByAppendingPathComponent:@"x86_64-executable"]; struct FIRCLSMachOFile file;