Repository navigation
Fill a TypedValue of our own, not the one Resources lends out - #1048
Merged
Merged
Conversation
XResources.getLayout labels a layout's callbacks with the variant from its resource path, and took the TypedValue for that read out of Resources' mTmpValue. The field is a one-slot pool rather than scratch space: obtainTempTypedValue hands the instance over and leaves it null under mTmpValueLock until releaseTempTypedValue puts it back, which fourteen methods of Resources do, unchanged from android10-release to android16-release. The borrowed value is therefore null whenever another lookup on the same Resources is in flight, and AssetManager.getResourceValue opens with Objects.requireNonNull(outValue, "outValue"): InflateException: Binary XML file line #31 in com.android.systemui:layout/scene_window_root: outValue Caused by: NullPointerException: outValue seen on the Android 17 QPR3 beta, where a module hooking the shade layout meets it through an <include> while SystemUIService is created, and SystemUI crash-looped at boot until an inflation found the slot full. Allocate one instead, on a path already building a parser and a callback list; nothing then depends on which lookup held the slot. XTypedArray.peekValue keeps the field it borrows, TypedArray.mValue being assigned only where it is declared and returning that instance being peekValue's contract.
JingMatrix
force-pushed
the
layout-hook-own-typed-value
branch
from
October 30, 2026 13:00
e6f4227 to
3a3aadc
Compare
Owner
Author
|
@jiggs82 Thanks for reporting this issue, it should be fixed now in the CI build: https://github.com/JingMatrix/Vector/actions/runs/41059640616 Please confirm it so that I can merge. |
|
Yup it seems to be working fine for me thanks for the quick fix 👍🏻 |
A layout hook is filed under the resource directory it was registered for, and a system-wide one under null. getLayout honours both keys when it looks for the callbacks and only the first when it looks for the names, so a framework layout hooked through hookSystemWideLayout and inflated by an app — what that API is for — reaches the callback with resNames null, and a module reading a name off it gets a NullPointerException instead. Look the names up under both keys. Their registration also moves ahead of the callbacks'. An inflation takes the callbacks first and the names second, and one that finds callbacks with no names records nothing against the document it returns, so a callback added a moment later misses that document as well: the names have to be there before anything can find a callback. A first callback is likewise added to its set before the set is published, a set being findable while still empty having the same effect for the inflation that finds it. Also here: the id of a layout whose file name cannot be read is put inside the logged exception rather than concatenated to it, which turned the whole NotFoundException into its own message and dropped the stack.
… them The constructor takes (id, pkg, name, type) and spells a full name as pkg:type/name, which is the form getResourceName returns. Both factories passed getResourceTypeName as the name and getResourceEntryName as the type, so every layout callback received android:simple_list_item_1/layout for what is android:layout/simple_list_item_1, with name and type the same way round. ResourceNames.equals, whose whole purpose is to let a module match on the type or the entry it asked for, could not match either. Pass them as the parameters are named. The swap is inherited: LSPosed's own XResources has it too, so a module that worked around it by naming the type where a name belongs will stop matching, which is the cost of the fields meaning what they are documented to mean.
…are read A replacement's id is noted in a bitmap first and in sReplacements second, so a resource read whose bit is clear returns the original without taking a lock. Two things made that bit unreliable. It was a byte[] written under a monitor and read under none, which the memory model lets a reader miss for as long as it likes: a module registering replacements from handleInitPackageResources while the app reads resources on its own threads is the ordinary case, not an exotic one. An AtomicIntegerArray element is read with a volatile load and set with a compare-and-swap, which is the ordering this wants and still no lock on the read. A loop rather than getAndUpdate, whose lambda may not name this class. And it was set outside the lock that isFirstLoad clears it under, so a replacement registered just as a module was reloaded could keep its row in sReplacements and lose its bit, leaving the resource unreplaceable for the life of the process. The bit now goes in beside the row, under that same lock. The bitmaps carry exactly the bits the old key-and-offset arithmetic reached, 0x000700ff of a framework id and 0x0007007f of an app's, each now named once as the bit an id stands for. The array reference is final, so no thread can reach a resource lookup through an object whose cache it cannot see yet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four defects in the legacy resource hooks, found chasing one of them.
The borrowed
TypedValue.XResources.getLayoutneeds the variant segment of a layout's resource path to label theXC_LayoutInflatedcallbacks registered for that id, and read it through theTypedValuesitting inResources.mTmpValue, a field reached by reflection rather than one of ours. That field is a pool of one, in AOSP's own wording: declared beside the lock that guards it, handed out byobtainTempTypedValue, which leaves itnulluntilreleaseTempTypedValue"returns a TypedValue to the pool", and taken that way by fourteen methods ofResources—getLayoutby way ofloadXmlResourceParser, the one this override calls through to, among them. android10-release holds the same pool, so there is no API change to adapt to: the borrowed value has always beennullwhenever another lookup on the sameResourceswas in flight, andAssetManager.getResourceValueopens withObjects.requireNonNull(outValue, "outValue"). What changed is how often it overlaps: on the Android 17 QPR3 beta a module hooking SystemUI's shade layout meets it through an<include>—scene_window_root, absent from android14-release, wrapssuper_notification_shadeand is inflated whileSystemUIServiceandKeyguardServiceare created — and SystemUI crash-loops at boot until an inflation finds the slot full. That a later attempt on the same build and device comes up is the race showing itself.Names a system-wide hook never received. A layout hook is filed under the resource directory it was registered for and a system-wide one under
null.getLayouthonoured both keys for the callbacks and only the first for the names, so a framework layout hooked throughhookSystemWideLayoutand inflated by an app — what that API is for — reached the callback withresNamesnull. Their registration also moves ahead of the callbacks', and a first callback now joins its set before the set is published, because an inflation reads the two in the other order and one that finds callbacks with no names records nothing against the document it returns.ResourceNamesbuilt the other way round. The constructor takes(id, pkg, name, type)and spells a full name aspkg:type/name, the formgetResourceNamereturns; both factories passed the type name as the name and the entry name as the type. Every layout callback has been receivingandroid:simple_list_item_1/layoutfor what isandroid:layout/simple_list_item_1, andResourceNames.equalscould match neither field. The swap is inherited — LSPosed'sXResourceshas it too — so a module that worked around it will stop matching, which is the price of the fields meaning what they document.A cache bit that could be missed or lost. A replacement's id is noted in a bitmap first and in
sReplacementssecond, so a resource read whose bit is clear returns the original without taking a lock. The bitmap was abyte[]written under a monitor and read under none, which the memory model lets a reader miss for as long as it likes, and it was set outside the lockisFirstLoadclears it under, so a replacement registered as a module reloaded could keep its row and lose its bit — unreplaceable for the life of the process. It is now anAtomicIntegerArray, read with a volatile load, set with a compare-and-swap beside the row under the same lock, and held in a final field. The bits are exactly the ones the old key-and-offset arithmetic reached,0x000700ffof a framework id and0x0007007fof an app's, each named once as the bit an id stands for.Three things in the same code were looked at and left alone:
XTypedArray.peekValuekeeps the field it borrows, sinceTypedArray.mValueis assigned only where it is declared and returning that instance is its contract;mParseStateandmBlockare still spelled asXResourcesreads them in android10-release and android16-release alike, and the native side returns early on a null parse state; and every id-taking getter ofResourcesthat Xposed documents as replaceable is still overridden,getQuantityStringarriving through the overriddengetQuantityText.