Install a fresh StAX resolver floor per hook instead of mutating one - #75
Merged
garydgregory merged 1 commit intoSep 1, 2026
Merged
Conversation
Routing a caller resolver by calling setDelegate on the floor already installed treated that floor as private to the hook being set, which it is not. Woodstox routes setXMLResolver to both its DTD-subset and entity hooks, so one floor object sits on several of them, and its ReaderConfig.createNonShared copies the reference into every reader it creates. Mutating the object therefore answered hooks the call never named, and changed the resolution policy of readers created earlier, including ones already parsing on another thread. Install a new floor on the named hook instead. Each hook then keeps the floor it was given and each reader the one it captured, so a resolver is scoped to the hook it was set on and bound when the reader was made. Nothing is mutated after publication any more, which also removes the unsynchronized cross-thread write. The com.ctc.wstx.* hooks had no tests; they have two now, both verified to fail without the main-code change. The existing setXMLResolver test asserted the mutation itself, so it now asserts the contract it was standing in for: the hook keeps a floor and the caller sits behind it. Assisted-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CLnTBsvmYtxzNTWVGNyz33
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.
setResolverPropertyrouted a caller's resolver by callingsetDelegateon the floor already installed on the named hook. That treats the floor as private to that hook, and it is not — confirmed in Woodstox's own sources rather than inferred:The constructor installs the floor through
setXMLResolver, so on Woodstox one object lands on both the DTD-subset and entity hooks, andcreateNonSharedcopies that reference into every reader the factory creates. Mutating it therefore had two effects the caller never asked for:com.ctc.wstx.dtdResolveralso answeredcom.ctc.wstx.entityResolver(and the reverse), silently widening the scope of an opt-in and discarding any resolver previously set on the other hook;Everything stayed behind a floor throughout: an unresolved reference still resolved to empty and no raw fetch was ever re-opened. This is a state and aliasing bug, in the same family as the shared mutable empty document in #69, not a way to bypass the securing.
The fix installs a new floor on the named hook rather than re-delegating the one already there. Each hook then keeps the floor it was given and each reader the one it captured, which is both per-hook independence and the creation-time binding StAX callers expect. Since nothing is mutated after publication any more, the cross-thread write disappears too, so no
volatileis needed.Tests
The
com.ctc.wstx.*hooks had no coverage at all. Two tests are added, and I verified both fail against the pre-fix code, so they discriminate rather than passing vacuously:woodstoxResolverHooksStayIndependent— a resolver set on the DTD hook must not answer the entity hook (skips where the implementation does not know the properties).settingAResolverInstallsAFreshFloorInsteadOfMutatingTheInstalledOne— the floor an existing reader captured keeps resolving to empty.setXMLResolverRoutesCallerBehindInstalledFloorasserted the old mechanism (that the installed floor object be mutated, and thatsetPropertynever be called on the hook), which is exactly what changes here. It now asserts the contract it was standing in for: the hook still holds a floor, the caller's resolver is its delegate, and both getters report it unwrapped.Full surefire matrix green. No
changes.xmlentry: the behaviour is fail-secure throughout and lands in the same release cycle that introduced it.🤖 Generated with Claude Code