Background Assets: fix EXC_BAD_ACCESS in AssetPackManifest on iOS 26 (non-blittable baw_err) - #114
Conversation
…(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.
…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
left a comment
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this comment.
nit: There's no tests in this PR :)
There was a problem hiding this comment.
Hey, I've added a comment in the unit tests PR, approved it and merged it, so they are visible now here.
|
Hello @ryzngard do you have ETA for when this is planned to be merged? |
Background Assets: add blittability guards for the interop structs
Summary
Apple.BackgroundAssets.Error.baw_errdeclares its_staticfield as a C#bool. A C#boolis not blittable, so IL2CPP generates a marshaled copy for every struct that embedsbaw_err, including the unionbaw_assetpackmanifest_res. The generated unmarshal code copies the union'ssuccessmember and then itsfailuremember 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
bytemakesbaw_errblittable. 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()orGetAllAssetPacks()afterAssetPackManager.GetManifestAsync()crashes:The faulting instruction is
ldr x1, [x21]wherex21iscManifest.compat.assetpackv, and its value is1.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):successandfailureoverlap at offset 0. After the second assignment, bytes 8..15 contain0x0000000000000001(theboolconverted from the marshaledint32), which is wherebaw_assetpackmanifest_compat.assetpackvlives on iOS 26.A secondary mismatch is fixed by the same change: C
boolis 1 byte, but C#boolis marshaled as a 4-byteBOOLby default, so_staticwas 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_pinvokevariant, and no copy is performed.Verification
ReversePInvokeWrapper_AssetPackManager_ManifestCallbacknow 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 plainbaw_errcallbacks). They didn't crash only because their overlapping bytes happened to carry the same values, so this change makes them correct as well.