https://github.com/kevinlzh1108 updated https://github.com/llvm/llvm-project/pull/181482
>From 7f9f455552f801a11033371e8d0acbca07e52273 Mon Sep 17 00:00:00 2001 From: kevinlzh1108 <[email protected]> Date: Sat, 14 Feb 2026 22:41:32 +0800 Subject: [PATCH 1/2] [Clang] Fix isWeakImported() to traverse redeclaration chain for availability attributes Decl::isWeakImported() only checked attributes on getMostRecentDecl(), which missed availability attributes on earlier declarations in the redeclaration chain. This caused incorrect strong linking when a forward declaration (@class) without availability attributes became the most recent declaration, shadowing the original @interface declaration that carried the availability(ios,introduced=14.0) attribute. This is particularly problematic with Clang Modules and Precompiled Headers (PCH), where module deserialization order can cause a forward declaration from one module (e.g., UIKit's @class UTType) to become the most recent declaration, even though the original @interface declaration (in UniformTypeIdentifiers) has the availability attribute. The fix changes isWeakImported() to iterate over all redeclarations using redecls(), consistent with how Decl::isReferenced() already traverses the redeclaration chain in the same file. Fixes a bug where apps targeting iOS < 14.0 would strongly link UniformTypeIdentifiers symbols instead of weak-linking them, causing crashes on older iOS versions. --- clang/lib/AST/DeclBase.cpp | 18 ++++--- .../attr-availability-redecl-weak.m | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 clang/test/CodeGenObjC/attr-availability-redecl-weak.m diff --git a/clang/lib/AST/DeclBase.cpp b/clang/lib/AST/DeclBase.cpp index 0a1e442656c35..0c122f808dea4 100644 --- a/clang/lib/AST/DeclBase.cpp +++ b/clang/lib/AST/DeclBase.cpp @@ -872,14 +872,18 @@ bool Decl::isWeakImported() const { if (!canBeWeakImported(IsDefinition)) return false; - for (const auto *A : getMostRecentDecl()->attrs()) { - if (isa<WeakImportAttr>(A)) - return true; - - if (const auto *Availability = dyn_cast<AvailabilityAttr>(A)) { - if (CheckAvailability(getASTContext(), Availability, nullptr, - VersionTuple()) == AR_NotYetIntroduced) + // Traverse the entire redeclaration chain, since availability attributes + // may not be present on the most recent declaration (e.g., a @class forward + // declaration may lack the availability attribute from the @interface). + for (const auto *D : redecls()) { + for (const auto *A : D->attrs()) { + if (isa<WeakImportAttr>(A)) return true; + + if (const auto *Availability = dyn_cast<AvailabilityAttr>(A)) + if (CheckAvailability(getASTContext(), Availability, nullptr, + VersionTuple()) == AR_NotYetIntroduced) + return true; } } diff --git a/clang/test/CodeGenObjC/attr-availability-redecl-weak.m b/clang/test/CodeGenObjC/attr-availability-redecl-weak.m new file mode 100644 index 0000000000000..e1000e66a6042 --- /dev/null +++ b/clang/test/CodeGenObjC/attr-availability-redecl-weak.m @@ -0,0 +1,47 @@ +// RUN: %clang_cc1 -triple arm64-apple-ios12.0 -emit-llvm -o - %s | FileCheck %s + +// Test that isWeakImported() correctly traverses the redeclaration chain +// to find availability attributes, even when a forward declaration (@class) +// without availability attributes becomes the most recent declaration. + +// Case 1: @interface (with availability) first, then @class (without availability). +// The @class becomes getMostRecentDecl(). Without the fix, isWeakImported() +// would only check the @class's attributes and miss the availability attribute, +// resulting in strong linkage instead of extern_weak. + +__attribute__((availability(ios,introduced=14.0))) +@interface WeakRedecl1 +@end + +@class WeakRedecl1; + +@implementation WeakRedecl1 (TestCategory1) +@end + +// CHECK: @"OBJC_CLASS_$_WeakRedecl1" = extern_weak global + +// Case 2: @class first, then @interface (with availability). +// This order already worked before the fix because @interface becomes +// getMostRecentDecl() and carries the availability attribute. +// We test it here to ensure the fix doesn't regress this case. + +@class WeakRedecl2; + +__attribute__((availability(ios,introduced=14.0))) +@interface WeakRedecl2 +@end + +@implementation WeakRedecl2 (TestCategory2) +@end + +// CHECK: @"OBJC_CLASS_$_WeakRedecl2" = extern_weak global + +// Case 3: Single declaration with availability (baseline, no redeclaration). +__attribute__((availability(ios,introduced=14.0))) +@interface WeakSingle +@end + +@implementation WeakSingle (TestCategory3) +@end + +// CHECK: @"OBJC_CLASS_$_WeakSingle" = extern_weak global >From 374420f77132fa1809c34eeb768827115c1ba1c7 Mon Sep 17 00:00:00 2001 From: kevinlzh1108 <[email protected]> Date: Mon, 13 Apr 2026 20:08:31 +0800 Subject: [PATCH 2/2] [Clang] Replace single-file test with Clang Modules test for isWeakImported() fix The previous single-file test (attr-availability-redecl-weak.m) was not a valid reduction: within a single translation unit, Sema's mergeDeclAttributes() automatically merges all availability attributes from @interface to @class, so the old getMostRecentDecl()->attrs() code also found the ios availability attribute. The test passed with or without the fix. Replace it with a Clang Modules test that reproduces the actual bug: - InterfaceMod: @interface with availability(macos,introduced=11.0) and availability(ios,introduced=14.0) (macos listed first) - ForwardMod: bare @class (no availability attributes) When InterfaceMod is imported first and ForwardMod second, the @class becomes getMostRecentDecl(). Cross-PCM mergeInheritableAttributes (ASTReaderDecl.cpp) uses getAttr<AvailabilityAttr>() which only copies the first AvailabilityAttr (macos), losing the ios attr. The old isWeakImported() only checked getMostRecentDecl()->attrs(), found only macos (wrong platform), and incorrectly returned false (strong linkage). Both import orders are tested to ensure no regression. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --- .../attr-availability-redecl-weak.m | 47 ------------------- .../Inputs/availability-redecl-weak/forward.h | 1 + .../availability-redecl-weak/interface.h | 8 ++++ .../availability-redecl-weak/module.modulemap | 2 + clang/test/Modules/availability-redecl-weak.m | 39 +++++++++++++++ 5 files changed, 50 insertions(+), 47 deletions(-) delete mode 100644 clang/test/CodeGenObjC/attr-availability-redecl-weak.m create mode 100644 clang/test/Modules/Inputs/availability-redecl-weak/forward.h create mode 100644 clang/test/Modules/Inputs/availability-redecl-weak/interface.h create mode 100644 clang/test/Modules/Inputs/availability-redecl-weak/module.modulemap create mode 100644 clang/test/Modules/availability-redecl-weak.m diff --git a/clang/test/CodeGenObjC/attr-availability-redecl-weak.m b/clang/test/CodeGenObjC/attr-availability-redecl-weak.m deleted file mode 100644 index e1000e66a6042..0000000000000 --- a/clang/test/CodeGenObjC/attr-availability-redecl-weak.m +++ /dev/null @@ -1,47 +0,0 @@ -// RUN: %clang_cc1 -triple arm64-apple-ios12.0 -emit-llvm -o - %s | FileCheck %s - -// Test that isWeakImported() correctly traverses the redeclaration chain -// to find availability attributes, even when a forward declaration (@class) -// without availability attributes becomes the most recent declaration. - -// Case 1: @interface (with availability) first, then @class (without availability). -// The @class becomes getMostRecentDecl(). Without the fix, isWeakImported() -// would only check the @class's attributes and miss the availability attribute, -// resulting in strong linkage instead of extern_weak. - -__attribute__((availability(ios,introduced=14.0))) -@interface WeakRedecl1 -@end - -@class WeakRedecl1; - -@implementation WeakRedecl1 (TestCategory1) -@end - -// CHECK: @"OBJC_CLASS_$_WeakRedecl1" = extern_weak global - -// Case 2: @class first, then @interface (with availability). -// This order already worked before the fix because @interface becomes -// getMostRecentDecl() and carries the availability attribute. -// We test it here to ensure the fix doesn't regress this case. - -@class WeakRedecl2; - -__attribute__((availability(ios,introduced=14.0))) -@interface WeakRedecl2 -@end - -@implementation WeakRedecl2 (TestCategory2) -@end - -// CHECK: @"OBJC_CLASS_$_WeakRedecl2" = extern_weak global - -// Case 3: Single declaration with availability (baseline, no redeclaration). -__attribute__((availability(ios,introduced=14.0))) -@interface WeakSingle -@end - -@implementation WeakSingle (TestCategory3) -@end - -// CHECK: @"OBJC_CLASS_$_WeakSingle" = extern_weak global diff --git a/clang/test/Modules/Inputs/availability-redecl-weak/forward.h b/clang/test/Modules/Inputs/availability-redecl-weak/forward.h new file mode 100644 index 0000000000000..c9123d49b7d99 --- /dev/null +++ b/clang/test/Modules/Inputs/availability-redecl-weak/forward.h @@ -0,0 +1 @@ +@class WeakRedecl1; diff --git a/clang/test/Modules/Inputs/availability-redecl-weak/interface.h b/clang/test/Modules/Inputs/availability-redecl-weak/interface.h new file mode 100644 index 0000000000000..9de6f4aaa30e2 --- /dev/null +++ b/clang/test/Modules/Inputs/availability-redecl-weak/interface.h @@ -0,0 +1,8 @@ +// Mimics a class like UTType that has availability attrs for multiple platforms. +// The 'macos' attr comes before 'ios', so getAttr<AvailabilityAttr>() returns +// 'macos' first. When mergeInheritableAttributes copies only the first attr +// across PCM boundaries, the 'ios' attr is lost on the @class redeclaration. +__attribute__((availability(macos,introduced=11.0))) +__attribute__((availability(ios,introduced=14.0))) +@interface WeakRedecl1 +@end diff --git a/clang/test/Modules/Inputs/availability-redecl-weak/module.modulemap b/clang/test/Modules/Inputs/availability-redecl-weak/module.modulemap new file mode 100644 index 0000000000000..0123708ab26cd --- /dev/null +++ b/clang/test/Modules/Inputs/availability-redecl-weak/module.modulemap @@ -0,0 +1,2 @@ +module InterfaceMod { header "interface.h" export * } +module ForwardMod { header "forward.h" export * } diff --git a/clang/test/Modules/availability-redecl-weak.m b/clang/test/Modules/availability-redecl-weak.m new file mode 100644 index 0000000000000..1f093e2a8b3e6 --- /dev/null +++ b/clang/test/Modules/availability-redecl-weak.m @@ -0,0 +1,39 @@ +// RUN: rm -rf %t +// RUN: %clang_cc1 -triple arm64-apple-ios12.0 -fmodules-cache-path=%t \ +// RUN: -fmodules -fimplicit-module-maps \ +// RUN: -I %S/Inputs/availability-redecl-weak \ +// RUN: -DINTERFACE_FIRST -emit-llvm -o - %s | FileCheck %s +// RUN: rm -rf %t +// RUN: %clang_cc1 -triple arm64-apple-ios12.0 -fmodules-cache-path=%t \ +// RUN: -fmodules -fimplicit-module-maps \ +// RUN: -I %S/Inputs/availability-redecl-weak \ +// RUN: -emit-llvm -o - %s | FileCheck %s + +// Test that isWeakImported() traverses the redeclaration chain across module +// boundaries to find availability attributes. +// +// InterfaceMod has @interface WeakRedecl1 with availability(macos,introduced=11.0) +// and availability(ios,introduced=14.0). ForwardMod has a bare @class WeakRedecl1. +// +// When InterfaceMod is imported first and ForwardMod second, the @class becomes +// getMostRecentDecl(). Cross-PCM mergeInheritableAttributes only copies the first +// AvailabilityAttr (macos), losing the ios attr. The old isWeakImported() only +// checked getMostRecentDecl()->attrs(), found only macos (not the ios target +// platform), and incorrectly returned false (strong linkage). + +#ifdef INTERFACE_FIRST +// This order triggers the bug: @interface loaded first, then @class becomes +// the most recent decl with only an inherited macos availability attr. +@import InterfaceMod; +@import ForwardMod; +#else +// This order works even without the fix: @class loaded first, then @interface +// becomes the most recent decl with all availability attrs intact. +@import ForwardMod; +@import InterfaceMod; +#endif + +@implementation WeakRedecl1 (TestCategory1) +@end + +// CHECK: @"OBJC_CLASS_$_WeakRedecl1" = extern_weak global _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
