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

Reply via email to