| Issue |
181298
|
| Summary |
[Clang][Modules] Import order of ObjC modules causes weak-linked symbol to become strong-linked
|
| Labels |
clang
|
| Assignees |
|
| Reporter |
kevinlzh1108
|
## Summary
`Decl::isWeakImported()` in `clang/lib/AST/DeclBase.cpp` only inspects attributes on `getMostRecentDecl()`. When Clang Modules is enabled, an ObjC `@class` forward declaration from one module (e.g. UIKit) can become the most recent declaration, shadowing the original `@interface` declaration's availability attributes from another module (e.g. UniformTypeIdentifiers). This causes symbols that should be weak-linked to be incorrectly emitted as strong-linked.
## Impact
Apps targeting iOS 12.0 that use `UTType` (introduced in iOS 14.0) crash at launch on older devices because `dyld` cannot find `UniformTypeIdentifiers.framework`. The crash is not catchable. The only workaround is manually passing `-weak_framework UniformTypeIdentifiers` to the linker.
## Steps to Reproduce
Compile two files with the only difference being `#import` order:
**File A — UTI first (produces incorrect strong linkage):**
```objc
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <UIKit/UIKit.h>
void test(void) {
if (@available(iOS 14.0, *)) {
UTType *t = [UTType typeWithIdentifier:@"public.image"];
(void)t;
}
}
```
**File B — UIKit first (produces correct weak linkage):**
```objc
#import <UIKit/UIKit.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
void test(void) {
if (@available(iOS 14.0, *)) {
UTType *t = [UTType typeWithIdentifier:@"public.image"];
(void)t;
}
}
```
**Build & verify:**
```bash
SDK=$(xcrun --sdk iphoneos --show-sdk-path)
# File A
rm -rf /tmp/mc && mkdir /tmp/mc
xcrun clang -c test_a.m -o test_a.o \
-target arm64-apple-ios12.0 -isysroot "$SDK" \
-fmodules -fmodules-cache-path=/tmp/mc
nm -m test_a.o | grep "OBJC_CLASS.*UTType"
# → (undefined) external _OBJC_CLASS_$_UTType ← BUG: strong
# File B
rm -rf /tmp/mc && mkdir /tmp/mc
xcrun clang -c test_b.m -o test_b.o \
-target arm64-apple-ios12.0 -isysroot "$SDK" \
-fmodules -fmodules-cache-path=/tmp/mc
nm -m test_b.o | grep "OBJC_CLASS.*UTType"
# → (undefined) weak external _OBJC_CLASS_$_UTType ← correct: weak
```
**Disabling modules produces correct results for both orders:**
```bash
xcrun clang -c test_a.m -o test_a.o \
-target arm64-apple-ios12.0 -isysroot "$SDK" -fno-modules
nm -m test_a.o | grep "OBJC_CLASS.*UTType"
# → (undefined) weak external _OBJC_CLASS_$_UTType ← correct
```
## Environment
- Apple Clang 17.0.0 (clang-1700.3.19.1), Xcode 26.0
- iPhoneOS 26.0 SDK, deployment target iOS 12.0
- arm64
## Root Cause
UIKit headers contain bare `@class` forward declarations of `UTType`:
```objc
// UIDocumentPickerViewController.h:15
@class UIDocumentPickerViewController, UIDocumentMenuViewController, UTType;
```
These forward declarations carry **no iOS availability attribute** (only an inherited macOS attribute from the enclosing declaration context). The `@class` syntax does not support `API_AVAILABLE()`.
When modules are loaded in the order UTI → UIKit, the redeclaration chain becomes:
```
getMostRecentDecl()
│
▼
┌─ ObjCInterfaceDecl (from UIKit, @class UTType) ──────────┐
│ attrs: [AvailabilityAttr: macos 11.0 (Inherited)] │
│ ❌ No iOS AvailabilityAttr │
├───────────────────────────────────────────────────────────┤
│ ObjCInterfaceDecl (from UTType.h, @interface UTType) │
│ attrs: [AvailabilityAttr: ios 14.0] ← key attribute │
│ [AvailabilityAttr: macos 11.0] │
│ [AvailabilityAttr: tvos 14.0] ... │
└───────────────────────────────────────────────────────────┘
```
`isWeakImported()` ([DeclBase.cpp](https://github.com/llvm/llvm-project/blob/main/clang/lib/AST/DeclBase.cpp)) then executes:
```cpp
for (const auto *A : getMostRecentDecl()->attrs()) {
if (const auto *Availability = dyn_cast<AvailabilityAttr>(A)) {
if (CheckAvailability(...) == AR_NotYetIntroduced)
return true; // weak
}
}
return false; // strong (default)
```
It only sees `macos 11.0` on the UIKit forward declaration, which doesn't match the target platform `ios`. The loop ends without finding any iOS availability → returns `false` → **strong linkage**.
## Suggested Fix
`isWeakImported()` should traverse the entire redeclaration chain:
```cpp
bool Decl::isWeakImported() const {
bool IsDefinition;
if (!canBeWeakImported(IsDefinition))
return false;
for (const auto *D : redecls()) { // ← iterate ALL redeclarations
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;
}
}
}
return false;
}
```
Alternatively, ensure that when a `@class` forward declaration is deserialized from a module, it inherits all platform availability attributes from the existing `@interface` declaration.
## Additional Notes
- Only occurs with `-fmodules`; `-fno-modules` always produces correct weak linkage
- Not specific to `UTType` — any ObjC class satisfying these conditions is affected:
1. Class declared with `API_AVAILABLE(ios(X))` in framework A
2. Bare `@class` forward declaration (without availability) in framework B
3. Framework A imported before framework B
4. Deployment target < X
- Swift is not affected (no ObjC redeclaration chain mechanism)
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs