Skip to content

[FunctionSpecialization] Strip ptrauth bundles from devirtualized calls - #17

Draft
oskarwirga wants to merge 1 commit into
Kotlin:kotlin/llvm-19-applefrom
oskarwirga:funcspec-strip-ptrauth-bundles
Draft

[FunctionSpecialization] Strip ptrauth bundles from devirtualized calls#17
oskarwirga wants to merge 1 commit into
Kotlin:kotlin/llvm-19-applefrom
oskarwirga:funcspec-strip-ptrauth-bundles

Conversation

@oskarwirga

Copy link
Copy Markdown

Downstream apply of a proposed upstream LLVM fix needed to unblock the arm64e Kotlin/Native target. Without it, FunctionSpecializationPass fails the IR verifier with:

Direct call cannot have a ptrauth bundle
invoke void @"sym"() [ "ptrauth"(i32 0, i64 0) ]
to label %52 unwind label %55

Root cause: when FunctionSpecialization clones a function with a constant function-pointer argument substituted into the body, an indirect call

invoke void %fn_ptr_param() [ "ptrauth"(i32 0, i64 0) ] ; LEGAL indirect

becomes a direct call after IPSCCP's V->replaceAllUsesWith(Const):

invoke void @"sym"() [ "ptrauth"(i32 0, i64 0) ] ; ILLEGAL direct

The verifier rejects: the ptrauth bundle is only meaningful on indirect calls (it tells the backend which key/discriminator to use to authenticate the loaded function pointer before branching). replaceAllUsesWith only rewrites the called operand; it does not touch operand bundles.

Concrete reproducer from K/N arm64e bring-up: CallInitGlobalPossiblyLock (a runtime helper) takes a fn-pointer arg and invokes it with a ptrauth bundle. FunctionSpecialization produces
@CallInitGlobalPossiblyLock.specialized.1 and rewrites the indirect invoke into a direct invoke to @"kfun:kotlin.text.$init_global#internal.2", with the bundle still attached. Compilation aborts at the verifier.

Fix: in FunctionSpecializer::createSpecialization, after cloning the function, walk the clone body and strip ptrauth operand bundles from any CallBase whose callee is one of the formal Arguments being specialized to a Function constant. This pre-empts the IPSCCP rewrite so the bundle is gone before the indirect-to-direct transition. Other operand bundles are preserved; non-PAC targets are unaffected (no calls carry ptrauth bundles).

Test: llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll. Triggers via -passes="ipsccp" -force-specialization with callers passing different Function constants (forces the FuncSpec clone path). Without the fix, opt aborts with the verifier error above. With the fix, specialized clones contain direct calls with no ptrauth bundle; a sibling negative case confirms bundles are preserved on still-indirect calls.

Verified end-to-end against upstream llvm/llvm-project main:

  • Test fails (verifier abort) without the fix.
  • Test passes with the fix.
  • llvm/test/Transforms/FunctionSpecialization/ all pass (60/62, 2 pre-existing UNSUPPORTED).
  • llvm/test/Transforms/IPO/ and SCCP/ all pass (175/176, 1 pre-existing UNSUPPORTED).
  • clang-format clean.

Filed upstream: TBD (llvm/llvm-project PR). This downstream apply lets Kotlin/Native delete its StripDirectCallPtrauthBundlesPass workaround (kotlin-native/backend.native/.../optimizations/StripDirectCallPtrauthBundlesPass.kt, libllvmext/src/main/cpp/StripDirectCallPtrauthBundles.cpp, and the Bitcode.kt phase wiring).

Note: a related variant of this bug exists on the IPSCCP in-place constant-propagation path (when all callers pass the same Function constant, FuncSpec is bypassed and IPSCCP rewrites the original function in-place). That path is not addressed here and would need a corresponding fix in SCCPSolver. The Kotlin/Native trigger is the FuncSpec clone path.

Downstream apply of a proposed upstream LLVM fix needed to unblock the
arm64e Kotlin/Native target. Without it, FunctionSpecializationPass
fails the IR verifier with:

  Direct call cannot have a ptrauth bundle
    invoke void @"sym"() [ "ptrauth"(i32 0, i64 0) ]
            to label %52 unwind label %55

Root cause: when FunctionSpecialization clones a function with a
constant function-pointer argument substituted into the body, an
indirect call

  invoke void %fn_ptr_param() [ "ptrauth"(i32 0, i64 0) ]  ; LEGAL indirect

becomes a direct call after IPSCCP's V->replaceAllUsesWith(Const):

  invoke void @"sym"()        [ "ptrauth"(i32 0, i64 0) ]  ; ILLEGAL direct

The verifier rejects: the ptrauth bundle is only meaningful on indirect
calls (it tells the backend which key/discriminator to use to
authenticate the loaded function pointer before branching).
replaceAllUsesWith only rewrites the called operand; it does not touch
operand bundles.

Concrete reproducer from K/N arm64e bring-up: CallInitGlobalPossiblyLock
(a runtime helper) takes a fn-pointer arg and invokes it with a ptrauth
bundle. FunctionSpecialization produces
@CallInitGlobalPossiblyLock.specialized.1 and rewrites the indirect
invoke into a direct invoke to @"kfun:kotlin.text.\$init_global#internal.2",
with the bundle still attached. Compilation aborts at the verifier.

Fix: in FunctionSpecializer::createSpecialization, after cloning the
function, walk the clone body and strip ptrauth operand bundles from
any CallBase whose callee is one of the formal Arguments being
specialized to a Function constant. This pre-empts the IPSCCP rewrite
so the bundle is gone before the indirect-to-direct transition. Other
operand bundles are preserved; non-PAC targets are unaffected (no calls
carry ptrauth bundles).

Test: llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll.
Triggers via -passes="ipsccp<func-spec>" -force-specialization with
callers passing different Function constants (forces the FuncSpec
clone path). Without the fix, opt aborts with the verifier error above.
With the fix, specialized clones contain direct calls with no ptrauth
bundle; a sibling negative case confirms bundles are preserved on
still-indirect calls.

Verified end-to-end against upstream llvm/llvm-project main:
- Test fails (verifier abort) without the fix.
- Test passes with the fix.
- llvm/test/Transforms/FunctionSpecialization/ all pass (60/62, 2
  pre-existing UNSUPPORTED).
- llvm/test/Transforms/IPO/ and SCCP/ all pass (175/176, 1
  pre-existing UNSUPPORTED).
- clang-format clean.

Filed upstream: TBD (llvm/llvm-project PR). This downstream apply lets
Kotlin/Native delete its StripDirectCallPtrauthBundlesPass workaround
(kotlin-native/backend.native/.../optimizations/StripDirectCallPtrauthBundlesPass.kt,
libllvmext/src/main/cpp/StripDirectCallPtrauthBundles.cpp, and the
Bitcode.kt phase wiring).

Note: a related variant of this bug exists on the IPSCCP in-place
constant-propagation path (when all callers pass the same Function
constant, FuncSpec is bypassed and IPSCCP rewrites the original
function in-place). That path is not addressed here and would need a
corresponding fix in SCCPSolver. The Kotlin/Native trigger is the
FuncSpec clone path.
@anton-bannykh

Copy link
Copy Markdown

Relevant Kotlin PR: JetBrains/kotlin#6306

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants