Skip to content

HV-1916 Do not write container information to the shared root path node - #2083

Open
Develop-KIM wants to merge 1 commit into
hibernate:mainfrom
Develop-KIM:HV-1916
Open

HV-1916 Do not write container information to the shared root path node#2083
Develop-KIM wants to merge 1 commit into
hibernate:mainfrom
Develop-KIM:HV-1916

Conversation

@Develop-KIM

@Develop-KIM Develop-KIM commented Aug 6, 2026

Copy link
Copy Markdown

https://hibernate.atlassian.net/browse/HV-1916

Reduced after review. The reported symptom is indeed a misuse of the API — see the discussion below. What is left here is the one defect the investigation turned up, which is independent of it.

MutableNode.ROOT_NODE is a singleton shared by every path. inIterable() / atIndex() / atKey() / inContainer() write the container information onto the current leaf node of the path being built, and for a violation built from a class-level constraint — where ConstraintViolationBuilderImpl#dropLeafNodeIfRequired drops the leaf bean node — that current leaf is the shared root when the first node of the path is customized:

context.buildConstraintViolationWithTemplate( message )
        .addPropertyNode( "jobs" ).inIterable().atIndex( 3 )
        .addConstraintViolation();

The write then stays on the singleton for the lifetime of the JVM. It never shows up in the violation path itself, because the first node is re-parented to the pristine materialized root on the way out, but every path built afterwards hands the flags out on its first node. A TraversableResolver sees it:

traversableProperty for a plain @NotNull property of an unrelated bean
before the violation above is built name inIterable=false index=null
after it name inIterable=true index=3

Fix

MutablePath replaces the shared root with a private copy before any of those mutators writes to it. Violation paths come out exactly as they do today; the only thing that changes is that the write no longer escapes the path it belongs to.

Verification

The added test fails on main (expected [false] but found [true]) and passes with the fix. Full engine suite green — 2485 tests, JDK 25 — and spotless:check / checkstyle:check clean.

I used Claude Code while working on this; the analysis and the change are mine and I can walk through them.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on licensing, please check here.


@hibernate-github-bot

hibernate-github-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

Thanks for your pull request!

This pull request appears to follow the contribution rules.

› This message was automatically generated.

@marko-bekhta

Copy link
Copy Markdown
Member

Hey @Develop-KIM 👋🏻

thanks for the pull request, I think we might be trying to fix a non-existing issue here, please see the comment: https://hibernate.atlassian.net/browse/HV-1916?focusedCommentId=147800

`inIterable()`, `atIndex()`, `atKey()` and `inContainer()` write the container
information onto the current leaf node of the violation path. For a violation
built from a class-level constraint the leaf bean node is dropped, so when the
first node added to the path is customized that current leaf is the path's root
node -- and `MutableNode.ROOT_NODE` is a singleton shared by every path.

The write therefore stays there for the lifetime of the JVM. It is invisible in
the violation path itself, because the first node is re-parented to the pristine
materialized root, but every path built afterwards hands the flags out on its
first node, e.g. to a `TraversableResolver`, which then sees a top-level property
reported as being at an index inside an iterable.

Replace the shared root with a private copy before writing to it.

Signed-off-by: Donghwan Kim <kimdonghwan913@gmail.com>
@Develop-KIM Develop-KIM changed the title HV-1916 Keep iterable information set on the first node of a custom violation path HV-1916 Do not write container information to the shared root path node Aug 14, 2026
@Develop-KIM

Copy link
Copy Markdown
Author

Thanks for taking a look, and you're right about the report itself — addContainerElementNode is about type arguments, and the path the reporter wanted comes from addPropertyNode("jobs").addBeanNode().inIterable().atIndex(index). I checked and that chain does produce jobs[3] today, so there's nothing to fix there.

While digging into it I did run into one thing I think is a real bug, so I've cut the PR down to just that part.

MutableNode.ROOT_NODE is a singleton shared by every path, and inIterable() / atIndex() / atKey() / inContainer() write onto the current leaf node. For a class-level constraint the leaf bean node is dropped, so if the first node of a custom path gets customized, that write lands on the shared singleton and stays there for the rest of the JVM's life. It's invisible in the violation path — the first node is re-parented to the pristine materialized root on the way out — but anything handed a live path node afterwards sees it. A TraversableResolver, for instance:

// validator A: class-level constraint doing
//   .addPropertyNode( "jobs" ).inIterable().atIndex( 3 ).addConstraintViolation()
pollutingValidator.validate( new ClassLevel() );

// validator B, built separately, validating a bean with a single @NotNull String name
validator.validate( new Plain() );
before: name inIterable=false index=null
after:  name inIterable=true  index=3

Different bean, different validator, next call. Whether or not the chain that triggers it is a sensible thing to write, I don't think a misuse should be able to poison every validation that follows it — and the JPA resolver is on the receiving end of those nodes.

The fix is now three calls in MutablePath: swap in a private root before writing to it. Nothing about the violation paths changes; the full engine suite is green (2485 tests) and the one test I kept fails on main.

Happy to close this if you'd rather leave it as is, and I can move it to a fresh JIRA key since HV-1916 is closed — whichever you prefer.

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