Skip to content

Prevent mutation of various records#1353

Merged
njr-11 merged 4 commits intojakartaee:mainfrom
njr-11:prevent-mutation-of-records
Feb 11, 2026
Merged

Prevent mutation of various records#1353
njr-11 merged 4 commits intojakartaee:mainfrom
njr-11:prevent-mutation-of-records

Conversation

@njr-11
Copy link
Copy Markdown
Member

@njr-11 njr-11 commented Feb 5, 2026

@gavinking pointed out in jakartaee/persistence#944 that records provided by the Jakarta Data API might be vulnerable to mutation. I looked into this and found 3 places (fixed in this PR) plus one other place that I did not cover in this PR.

The one that is not covered is,

Literal.of(anyValue)

A user could supply many different types of mutable objects to that method and later change them, or could invoke value() to retrieve its value and then mutate the value, impacting subsequent invocations of .value(). However, it does not seem reasonable to try to identify all possible mutable types and provide code to copy them.

@njr-11 njr-11 added this to the 1.1 milestone Feb 5, 2026
Comment on lines +64 to +66
this.pageRequest = copy(pageRequest);
this.nextPageRequest = copy(nextPageRequest);
this.previousPageRequest = copy(previousPageRequest);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is it necessary to copy PageRequests?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Pagination is immutable, no?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Pagination is immutable, no?

Good point, if we assume the PageRequest implementation is always the immutable built-in one, then we don't need to copy it.

* Key values.
*/
private final Object[] key;
private final List<Object> key;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually @njr-11 I don't think we need to make such a radical change here either. This is a private field, belonging to a package private class. As long as you just clone() the array in the constructor, you are fine. No need to change it to List. That's just an extra object instantiation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The reason I switched to a List was that PageRequestCursor's implementation of the List<?> elements() method was already converting from array to List (and doing so each time invoked), so I thought the List conversion would need to be done at least once regardless, making the overall usage less costly than including an extra clone. However, I went back and checked, and there is an alternative to the elements() method, which is to invoke cursor.get(index) to retrieve each element individually. If an implementation is doing that instead of using cursor.elements(), then cloning the array would be more efficient as you point out. I'm not sure which approach implementations are actually going with.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Aah ok, I see.

Copy link
Copy Markdown
Member Author

@njr-11 njr-11 Feb 5, 2026

Choose a reason for hiding this comment

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

I just noticed that the only API method that invokes the PageRequestCursor constructor is Cursor.forKey(Object ...)

        static Cursor forKey(Object... key) {
            return new PageRequestCursor(key);
        }

Given that it's a varargs, it will almost always be an array instance created by Java that the application doesn't even have a reference to in order to mutate. Technically, someone could pass in an Object[] to it, in which there is risk of the array being modified. But I'm tempted to just put this code back how it was originally and do nothing about this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

With my prior comment, I convinced myself that the original approach of no copying for Cursor.forKey is best. Technically, it is just as vulnerable to someone mutating the individual values within the array as it is to someone altering the array itself. A user doing that would be trying to make themselves fail. I'd rather just have clarifying text indicating don't do that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I also improved the documentation of this method while I was there.

implements Page<T> {

// Disallow mutation of PageRequest and List fields after creation
public PageRecord(PageRequest pageRequest,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shall we have a test to prove this behavior?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Shall we have a test to prove this behavior?

sure, I added a test for you,
c41b0e2

@njr-11 njr-11 merged commit 7d367f8 into jakartaee:main Feb 11, 2026
4 checks passed
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.

4 participants