Skip to content

MDEV-40378 heap: roll back key changes when a blob write fails in heap_update() - #5407

Open
arcivanov wants to merge 1 commit into
MariaDB:preview-13.1-previewfrom
arcivanov:MDEV-40378
Open

MDEV-40378 heap: roll back key changes when a blob write fails in heap_update()#5407
arcivanov wants to merge 1 commit into
MariaDB:preview-13.1-previewfrom
arcivanov:MDEV-40378

Conversation

@arcivanov

@arcivanov arcivanov commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

heap_update() moves all changed key entries to the new key values before writing the new blob chains. When a blob chain write then fails (e.g. HA_ERR_RECORD_FILE_FULL on a full HEAP table), the rollback restores the record bytes and blob chain pointers, but the err: label only undoes key changes for HA_ERR_FOUND_DUPP_KEY — historically the only possible failure once the key loop had run.

The hash/btree entries are left keyed on the new values while pointing at a record holding the old values, corrupting the index:

  1. index lookups by the old key value miss the row;
  2. CHECK TABLE reports the table corrupt;
  3. on debug builds the heap consistency check in ha_heap::external_lock() raises a second error into an already-set diagnostics area, firing a Diagnostics_area assertion on the next statement.

Fix

Widen the err: recovery gate to also run for HA_ERR_RECORD_FILE_FULL, HA_ERR_OUT_OF_MEM and ENOMEM, so a failure raised after the key loop also moves every changed key back to its old value. One recovery path now serves every failure that leaves keys moved to their new values, including any future error source in the key loop itself.

The err: block assumed the failure happened inside the key loop, so that keydef addresses the partially processed keydef. A blob-chain write fails after that loop has run to completion, and therefore arrives with keydef == keydef_end. Reading info->errkey and keydef->algorithm from there addresses share->keydef[share->keys]; since sizeof(HP_KEYDEF) (888) far exceeds the key segments and blob descriptors that follow the keydef array, that read runs past the end of the HP_SHARE allocation, and the rollback sweep then dereferences a garbage keydef->seg in hp_rec_key_cmp(). The recovery therefore distinguishes the two failure sites: with keydef == keydef_end there is no partly updated key to repair and none to name in info->errkey, and the sweep starts at the last keydef instead. The same branch also covers a table with no keys at all (share->keys == 0), where the sweep has nothing to do.

info->errkey is initialized to -1 on entry to err:, so a failure that is not a key error can never expose a stale key number from an earlier operation.

The original errno is captured before the recovery and restored after it, so a rollback write_key failure (which hp_rb_write_key() reports as HA_ERR_FOUND_DUPP_KEY with a stale errkey) cannot mask it.

Test

New heap.blob_update_key_rollback exercises:

  • hash index, single-row UPDATE;
  • BTREE index, single-row UPDATE;
  • two changed indexes (both rolled back);
  • an index on an unchanged column (rollback must leave it untouched);
  • a partial multi-row UPDATE (invariant-only assertions, no platform-dependent fill point);
  • a table with no indexes at all (the keydef == keydef_end branch with share->keys == 0).

Each asserts table consistency after the failure via CHECK TABLE and index lookups. Verified to crash the unfixed server (DA assertion / index-corrupt) and pass with the fix; full heap suite passes.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@gkodinov gkodinov added the External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. label Jul 20, 2026
@gkodinov gkodinov self-assigned this Jul 20, 2026

@gkodinov gkodinov left a comment

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.

Thank you for your contribution. This is a preliminary review.

From the remarks in the Jira I'm assuming these are problems found with the main contribution. I still think it's best if this goes to main. But, after discussing with my colleagues, I'm approving it as is and leaving this to the final reviewer to reply.

@arcivanov

Copy link
Copy Markdown
Contributor Author

No, this code is not on main, it's in preview 13.1. I literally can't PR this into main.

…ap_update()`

`heap_update()` moves all changed key entries to the new key values
**before** writing the new blob chains. When a blob chain write then
failed (e.g. with `HA_ERR_RECORD_FILE_FULL`), the rollback restored the
record bytes and blob chain pointers, but the `err:` label only undid
key changes for `HA_ERR_FOUND_DUPP_KEY` -- historically the only
possible failure once the key loop had run. The hash/btree entries were
left keyed on the new values while pointing at a record holding the old
values, corrupting the index:

1. index lookups by the old key value missed the row
2. `CHECK TABLE` reported the table corrupt
3. on debug builds the heap consistency check in
   `ha_heap::external_lock()` raised a second error into an already-set
   diagnostics area, firing a `Diagnostics_area` assertion on the next
   statement

Fix: widen the `err:` recovery to run for `HA_ERR_RECORD_FILE_FULL`,
`HA_ERR_OUT_OF_MEM` and `ENOMEM` as well, so a failure raised after the
key loop also moves every changed key back to its old value. One
recovery path now serves every failure that leaves keys moved to their
new values, including any future error source in the key loop itself.

The `err:` block assumed the failure happened **inside** the key loop,
so that `keydef` addresses the partially processed keydef. A blob-chain
write fails after that loop has run to completion, and therefore
arrives with `keydef == keydef_end`. Reading `info->errkey` and
`keydef->algorithm` from there addresses `share->keydef[share->keys]`;
as `sizeof(HP_KEYDEF)` (888) far exceeds the key segments and blob
descriptors that follow the keydef array, that read runs past the end
of the `HP_SHARE` allocation, and the rollback sweep then dereferences
a garbage `keydef->seg` in `hp_rec_key_cmp()`. So the recovery
distinguishes the two failure sites: with `keydef == keydef_end` there
is no partly updated key to repair and none to name in `info->errkey`,
and the sweep starts at the last keydef instead. The same branch also
covers a table with no keys at all (`share->keys == 0`), where the
sweep has nothing to do.

`info->errkey` is initialized to `-1` on entry to `err:`, so a failure
that is not a key error can never expose a stale key number from an
earlier operation.

The original errno is captured before the recovery and restored after
it, so that a rollback `write_key` failure (which `hp_rb_write_key()`
reports as `HA_ERR_FOUND_DUPP_KEY` with a stale `errkey`) cannot mask
it.

The new test `heap.blob_update_key_rollback` exercises hash, BTREE, two
changed indexes, an index on an unchanged column (which the rollback
must leave untouched), a partial multi-row UPDATE, and a table with no
indexes at all; each asserts the table stays consistent after the
failure via `CHECK TABLE` and index lookups.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements.

Development

Successfully merging this pull request may close these issues.

3 participants