Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crash in LSET command when replacing small list items with larger ones, creating listpacks larger than 4GB #12955

Merged
merged 5 commits into from
Feb 6, 2024

Conversation

sundb
Copy link
Collaborator

@sundb sundb commented Jan 16, 2024

Fix #12864

The main reason for this crash is that when replacing a element of a quicklist packed node with lpReplace() method,
if the final size is larger than 4GB, lpReplace() will fail and returns NULL, causing node->entry to be incorrectly set to NULL.

Since the inserted data is not a large element, we can't just replace it like a large element, first quicklistInsertAfter()
and then quicklistDelIndex(), because the current node may be merged and invalidated in quicklistInsertAfter().

The solution of this PR:
When replacing a node fails (listpack exceeds 4GB), split the current node, create a new node to put in the middle, and try to merge them.
This is the same as inserting a large element.
In the worst case, its size will not exceed 4GB.

Release notes:
Crash in LSET command when replacing small items and exceeding 4GB

@sundb sundb changed the title Prevent LSET command from causing quicklist node size to exceed 4GB Prevent LSET command from causing quicklist plain node size to exceed 4GB Jan 16, 2024
Copy link
Collaborator

@enjoy-binbin enjoy-binbin left a comment

Choose a reason for hiding this comment

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

did not CR, but great work, thanks for finding this

@sundb
Copy link
Collaborator Author

sundb commented Jan 16, 2024

@enjoy-binbin in fact it was found by @imchuncai.

@sundb
Copy link
Collaborator Author

sundb commented Jan 17, 2024

I'd actually prefer to continue ##12659, which is a better solution to the problem of over-sized nodes due to LSET commands.

@sundb sundb requested a review from oranagra February 6, 2024 09:05
Copy link
Member

@oranagra oranagra left a comment

Choose a reason for hiding this comment

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

LGTM.
just to confirm, this pile of new code in quicklistReplaceEntry is reachable with other tests we have (ones not using large-memory).
i.e. if we had some off by one bug inserting the new entry in the wrong index, would it be detected?

@sundb
Copy link
Collaborator Author

sundb commented Feb 6, 2024

@oranagra yes, current tests without --large-memory have 100% covered quicklistReplaceEntry(), which is verified by lcov.

@oranagra oranagra merged commit 1f00c95 into redis:unstable Feb 6, 2024
13 checks passed
@oranagra oranagra added the release-notes indication that this issue needs to be mentioned in the release notes label Feb 6, 2024
sundb added a commit to sundb/redis that referenced this pull request Feb 7, 2024
sundb added a commit to sundb/redis that referenced this pull request Feb 8, 2024
sundb added a commit to sundb/redis that referenced this pull request Feb 8, 2024
sundb added a commit to sundb/redis that referenced this pull request Feb 8, 2024
sundb added a commit to sundb/redis that referenced this pull request Feb 8, 2024
oranagra pushed a commit that referenced this pull request Feb 8, 2024
Fix two crash introducted by #12955

When a quicklist node can't be inserted and split, we eventually merge
the current node with its neighboring
nodes after inserting, and compress the current node and its siblings.

1. When the current node is merged with another node, the current node
may become invalid and can no longer be used.

   Solution: let `_quicklistMergeNodes()` return the merged nodes.

3. If the current node is a LZF quicklist node, its recompress will be
1. If the split node can be merged with a sibling node to become head or
tail, recompress may cause the head and tail to be compressed, which is
not allowed.

    Solution: always recompress to 0 after merging.
roggervalf pushed a commit to roggervalf/redis that referenced this pull request Feb 11, 2024
… 4GB (redis#12955)

Fix redis#12864

The main reason for this crash is that when replacing a element of a
quicklist packed node with lpReplace() method,
if the final size is larger than 4GB, lpReplace() will fail and returns
NULL, causing `node->entry` to be incorrectly set to NULL.

Since the inserted data is not a large element, we can't just replace it
like a large element, first quicklistInsertAfter()
and then quicklistDelIndex(), because the current node may be merged and
invalidated in quicklistInsertAfter().

The solution of this PR:
When replacing a node fails (listpack exceeds 4GB), split the current
node, create a new node to put in the middle, and try to merge them.
This is the same as inserting a large element.
In the worst case, its size will not exceed 4GB.
roggervalf pushed a commit to roggervalf/redis that referenced this pull request Feb 11, 2024
…edis#13040)

Fix two crash introducted by redis#12955

When a quicklist node can't be inserted and split, we eventually merge
the current node with its neighboring
nodes after inserting, and compress the current node and its siblings.

1. When the current node is merged with another node, the current node
may become invalid and can no longer be used.

   Solution: let `_quicklistMergeNodes()` return the merged nodes.

3. If the current node is a LZF quicklist node, its recompress will be
1. If the split node can be merged with a sibling node to become head or
tail, recompress may cause the head and tail to be compressed, which is
not allowed.

    Solution: always recompress to 0 after merging.
@sundb sundb deleted the lset_exceed_4gb branch March 5, 2024 03:10
@oranagra oranagra changed the title Prevent LSET command from causing quicklist plain node size to exceed 4GB Fix crash in LSET command when replacing small list items with larger ones, creating listpacks larger than 4GB May 13, 2024
YaacovHazan pushed a commit to YaacovHazan/redis that referenced this pull request May 16, 2024
… 4GB (redis#12955)

Fix redis#12864

The main reason for this crash is that when replacing a element of a
quicklist packed node with lpReplace() method,
if the final size is larger than 4GB, lpReplace() will fail and returns
NULL, causing `node->entry` to be incorrectly set to NULL.

Since the inserted data is not a large element, we can't just replace it
like a large element, first quicklistInsertAfter()
and then quicklistDelIndex(), because the current node may be merged and
invalidated in quicklistInsertAfter().

The solution of this PR:
When replacing a node fails (listpack exceeds 4GB), split the current
node, create a new node to put in the middle, and try to merge them.
This is the same as inserting a large element.
In the worst case, its size will not exceed 4GB.

(cherry picked from commit 1f00c95)
YaacovHazan pushed a commit to YaacovHazan/redis that referenced this pull request May 16, 2024
…edis#13040)

Fix two crash introducted by redis#12955

When a quicklist node can't be inserted and split, we eventually merge
the current node with its neighboring
nodes after inserting, and compress the current node and its siblings.

1. When the current node is merged with another node, the current node
may become invalid and can no longer be used.

   Solution: let `_quicklistMergeNodes()` return the merged nodes.

3. If the current node is a LZF quicklist node, its recompress will be
1. If the split node can be merged with a sibling node to become head or
tail, recompress may cause the head and tail to be compressed, which is
not allowed.

    Solution: always recompress to 0 after merging.

(cherry picked from commit 1e8dc1d)
YaacovHazan pushed a commit that referenced this pull request May 19, 2024
… 4GB (#12955)

Fix #12864

The main reason for this crash is that when replacing a element of a
quicklist packed node with lpReplace() method,
if the final size is larger than 4GB, lpReplace() will fail and returns
NULL, causing `node->entry` to be incorrectly set to NULL.

Since the inserted data is not a large element, we can't just replace it
like a large element, first quicklistInsertAfter()
and then quicklistDelIndex(), because the current node may be merged and
invalidated in quicklistInsertAfter().

The solution of this PR:
When replacing a node fails (listpack exceeds 4GB), split the current
node, create a new node to put in the middle, and try to merge them.
This is the same as inserting a large element.
In the worst case, its size will not exceed 4GB.

(cherry picked from commit 1f00c95)
YaacovHazan pushed a commit that referenced this pull request May 19, 2024
Fix two crash introducted by #12955

When a quicklist node can't be inserted and split, we eventually merge
the current node with its neighboring
nodes after inserting, and compress the current node and its siblings.

1. When the current node is merged with another node, the current node
may become invalid and can no longer be used.

   Solution: let `_quicklistMergeNodes()` return the merged nodes.

3. If the current node is a LZF quicklist node, its recompress will be
1. If the split node can be merged with a sibling node to become head or
tail, recompress may cause the head and tail to be compressed, which is
not allowed.

    Solution: always recompress to 0 after merging.

(cherry picked from commit 1e8dc1d)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes indication that this issue needs to be mentioned in the release notes
Projects
Status: In Progress
Status: Done
Development

Successfully merging this pull request may close these issues.

[CRASH] in quicklist violate size limit
3 participants