From a02791755bb77a25162ae51a74eb2b10793e35e7 Mon Sep 17 00:00:00 2001 From: Zach Anderson Date: Mon, 4 Dec 2023 07:21:37 -0800 Subject: [PATCH] Fix _availability_version_check for iOS 11 and 12 --- BUILD.gn | 1 + ci/licenses_golden/excluded_files | 1 + ci/licenses_golden/licenses_flutter | 4 + shell/platform/darwin/common/BUILD.gn | 19 +++ .../common/availability_version_check.cc | 159 +++++++++++++++++- .../common/availability_version_check.h | 18 ++ .../availability_version_check_unittests.cc | 35 ++++ shell/platform/darwin/ios/BUILD.gn | 1 + .../Source/availability_version_check_test.mm | 26 +++ testing/run_tests.py | 1 + 10 files changed, 260 insertions(+), 5 deletions(-) create mode 100644 shell/platform/darwin/common/availability_version_check.h create mode 100644 shell/platform/darwin/common/availability_version_check_unittests.cc create mode 100644 shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm diff --git a/BUILD.gn b/BUILD.gn index a3cd12f7aa565..10c84c6da6aa1 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -201,6 +201,7 @@ group("unittests") { public_deps += [ "//flutter/impeller/golden_tests:impeller_golden_tests", "//flutter/shell/gpu:gpu_surface_metal_unittests", + "//flutter/shell/platform/darwin/common:availability_version_check_unittests", "//flutter/shell/platform/darwin/common:framework_common_unittests", "//flutter/third_party/spring_animation:spring_animation_unittests", ] diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 1112ad7fa5a67..ba1ad98c67024 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -303,6 +303,7 @@ ../../../flutter/shell/platform/common/text_input_model_unittests.cc ../../../flutter/shell/platform/common/text_range_unittests.cc ../../../flutter/shell/platform/darwin/Doxyfile +../../../flutter/shell/platform/darwin/common/availability_version_check_unittests.cc ../../../flutter/shell/platform/darwin/common/framework/Source/flutter_codecs_unittest.mm ../../../flutter/shell/platform/darwin/common/framework/Source/flutter_standard_codec_unittest.mm ../../../flutter/shell/platform/darwin/macos/README.md diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 2393b61f71f5e..99c8122d0d994 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -6465,6 +6465,7 @@ ORIGIN: ../../../flutter/shell/platform/common/text_input_model.cc + ../../../fl ORIGIN: ../../../flutter/shell/platform/common/text_input_model.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/text_range.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/common/availability_version_check.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/buffer_conversions.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/command_line.h + ../../../flutter/LICENSE @@ -6597,6 +6598,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibilit ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection_test.mm + ../../../flutter/LICENSE @@ -9281,6 +9283,7 @@ FILE: ../../../flutter/shell/platform/common/text_input_model.cc FILE: ../../../flutter/shell/platform/common/text_input_model.h FILE: ../../../flutter/shell/platform/common/text_range.h FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc +FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.mm FILE: ../../../flutter/shell/platform/darwin/common/command_line.h @@ -9414,6 +9417,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection_test.mm diff --git a/shell/platform/darwin/common/BUILD.gn b/shell/platform/darwin/common/BUILD.gn index 359883ff4320f..26ea3bd263b25 100644 --- a/shell/platform/darwin/common/BUILD.gn +++ b/shell/platform/darwin/common/BUILD.gn @@ -50,6 +50,25 @@ source_set("availability_version_check") { public_configs = [ "//flutter:config" ] } +test_fixtures("availability_version_check_fixtures") { + fixtures = [] +} + +executable("availability_version_check_unittests") { + testonly = true + + sources = [ "availability_version_check_unittests.cc" ] + + deps = [ + ":availability_version_check", + ":availability_version_check_fixtures", + "//flutter/fml", + "//flutter/testing", + ] + + public_configs = [ "//flutter:config" ] +} + # Shared framework headers end up in the same folder as platform-specific # framework headers when consumed by clients, so the include paths assume they # are next to each other. diff --git a/shell/platform/darwin/common/availability_version_check.cc b/shell/platform/darwin/common/availability_version_check.cc index 67514cbf5561f..1564bec4335f0 100644 --- a/shell/platform/darwin/common/availability_version_check.cc +++ b/shell/platform/darwin/common/availability_version_check.cc @@ -2,20 +2,141 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "flutter/shell/platform/darwin/common/availability_version_check.h" + +#include +#include +#include + +#include #include #include -#include +#include "flutter/fml/build_config.h" +#include "flutter/fml/file.h" #include "flutter/fml/logging.h" +#include "flutter/fml/mapping.h" +#include "flutter/fml/platform/darwin/cf_utils.h" + +// The implementation of _availability_version_check defined in this file is +// based on the code in the clang-rt library at: +// +// https://github.com/llvm/llvm-project/blob/e315bf25a843582de39257e1345408a10dc08224/compiler-rt/lib/builtins/os_version_check.c +// +// Flutter provides its own implementation due to an issue introduced in recent +// versions of Clang following Clang 18 in which the clang-rt library declares +// weak linkage against the _availability_version_check symbol. This declaration +// causes apps to be rejected from the App Store. When Flutter statically links +// the implementation below, the weak linkage is satisfied at Engine build time, +// the symbol is no longer exposed from the Engine dylib, and apps will then +// not be rejected from the App Store. +// +// The implementation of _availability_version_check can delegate to the +// dynamically looked-up symbol on recent iOS versions, but the lookup will fail +// on iOS 11 and 12. When the lookup fails, the current OS version must be +// retrieved from a plist file at a well-known path. The logic for this below is +// copied from the clang-rt implementation and adapted for the Engine. -// See context in https://github.com/flutter/flutter/issues/132130 and +// See more context in https://github.com/flutter/flutter/issues/132130 and // https://github.com/flutter/engine/pull/44711. // TODO(zanderso): Remove this after Clang 18 rolls into Xcode. -// https://github.com/flutter/flutter/issues/133203 +// https://github.com/flutter/flutter/issues/133203. + +#define CF_PROPERTY_LIST_IMMUTABLE 0 + +namespace flutter { + +// This function parses the platform's version information out of a plist file +// at a well-known path. It parses the plist file using CoreFoundation functions +// to match the implementation in the clang-rt library. +std::optional ProductVersionFromSystemVersionPList() { + std::string plist_path = "/System/Library/CoreServices/SystemVersion.plist"; +#if FML_OS_IOS_SIMULATOR + char* plist_path_prefix = getenv("IPHONE_SIMULATOR_ROOT"); + if (!plist_path_prefix) { + FML_DLOG(ERROR) << "Failed to getenv IPHONE_SIMULATOR_ROOT"; + return std::nullopt; + } + plist_path = std::string(plist_path_prefix) + plist_path; +#endif // FML_OS_IOS_SIMULATOR + + auto plist_mapping = fml::FileMapping::CreateReadOnly(plist_path); + + // Get the file buffer into CF's format. We pass in a null allocator here * + // because we free PListBuf ourselves + auto file_contents = fml::CFRef(CFDataCreateWithBytesNoCopy( + nullptr, plist_mapping->GetMapping(), + static_cast(plist_mapping->GetSize()), kCFAllocatorNull)); + if (!file_contents) { + FML_DLOG(ERROR) << "Failed to CFDataCreateWithBytesNoCopyFunc"; + return std::nullopt; + } + + auto plist = fml::CFRef( + reinterpret_cast(CFPropertyListCreateWithData( + nullptr, file_contents, CF_PROPERTY_LIST_IMMUTABLE, nullptr, + nullptr))); + if (!plist) { + FML_DLOG(ERROR) << "Failed to CFPropertyListCreateWithDataFunc or " + "CFPropertyListCreateFromXMLDataFunc"; + return std::nullopt; + } + + auto product_version = + fml::CFRef(CFStringCreateWithCStringNoCopy( + nullptr, "ProductVersion", kCFStringEncodingASCII, kCFAllocatorNull)); + if (!product_version) { + FML_DLOG(ERROR) << "Failed to CFStringCreateWithCStringNoCopyFunc"; + return std::nullopt; + } + CFTypeRef opaque_value = CFDictionaryGetValue(plist, product_version); + if (!opaque_value || CFGetTypeID(opaque_value) != CFStringGetTypeID()) { + FML_DLOG(ERROR) << "Failed to CFDictionaryGetValueFunc"; + return std::nullopt; + } + + char version_str[32]; + if (!CFStringGetCString(reinterpret_cast(opaque_value), + version_str, sizeof(version_str), + kCFStringEncodingUTF8)) { + FML_DLOG(ERROR) << "Failed to CFStringGetCStringFunc"; + return std::nullopt; + } + + int32_t major = 0; + int32_t minor = 0; + int32_t subminor = 0; + int matches = sscanf(version_str, "%d.%d.%d", &major, &minor, &subminor); + // A major version number is sufficient. The minor and subminor numbers might + // not be present. + if (matches < 1) { + FML_DLOG(ERROR) << "Failed to match product version string: " + << version_str; + return std::nullopt; + } + + return ProductVersion{major, minor, subminor}; +} + +bool IsEncodedVersionLessThanOrSame(uint32_t encoded_lhs, ProductVersion rhs) { + // Parse the values out of encoded_lhs, then compare against rhs. + const int32_t major = (encoded_lhs >> 16) & 0xffff; + const int32_t minor = (encoded_lhs >> 8) & 0xff; + const int32_t subminor = encoded_lhs & 0xff; + auto lhs = ProductVersion{major, minor, subminor}; + + return lhs <= rhs; +} + +} // namespace flutter namespace { +// The host's OS version when the dynamic lookup of _availability_version_check +// has failed. +static flutter::ProductVersion g_version; + typedef uint32_t dyld_platform_t; typedef struct { @@ -36,13 +157,41 @@ void InitializeAvailabilityCheck(void* unused) { } AvailabilityVersionCheck = reinterpret_cast( dlsym(RTLD_DEFAULT, "_availability_version_check")); - FML_CHECK(AvailabilityVersionCheck); + if (AvailabilityVersionCheck) { + return; + } + + // If _availability_version_check can't be dynamically loaded, then version + // information must be parsed out of a system plist file. + auto product_version = flutter::ProductVersionFromSystemVersionPList(); + if (product_version.has_value()) { + g_version = product_version.value(); + } else { + // If reading version info out of the system plist file fails, then + // fall back to the minimum version that Flutter supports. +#if FML_OS_IOS || FML_OS_IOS_SIMULATOR + g_version = std::make_tuple(11, 0, 0); +#elif FML_OS_MACOSX + g_version = std::make_tuple(10, 14, 0); +#endif // FML_OS_MACOSX + } } extern "C" bool _availability_version_check(uint32_t count, dyld_build_version_t versions[]) { dispatch_once_f(&DispatchOnceCounter, NULL, InitializeAvailabilityCheck); - return AvailabilityVersionCheck(count, versions); + if (AvailabilityVersionCheck) { + return AvailabilityVersionCheck(count, versions); + } + + if (count == 0) { + return true; + } + + // This function is called in only one place in the clang-rt implementation + // where there is only one element in the array. + return flutter::IsEncodedVersionLessThanOrSame(versions[0].version, + g_version); } } // namespace diff --git a/shell/platform/darwin/common/availability_version_check.h b/shell/platform/darwin/common/availability_version_check.h new file mode 100644 index 0000000000000..8724a72d1990a --- /dev/null +++ b/shell/platform/darwin/common/availability_version_check.h @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +namespace flutter { + +using ProductVersion = + std::tuple; + +std::optional ProductVersionFromSystemVersionPList(); + +bool IsEncodedVersionLessThanOrSame(uint32_t encoded_lhs, ProductVersion rhs); + +} // namespace flutter diff --git a/shell/platform/darwin/common/availability_version_check_unittests.cc b/shell/platform/darwin/common/availability_version_check_unittests.cc new file mode 100644 index 0000000000000..7753d1cfa522d --- /dev/null +++ b/shell/platform/darwin/common/availability_version_check_unittests.cc @@ -0,0 +1,35 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "flutter/shell/platform/darwin/common/availability_version_check.h" + +#include "gtest/gtest.h" + +TEST(AvailabilityVersionCheck, CanDecodeSystemPlist) { + auto maybe_product_version = flutter::ProductVersionFromSystemVersionPList(); + ASSERT_TRUE(maybe_product_version.has_value()); + if (maybe_product_version.has_value()) { + auto product_version = maybe_product_version.value(); + ASSERT_GT(product_version, std::make_tuple(0, 0, 0)); + } +} + +static inline uint32_t ConstructVersion(uint32_t major, + uint32_t minor, + uint32_t subminor) { + return ((major & 0xffff) << 16) | ((minor & 0xff) << 8) | (subminor & 0xff); +} + +TEST(AvailabilityVersionCheck, CanParseAndCompareVersions) { + auto rhs_version = std::make_tuple(17, 2, 0); + uint32_t encoded_lower_version = ConstructVersion(12, 3, 7); + ASSERT_TRUE(flutter::IsEncodedVersionLessThanOrSame(encoded_lower_version, + rhs_version)); + + uint32_t encoded_higher_version = ConstructVersion(42, 0, 1); + ASSERT_FALSE(flutter::IsEncodedVersionLessThanOrSame(encoded_higher_version, + rhs_version)); +} diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index b8cebad60fa4b..ad924acfbe84f 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -273,6 +273,7 @@ shared_library("ios_test_flutter") { "framework/Source/UIViewController_FlutterScreenAndSceneIfLoadedTest.mm", "framework/Source/VsyncWaiterIosTest.mm", "framework/Source/accessibility_bridge_test.mm", + "framework/Source/availability_version_check_test.mm", "framework/Source/connection_collection_test.mm", "platform_message_handler_ios_test.mm", ] diff --git a/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm b/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm new file mode 100644 index 0000000000000..c843893c216e8 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import +#import + +#import "flutter/shell/platform/darwin/common/availability_version_check.h" + +@interface AvailabilityVersionCheckTest : XCTestCase +@end + +@implementation AvailabilityVersionCheckTest + +- (void)testSimple { + auto maybe_product_version = flutter::ProductVersionFromSystemVersionPList(); + XCTAssertTrue(maybe_product_version.has_value()); + if (maybe_product_version.has_value()) { + auto product_version = maybe_product_version.value(); + XCTAssertTrue(product_version > std::make_tuple(0, 0, 0)); + } +} + +@end diff --git a/testing/run_tests.py b/testing/run_tests.py index 62ebcd507592a..3e24850ba75bb 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -435,6 +435,7 @@ def make_test(name, flags=None, extra_env=None): unittests += [ # The accessibility library only supports Mac and Windows. make_test('accessibility_unittests'), + make_test('availability_version_check_unittests'), make_test('framework_common_unittests'), make_test('spring_animation_unittests'), make_test('gpu_surface_metal_unittests'),