Skip to content

DAOS-19721 vos: fix stale iterator re-probe in vos_iter_validate - #19182

Open
mchaarawi wants to merge 11 commits into
masterfrom
mschaara/19721
Open

mchaarawi wants to merge 11 commits into
masterfrom
mschaara/19721

Conversation

@mchaarawi

Copy link
Copy Markdown
Contributor

When the scrubber yields mid-extent, the object or key it is scanning may be punched and aggregated away. vos_iter_validate_internal() re-probes parents with GE semantics, so it silently lands on the next entry and child iterators then probe freed tree roots, triggering "Embedded root is incompatible with integer keys" in btr_probe_embedded.

  • Restart from the object level if the held object was evicted.
  • Treat an OBJ/DKEY/AKEY re-probe that lands on a different entry as "entry gone".
  • Use ia_ev (not ia_sv) as the RECX anchor so corruption is marked on the correct extent.

Steps for the author:

  • Commit message follows the guidelines.
  • Appropriate Features or Test-tag pragmas were used.
  • Appropriate Functional Test Stages were run.
  • At least two positive code reviews including at least one code owner from each category referenced in the PR.
  • Testing is complete. If necessary, forced-landing label added and a reason added in a comment.

After all prior steps are complete:

  • Gatekeeper requested (daos-gatekeeper added as a reviewer).

When the scrubber yields mid-extent, the object or key it is scanning
may be punched and aggregated away. vos_iter_validate_internal()
re-probes parents with GE semantics, so it silently lands on the next
entry and child iterators then probe freed tree roots, triggering
"Embedded root is incompatible with integer keys" in btr_probe_embedded.

- Restart from the object level if the held object was evicted.
- Treat an OBJ/DKEY/AKEY re-probe that lands on a different entry as
  "entry gone".
- Use ia_ev (not ia_sv) as the RECX anchor so corruption is marked on
  the correct extent.

Signed-off-by: Mohamad Chaarawi <mohamad.chaarawi@hpe.com>
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Ticket title is 'btr_probe_embedded() Assertion '!btr_is_int_key(tcx)' failed: Embedded root is incompatible with integer keys '
Status is 'In Review'
Labels: '2.8.1rc2'
https://daosio.atlassian.net/browse/DAOS-19721

grom72 added a commit that referenced this pull request Oct 5, 2026
Add object_deleted_by_aggregation_and_gc to the VOS scrubbing test
suite. It reproduces the DAOS-19721 use-after-free: while the
scrubber yields mid-extent, the object it is scrubbing is fully
punched, aggregated away, and garbage collected. A decoy object with
a higher OID is kept alive so vos_iter_validate()'s OBJ-level
BTR_PROBE_GE re-probe lands on it instead of correctly reporting
the deleted object as gone, letting the DKEY-level re-probe run
against the freed/reused dkey tree root.

Pre-fix, this aborts the process with the btr_probe_embedded()
assertion from DAOS-19721. Post-fix (see PR #19182), vos_scrub_pool()
completes cleanly, skipping the deleted object.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Nasf-Fan
Nasf-Fan previously approved these changes Oct 7, 2026
@grom72

grom72 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

@mchaarawi

Copy link
Copy Markdown
Contributor Author

The fix does not seem to solve the problem as it is confirmed in https://daosio.atlassian.net/browse/DAOS-19769 (https://github.com/daos-stack/daos/pull/19205/changes#diff-7bfac1f5c3abd14bc39ecbd989eaaceeef15f87acbdb7921de922c72ed329da6).

looked into your pr and correct me if im wrong that CSUM_SCRUBBING_15 links the standalone libvos, where vos_sched_seq() is hard-wired to 0.
That means vos_iter_sched_check() is never true, and vos_iter_validate_internal() / vos_iter_cb() return before any of this PR's logic runs. correct?
The test also frees the object synchronously inside the callback with no yield, which can't happen for the scrubber in the engine. Aggregation and GC run in other ULTs, and every ULT switch bumps si_cur_seq.
from what i understand, the situation you target is real, but the way it gets there can't happen in the engine.

to make it valid per my understanding, you need to add a test hook that advances the scheduler counter, and do the delete through the scrubber's yield callback, like CSUM_SCRUBBING_08–10, not inline. then use two or more objects and delete one that isn't last. That reproduces the production sequence and would fail on master but should pass with this PR.
happy to do that if you want me to and you agree about the test itself; let me know.

@mchaarawi
mchaarawi marked this pull request as ready for review October 14, 2026 14:44
@mchaarawi
mchaarawi requested review from a team as code owners October 14, 2026 14:44
Comment thread src/vos/vos_iterator.c Outdated
}

/* Object punched/deleted while yielding; cached obj_df and tree roots may be freed */
if (iter->it_type != VOS_ITER_OBJ && vos_obj_is_evicted(vos_iter2oiter(iter)->it_obj)) {

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.

I don't follow the 'vos_obj_evicted()' checking, why it's necessary here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

as i said in the ticket, this patch is ai generated. but here is the reasoning:

Guards the case where the object is deleted and recreated with the same oid while we're yielded. The OBJ re-probe then matches the anchor, but the child iterators are still bound to the freed tree. Eviction is the only signal at that level, same as the check vos_io.c uses after a yield.

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.

I don't follow this.

No matter if the object is deleted or not (or recreated with same oid or not). This vos_iter_validate_internal() will always re-probe from the object level, that would re-open subtrees and recreate btree context for every level.

Comment thread src/vos/vos_iterator.c Outdated
rc = iter->it_ops->iop_probe(iter, anchor, VOS_ITER_PROBE_AGAIN);
vos_dth_set(old, is_sysdb);

/* PROBE_AGAIN is GE: a different entry means ours is gone and children hold stale roots */

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.

I'm totally lost. If the entry is deleted, it'll iterate to next entry, I don't see why children hold stale roots?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Child iterators are opened in place on the parent record's root (dbtree_open_inplace_ex(ii_btr) / evt_open(&krec->kr_evt)). When validation re-probes the parent and it lands on the next entry, validation returns 0, so the child re-probes its own handle, still bound to the deleted entry's freed root. That's the btr_probe_embedded assertion in the backtrace.

Comment thread src/vos/vos_iterator.c
break;
case VOS_ITER_RECX:
anchor = &iter->it_anchors->ia_sv;
anchor = &iter->it_anchors->ia_ev;

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.

good catch.

@NiuYawei

Copy link
Copy Markdown
Contributor

BTW, as for DAOS-19721, the sc_verify_recx() looks suspicious to me, it iterates csums in a loop, but I'm not quite sure if the csum is still valid after sc_verify_finish() (called in the loop) yielding.

@mchaarawi

Copy link
Copy Markdown
Contributor Author

BTW, as for DAOS-19721, the sc_verify_recx() looks suspicious to me, it iterates csums in a loop, but I'm not quite sure if the csum is still valid after sc_verify_finish() (called in the loop) yielding.

yea probably should snapshot before the loop.

sc_verify_recx() yields between chunks, but the stored csums it compares
against point into the evtree record. If aggregation frees that record
while the scrubber yields, the remaining chunks are compared against
freed memory and reported as corruption, which then triggers
vos_iter_validate() on a stale iterator chain.

Copy the stored csums into a local buffer before the loop, matching the
data that is already read into DRAM up front.

Features: checksum
Signed-off-by: Mohamad Chaarawi <mohamad.chaarawi@hpe.com>
@daosbuild3

Copy link
Copy Markdown
Collaborator

@grom72

grom72 commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

The fix does not seem to solve the problem as it is confirmed in https://daosio.atlassian.net/browse/DAOS-19769 (https://github.com/daos-stack/daos/pull/19205/changes#diff-7bfac1f5c3abd14bc39ecbd989eaaceeef15f87acbdb7921de922c72ed329da6).

looked into your pr and correct me if im wrong that CSUM_SCRUBBING_15 links the standalone libvos, where vos_sched_seq() is hard-wired to 0. That means vos_iter_sched_check() is never true, and vos_iter_validate_internal() / vos_iter_cb() return before any of this PR's logic runs. correct? The test also frees the object synchronously inside the callback with no yield, which can't happen for the scrubber in the engine. Aggregation and GC run in other ULTs, and every ULT switch bumps si_cur_seq. from what i understand, the situation you target is real, but the way it gets there can't happen in the engine.

to make it valid per my understanding, you need to add a test hook that advances the scheduler counter, and do the delete through the scrubber's yield callback, like CSUM_SCRUBBING_08–10, not inline. then use two or more objects and delete one that isn't last. That reproduces the production sequence and would fail on master but should pass with this PR. happy to do that if you want me to and you agree about the test itself; let me know.

I have updated #19205 based on your feedback and now can confirm that the fix works.

@NiuYawei NiuYawei left a comment

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.

Mark as -1 until my concern is addressed.

Comment thread src/vos/vos_iterator.c Outdated
}

/* Object punched/deleted while yielding; cached obj_df and tree roots may be freed */
if (iter->it_type != VOS_ITER_OBJ && vos_obj_is_evicted(vos_iter2oiter(iter)->it_obj)) {

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.

I don't follow this.

No matter if the object is deleted or not (or recreated with same oid or not). This vos_iter_validate_internal() will always re-probe from the object level, that would re-open subtrees and recreate btree context for every level.

Comment thread src/vos/vos_iterator.c
(iter->it_type == VOS_ITER_OBJ || iter->it_type == VOS_ITER_DKEY ||
iter->it_type == VOS_ITER_AKEY) &&
memcmp(saved.da_buf, anchor->da_buf, sizeof(saved.da_buf)) != 0)
rc = -DER_NONEXIST;

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.

Looks AI is talking to Ghost. This looks not correct to me. (see my comment above)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok im checking things myself now, and correct my understanding if im wrong as im not familiar with this part of the code.

The scrubber is walking the data in a nested way: object → dkey → akey → extents. It keeps a cursor open at each level. while it is checking an extent, it can yield. but during that yield/pause, aggregation might happen and delete the object it is looking at. vos_iter_validate() is what now validates that we are still working with soemthing real.
as i understand the assertion we saw on aurora from the trace:

  • Each one of the cursors under the object (dkey, akey, extent) holds a pointer into the associated object's memory.
  • When validation asks the object level to find its position again and the object is gone, it lands on the next object and reports valid / 0.
  • the lower level carry on using their old pointers into the deleted object's freed memory.
  • as a result we see the crash / assertion.
    This is my interpretation of the issue @NiuYawei. if you disagree, can you please investigate the trace yourself and let me know what did i miss?

i believe the pushed fix is just returning >0 instead of 0 in the above.
you mentioned that validation reopens the lower cursors fresh every time. are you sure?? can you check again please. as i understand, It only reopens them when a level reports > 0 and before this fix the object level never reported > 0 in this case. please check. My understanding is that validation doesn't re-open the child trees.

in @grom72 PR (#19205), i believe he updated his test in to go through a real yield and confirmed it fails without this patch and passes with it.

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.

Ok. I overlooked that iop_probe() doesn't use BTR_PROBE_EQ, so looks this vos_iter_validate() interface doesn't work at all from very beginning.

And you are right that if parent is deleted but iop_probe() returns 0, probing in a deleted subtree won't work.

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.

#19205 has been updated to use real yield and the test pass with this PR + fix from #19182 (comment)

@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Hardware Medium MD on SSD completed with status UNSTABLE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net/job/daos-stack/job/daos//view/change-requests/job/PR-19182/2/testReport/

Comment thread src/vos/vos_pool_scrub.c
Comment thread src/vos/vos_iterator.c Outdated
if (iter->it_type != VOS_ITER_OBJ && vos_obj_is_evicted(vos_iter2oiter(iter)->it_obj)) {
iter->it_anchors->ia_probe_level = VOS_ITER_OBJ;
return VOS_ITER_OBJ;
}

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.

The comparison between saved anchor and anchor below is sufficient. This fix could be removed, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok, ill remove

Comment thread src/vos/vos_iterator.c Outdated
/* Children are bound to the old entry's subtree; a different entry means it's gone */
if (rc == 0 &&
(iter->it_type == VOS_ITER_OBJ || iter->it_type == VOS_ITER_DKEY ||
iter->it_type == VOS_ITER_AKEY) &&

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.

Why the checking for VOS_ITER_SINGLE and VOS_ITER_RECX are skipped? They'll move to next when the anchor is deleted, right?

BTW, should we skip comparison when vos_anchor_is_zero(saved)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

there is nothing below them right? the crash happens as i understand only when a parent level moves. those are at the leaf so there are no child pointers left on a deleted subtree.
for recx, probe is a exact match find that returns DER_AGAIN if the extent is gone.

BTW, should we skip comparison when vos_anchor_is_zero(saved)?
yes good point

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.

there is nothing below them right? the crash happens as i understand only when a parent level moves. those are at the leaf so there are no child pointers left on a deleted subtree.
for recx, probe is a exact match find that returns DER_AGAIN if the extent is gone.

But the point of this vos_iter_validate_internal() is to return positive value when the EV or SV is deleted, returning 0 won't cause corruption but it's wrong, isn't it?

Yeah, I didn't realize scrubber doesn't use sorted iterator for recx. For evtree non-sorted iterator, that'll be an exact match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok, i guess returning 0 there is wrong even if it doesn't corrupt anything. will leave RECX as is since the unsorted evtree probe is exact.

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.

The interface vos_iter_validate() is a general interface supposed being used for by any kind of iterators (unsorted evtree iterator is used by scrubber only). We can get it landed as-is though, since it can solve the existing corruption issue.

mchaarawi and others added 2 commits October 20, 2026 13:58
Co-authored-by: Tomasz Gromadzki <tomasz.gromadzki@hpe.com>
Signed-off-by: Mohamad Chaarawi <mohamad.chaarawi@hpe.com>
Drop the object eviction check in vos_iter_validate_internal(). Objects
are evicted on every punch and DTX abort without their trees being
freed, so it forced needless restarts; the anchor comparison covers the
deleted-entry case.

Skip the anchor comparison when the saved anchor is zero, since a zero
anchor probes from the first entry and would always mismatch.

Features: checksum
Signed-off-by: Mohamad Chaarawi <mohamad.chaarawi@hpe.com>
@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Hardware Medium MD on SSD completed with status UNSTABLE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net/job/daos-stack/job/daos//view/change-requests/job/PR-19182/4/testReport/

The SV re-probe moves past a deleted entry and returns 0 without
updating the caller's anchor, so validation reported the value as
still present. Fetch the current SV anchor after the probe and compare
it like the key levels. Unsorted evtree probes are already exact.

Features: checksum
Signed-off-by: Mohamad Chaarawi <mohamad.chaarawi@hpe.com>
NiuYawei
NiuYawei previously approved these changes Oct 21, 2026
@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Hardware Medium MD on SSD completed with status UNSTABLE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net/job/daos-stack/job/daos//view/change-requests/job/PR-19182/5/testReport/

@mchaarawi
mchaarawi requested a review from Nasf-Fan October 22, 2026 14:04
@mchaarawi

Copy link
Copy Markdown
Contributor Author

the failed test is already known with ticket https://daosio.atlassian.net/browse/DAOS-19589

Signed-off-by: Tomasz Gromadzki <tomasz.gromadzki@hpe.com>
Add an injectable scheduler-sequence callback to standalone VOS TLS and
build the scrubber test with VOS_STANDALONE.

Add CSUM_SCRUBBING_15 test to delete and aggregate the current object during
a yield between checksum chunks, then run GC before iteration resumes.
Poison freed PMEM allocations through DAOS fault injection to expose
use-after-free, and verify the scrubber skips the deleted object and
continues processing later objects.

Signed-off-by: Tomasz Gromadzki <tomasz.gromadzki@hpe.com>
Include the affected OID in checksum corruption logs and the RAS event to
make it easier to identify the corrupted object.

skip-nlt: true
skip-test: true
skip-test-hardware: true
Test-tag: pr,-unittest

Priority: 2

Signed-off-by: Tomasz Gromadzki <tomasz.gromadzki@hpe.com>
Signed-off-by: Tomasz Gromadzki <tomasz.gromadzki@hpe.com>
@grom72

grom72 commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Based on the latest test version the fix does not work exactly as expected.
There is no assert, but scrubber skip the first object after the one just deleted.
For details see my comment in Jira ticket.

@daosbuild3

Copy link
Copy Markdown
Collaborator

@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Unit Test with memcheck completed with status UNSTABLE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net/job/daos-stack/job/daos//view/change-requests/job/PR-19182/6/testReport/

@daosbuild3

Copy link
Copy Markdown
Collaborator

When vos_iter_validate() finds the current entry gone, it sets
ia_probe_level and iteration unwinds to that level, which then
re-probed with PROBE_AGAIN in its current stage (e.g. POST). The GE
re-probe lands on the next entry, which was then treated as already
visited and skipped, so the scrubber missed the next object.

Re-probe that level from the FILTER stage instead, so the entry it
lands on is processed normally. CSUM_SCRUBBING_15 covers this.

Features: checksum
Signed-off-by: Mohamad Chaarawi <mohamad.chaarawi@hpe.com>
@mchaarawi

Copy link
Copy Markdown
Contributor Author

Based on the latest test version the fix does not work exactly as expected. There is no assert, but scrubber skip the first object after the one just deleted. For details see my comment in Jira ticket.

good find. fixed by re-probes as a fresh entry when validation reports that the entry is gone, so the next object is scrubbed normally

@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Hardware Medium Provider MD on SSD completed with status FAILURE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net/job/daos-stack/job/daos/job/PR-19182/6/display/redirect

@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Cluster Box Medium Provider MD on SSD completed with status FAILURE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net//job/daos-stack/job/daos/view/change-requests/job/PR-19182/6/execution/node/1706/log

@daosbuild3

Copy link
Copy Markdown
Collaborator

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants