Description
Describe the bug
Then changing the Address of a Branch in TTree it seems to sometimes still use the old memory address after the call. As a minimal example here we create a TNamed, set it as branch but directly after delete it and set the branch address to nullptr
before filling:
#include <TTree.h>
#include <TFile.h>
#include <iostream>
int main() {
auto* tf = TFile::Open("test.root", "RECREATE");
auto* t = new TTree("tree", "tree");
TNamed *f = new TNamed("foo", "bar");
auto* b = t->Branch("FileMetaData", &f);
delete f;
f = nullptr;
b->SetAddress(nullptr);
t->Fill();
t->Write();
tf->Close();
return 0;
}
When running this with address sanitizer it gives the following error with 6.24:
g++ -g `root-config --cflags --libs` -fsanitize=address -o mwe mwe.C && ./mwe
=================================================================
==934566==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000112a68 at pc 0x7fcd23e920f0 bp 0x7ffcc8579ab0 sp 0x7ffcc8579aa8
READ of size 4 at 0x606000112a68 thread T0
#0 0x7fcd23e920ef in int TStreamerInfoActions::WriteBasicType<unsigned int>(TBuffer&, void*, TStreamerInfoActions::TConfiguration const*) root/io/io/src/TStreamerInfoActions.cxx:252
#1 0x7fcd239c8950 in TStreamerInfoActions::TConfiguredAction::operator()(TBuffer&, void*) const root/io/io/inc/TStreamerInfoActions.h:123
#2 0x7fcd239c8950 in TBufferFile::ApplySequence(TStreamerInfoActions::TActionSequence const&, void*) root/io/io/src/TBufferFile.cxx:3572
#3 0x7fcd21fdd5dc in TBranch::FillImpl(ROOT::Internal::TBranchIMTHelper*) root/tree/tree/src/TBranch.cxx:891
#4 0x7fcd22016bfa in TBranchElement::FillImpl(ROOT::Internal::TBranchIMTHelper*) root/tree/tree/src/TBranchElement.cxx:1265
#5 0x7fcd22016441 in TBranchElement::FillImpl(ROOT::Internal::TBranchIMTHelper*) root/tree/tree/src/TBranchElement.cxx:1290
#6 0x7fcd22016441 in TBranchElement::FillImpl(ROOT::Internal::TBranchIMTHelper*) root/tree/tree/src/TBranchElement.cxx:1290
#7 0x7fcd221d81a7 in TTree::Fill() root/tree/tree/src/TTree.cxx:4586
#8 0x4025f7 in main mwe.C:17
#9 0x7fcd1f9b3bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)
#10 0x402239 in _start (mwe+0x402239)
0x606000112a68 is located 8 bytes inside of 64-byte region [0x606000112a60,0x606000112aa0)
freed by thread T0 here:
#0 0x7fcd250835c7 in operator delete(void*) ([...]/lib/libasan.so.6+0xae5c7)
#1 0x402501 in main mwe.C:13
#2 0x7fcd1f9b3bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)
previously allocated by thread T0 here:
#0 0x7fcd25082bf7 in operator new(unsigned long) ([...]/lib/libasan.so.6+0xadbf7)
#1 0x7fcd247f9b81 in TStorage::ObjectAlloc(unsigned long) root/core/base/src/TStorage.cxx:330
#2 0x402948 in TObject::operator new(unsigned long) [...]/root/include/TObject.h:167
#3 0x40241e in main mwe.C:10
#4 0x7fcd1f9b3bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)
SUMMARY: AddressSanitizer: heap-use-after-free root/io/io/src/TStreamerInfoActions.cxx:252 in int TStreamerInfoActions::WriteBasicType<unsigned int>(TBuffer&, void*, TStreamerInfoActions::TConfiguration const*)
Shadow bytes around the buggy address:
0x0c0c8001a4f0: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fd
0x0c0c8001a500: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
0x0c0c8001a510: fd fd fd fd fd fd fd fd fa fa fa fa fd fd fd fd
0x0c0c8001a520: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fa
0x0c0c8001a530: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa
=>0x0c0c8001a540: fd fd fd fd fd fd fd fa fa fa fa fa fd[fd]fd fd
0x0c0c8001a550: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fd
0x0c0c8001a560: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
0x0c0c8001a570: fd fd fd fd fd fd fd fd fa fa fa fa 00 00 00 00
0x0c0c8001a580: 00 00 00 00 fa fa fa fa fd fd fd fd fd fd fd fd
0x0c0c8001a590: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==934566==ABORTING
Expected behavior
Expected behavior is for ROOT to realize the branch address has been changed to nullptr and create an internal buffer, not read from the freed memory area.
Interestingly it doesn't seem to have a problem without calling SetAddress
as it then realizes it points to nullptr and will create a new object and modify f
To Reproduce
Run the above with ROOT compiled with -Dasan=ON
Setup
- ROOT 6.24.0
- GCC 10.2
- Ubuntu 18.04
Additional context
We didn't see this with ROOT 6.20 but I also did not specifically test it with address sanitizer so it might have been there already and just not cause any problems.