Skip to content

Multi-threaded RDataFrame::Snapshot writes unreadable baskets (heap-use-after-free in TBasket::CopyTo) #23550

Open

Description

@theoreticMK

Check duplicate issues.

  • Checked for duplicates

Description

Recently, after updating to the 6.40.05 version of the ROOT from version 6.38, I started getting the following type of error while reading (of some) trees created by Snapshot. I am compiling root from source, using branch v6-40-00-patches (pulled and recompiled today).

Error in TBasket::Streamer: The value of fNbytes is incorrect (-235009601) ; trying to recover by setting it to zero
Error in TBranch::GetBasket: File: filter_kpiee_tree_standard.root at byte:3214918, branch:acc_em_chod, entry:2, badread=1, nerrors=1, basketnumber=2
Error in TTreeFormula::TTreeFormula::TT_EVAL_INIT: Could not read entry (2) of leaf (acc_em_chod), r=(-1).

I was not able to find the source of the problem, but I tried today to give the problem to Claude, and it seems to be a ROOT bug. I attach here the issue report generated by Claude and a short reproducer. The issue appears only with ROOT::EnableImplicitMT (number of threads does not matter). The point is that the number of entries needs to be small and the tree name needs to be long enough to trigger the bug.

ISSUE.md

Reproducer

// Multi-threaded RDataFrame::Snapshot writes baskets that cannot be read back.
// Seen with ROOT 6.40.05 on Linux x86_64 (Debian 13, GCC 14.2).
//
// Run with: root -l -b -q snapshot_mt_basket_corruption.C
//
// Expected: all 10 entries of the snapshot can be read back.
// Observed: several entries fail with "TBasket::Streamer: The value of fNbytes is incorrect".
//   - same result with ROOT::EnableImplicitMT(1)
//   - no errors without implicit multi-threading
//   - no errors with tree names of up to ~50 characters

void snapshot_mt_basket_corruption()
{
   ROOT::EnableImplicitMT(4);

   const std::string treeName(60, 'n');
   const char *fileName = "snapshot_mt_basket_corruption.root";
   ROOT::RDataFrame(1000000)
      .Define("b", "rdfentry_ % 2 == 0")
      .Filter("rdfentry_ % 100000 == 7") // 10 entries, each processed by a different task
      .Snapshot(treeName, fileName, {"b"});

   TFile f(fileName);
   auto tree = f.Get<TTree>(treeName.c_str());
   int unreadable = 0;
   for (Long64_t i = 0; i < tree->GetEntries(); i++)
      if (tree->GetEntry(i) < 0)
         unreadable++;
   std::cout << unreadable << " of " << tree->GetEntries() << " entries cannot be read back\n";
}

ROOT version


| Welcome to ROOT 6.40.05 https://root.cern |
| (c) 1995-2025, The ROOT Team; conception: R. Brun, F. Rademakers |
| Built for linuxx8664gcc on Sep 30 2026, 11:31:44 |
| From heads/v6-40-00-patches@v6-40-04-58-g05353987903 |
| With c++ (Debian 14.2.0-19) 14.2.0 std202302 |
| Try '.help'/'.?', '.demo', '.license', '.credits', '.quit'/'.q' |

Installation method

build from source

Operating system

Linux Debian

Additional context

No response

Activity

  1. Elvand-Lie commented on Oct 12, 2026

    @Elvand-Lie
    Contributor

    Hi @vepadulano, I had a look at this and I think the cause is in the TTree I/O layer rather than in RDataFrame. When the merger fast-copies a small basket, TBasket::LoadBasketBuffers gives it a buffer sized for reading, and CopyTo then writes the key header into it. If the header doesn't fit, the buffer is reallocated and the old memory is what ends up in the file. It became visible with 619d396, which is also on the 6.40 patch branch and was backported to 6.38 as 62c0ddf.

    I have a small fix with a regression test (tested on Linux, not with RDataFrame itself) and I would like to open a PR if possible. Thanks.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions