Skip to content

Background Assets: fix EXC_BAD_ACCESS in AssetPackManifest on iOS 26 (non-blittable baw_err) - #114

Merged
ryzngard merged 5 commits into
apple:mainfrom
petarkarov:fix/baw-err-non-blittable-bool
Sep 23, 2026
Merged

ryzngard merged 5 commits into
apple:mainfrom
petarkarov:fix/baw-err-non-blittable-bool

Conversation

@petarkarov

Copy link
Copy Markdown
Contributor

Summary

Apple.BackgroundAssets.Error.baw_err declares its _static field as a C# bool. A C# bool is not blittable, so IL2CPP generates a marshaled copy for every struct that embeds baw_err, including the union baw_assetpackmanifest_res. The generated unmarshal code copies the union's success member and then its failure member into the same bytes, so the second copy overwrites the first. On iOS 26 this destroys the manifest's asset-pack array pointer, and the next call into the native wrapper crashes.

Changing the field to byte makes baw_err blittable. IL2CPP then passes the union through untouched and the crash disappears. The field is never read on the C# side, so there is no behavior change beyond the fix.

Symptom

On any iOS 26 device (tested on 26.5), the first call to AssetPackManifest.GetAssetPack() or GetAllAssetPacks() after AssetPackManager.GetManifestAsync() crashes:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000001
0  BackgroundAssetsWrapper  baw_assetpackmanifest_assetpack + 412
1  UnityFramework           AssetPackManifest_GetAssetPack_...

The faulting instruction is ldr x1, [x21] where x21 is cManifest.compat.assetpackv, and its value is 1.

iOS 27 is not affected: there the manifest is stored as a single opaque pointer in the first 8 bytes of the union, which the overwrite happens to leave intact.

Root cause

IL2CPP output for the reverse P/Invoke wrapper of ManifestCallback (Unity 6000.4.0f1):

void baw_assetpackmanifest_res_..._marshal_pinvoke_back(const ..._marshaled_pinvoke& marshaled, ...& unmarshaled)
{
    unmarshaled.___success = marshaled.___success;                       // 16 bytes: {assetpackc, assetpackv}
    baw_err_..._marshal_pinvoke_back(marshaled.___failure, temp);        // temp = {description, _static = 1}, rest 0
    unmarshaled.___failure = temp;                                       // overwrites the same 16 bytes
}

success and failure overlap at offset 0. After the second assignment, bytes 8..15 contain 0x0000000000000001 (the bool converted from the marshaled int32), which is where baw_assetpackmanifest_compat.assetpackv lives on iOS 26.

A secondary mismatch is fixed by the same change: C bool is 1 byte, but C# bool is marshaled as a 4-byte BOOL by default, so _static was also being read from padding.

Fix

 [StructLayout(LayoutKind.Sequential)]
 internal struct baw_err {
     internal IntPtr description;
-    bool _static;
+    byte _static;
 }

With this change the generated wrapper takes baw_assetpackmanifest_res_t... directly instead of a _marshaled_pinvoke variant, and no copy is performed.

Verification

  • Environment: Unity 6000.4.0f1, Xcode 26.3.
  • Before: crash reproduced on iPhone 12, iOS 26.5, in 5 consecutive TestFlight builds across two Unity projects, always at the same address.
  • After: same device, same flow, manifest and asset pack resolve correctly and the on-demand pack downloads. Also verified no regression on an iPhone 14 Pro, iOS 27.0.
  • Confirmed in the exported Xcode project that ReversePInvokeWrapper_AssetPackManager_ManifestCallback now takes the raw struct.

Notes

The same pattern applies to every other union that embeds baw_err (baw_assetpack_status_res, baw_assetpackmanager_assetpack_update_res, and the plain baw_err callbacks). They didn't crash only because their overlapping bytes happened to carry the same values, so this change makes them correct as well.

…(non-blittable baw_err)

`Apple.BackgroundAssets.Error.baw_err` declared its `_static` field as a
C# `bool`. A C# `bool` is not blittable, so IL2CPP generated a marshaled
copy for every struct that embeds `baw_err`, including the union
`baw_assetpackmanifest_res`. The generated unmarshal code copied the
union's `success` member and then its `failure` member into the same
bytes, so the second copy overwrote the first. On iOS 26 this destroyed
the manifest's asset-pack array pointer, and the next call into the
native wrapper crashed.

Changing the field to `byte` makes `baw_err` blittable, so IL2CPP
passes the union through untouched and the crash disappears. The field
is never read on the C# side, so there is no behavior change beyond
the fix.
ryzngard added a commit to ryzngard/unityplugins that referenced this pull request Sep 17, 2026
…ssing (apple#114)

Same fix as before, reordered to check `if (fallbackLibrary.IsValid)` (warning) / `else` (error) for readability.
The baw_err fix is invisible in source: nothing about `byte` versus `bool`
says "this keeps a union from being corrupted", so the next person to tidy
it up reintroduces the crash. Add tests that read the invariant back off the
compiled assembly, plus a comment on the field itself.

Two guards, both reflection over Apple.BackgroundAssets:

- Every struct reachable from a [DllImport] signature or a reverse-P/Invoke
  delegate must be blittable, walking the whole field graph.
- Every LayoutKind.Explicit struct must be blittable. A union cannot be
  marshalled correctly at all: the marshaller writes each member in turn to
  the same bytes and the last one wins, which is what destroyed the manifest
  pointer in baw_assetpackmanifest_res.

Reverting baw_err._static to bool fails both, naming all four affected
unions and tracing the field path down to _static.

Note that GCHandle.Alloc(.., Pinned) is not usable as a blittability check:
pinning only rejects types containing managed references, so it accepts a
struct with a bool field on both Mono and CoreCLR. The tests apply the
blittability rule to field types directly instead.

@ryzngard ryzngard left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this excellent find and quick fix @petarkarov! One thing I would do is at minimum add a comment explaining why this has to be a byte instead of a bool (https://learn.microsoft.com/en-us/dotnet/standard/native-interop/blittable-and-non-blittable-types has full documentation).

I've also put up a PR to your repo adding tests. That isn't fully necessary for this fix and I can handle adding later if you prefer.

Per review feedback on PR apple#114: add a comment documenting the
blittability requirement so the reasoning isn't lost to history.
@petarkarov

Copy link
Copy Markdown
Contributor Author

Thanks for this excellent find and quick fix @petarkarov! One thing I would do is at minimum add a comment explaining why this has to be a byte instead of a bool (https://learn.microsoft.com/en-us/dotnet/standard/native-interop/blittable-and-non-blittable-types has full documentation).

I've also put up a PR to your repo adding tests. That isn't fully necessary for this fix and I can handle adding later if you prefer.

Hi @ryzngard , thanks for the fast response and for the review!

I've added the same explanatory comment on _static in this commit: #114 (commits)

As for the unit tests — I'd like to verify them locally first, and I'll follow up with a separate PR to add them once I have.

// would make every struct embedding this one non-blittable, and IL2CPP would then marshal
// those field by field — which silently corrupts the unions that overlay baw_err with a
// success value. [MarshalAs] does not help: it sets the width of the conversion, it does
// not remove it. See Tests/TestInteropBlittability.cs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: There's no tests in this PR :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I've added a comment in the unit tests PR, approved it and merged it, so they are visible now here.

@petarkarov

Copy link
Copy Markdown
Contributor Author

Hello @ryzngard do you have ETA for when this is planned to be merged?
Thanks!

@ryzngard
ryzngard merged commit 2c7d520 into apple:main Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants