fr00b0/nod

Signals created via move constructor/assignment use the moved from object

dummyunit opened this issue · 1 comments

Move support for signal_type added in #19 has a major flaw - the signal created by a move constructor/assignment keeps a pointer (_shared_disconnector) to _disconnector stored inside the moved from object.

If the original object is then deleted, any operation that uses the disconnector (another move, connection::disconnect) will try to access a freed memory and will most likely crash.
This can be easily detected by AddressSanitizer using a code like this:

#include "nod.hpp"
#include <memory>
int main()
{
	auto sig1 = std::make_unique<nod::signal<void ()>>();
	auto conn = sig1->connect([]{}); // initializes `_shared_disconnector`
	auto sig2 = std::move(*sig1);
	sig1.reset();
	// Any of the following lines tries to access memory of the deleted *sig1 object:
	conn.disconnect();
	auto sig3 = std::move(sig2);
}

Compiling (g++ -g -O1 -fsanitize=address nod_move_fail.cpp) and running this code with ASan results in this report:

==12348==ERROR: AddressSanitizer: heap-use-after-free on address 0x60b000000138 at pc 0x557ffecfb967 bp 0x7fff01c98960 sp 0x7fff01c98950
READ of size 8 at 0x60b000000138 thread T0                                                                                                                                                                                                                                                
    #0 0x557ffecfb966 in nod::connection::disconnect() /tmp/nod.hpp:657
    #1 0x557ffecfa32c in main /tmp/nod_move_fail.cpp:10
    #2 0x7f0b36963e9d in __libc_start_main (/lib64/libc.so.6+0x23e9d)
    #3 0x557ffecf9269 in _start (/tmp/a.out+0x2269)

0x60b000000138 is located 72 bytes inside of 104-byte region [0x60b0000000f0,0x60b000000158)
freed by thread T0 here:                                                                                                                                                                                                                                                                  
    #0 0x7f0b36f1ba87 in operator delete(void*, unsigned long) (/usr/lib/gcc/x86_64-pc-linux-gnu/11.1.0/libasan.so.6+0xb3a87)
    #1 0x557ffecfa320 in std::default_delete<nod::signal_type<nod::multithread_policy, void ()> >::operator()(nod::signal_type<nod::multithread_policy, void ()>*) const /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include/g++-v10/bits/unique_ptr.h:85
    #2 0x557ffecfa320 in std::__uniq_ptr_impl<nod::signal_type<nod::multithread_policy, void ()>, std::default_delete<nod::signal_type<nod::multithread_policy, void ()> > >::reset(nod::signal_type<nod::multithread_policy, void ()>*) /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include/g++-v10/bits/unique_ptr.h:182
    #3 0x557ffecfa320 in std::unique_ptr<nod::signal_type<nod::multithread_policy, void ()>, std::default_delete<nod::signal_type<nod::multithread_policy, void ()> > >::reset(nod::signal_type<nod::multithread_policy, void ()>*) /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include/g++-v10/bits/unique_ptr.h:456
    #4 0x557ffecfa320 in main /tmp/nod_move_fail.cpp:8

previously allocated by thread T0 here:
    #0 0x7f0b36f1aa97 in operator new(unsigned long) (/usr/lib/gcc/x86_64-pc-linux-gnu/11.1.0/libasan.so.6+0xb2a97)
    #1 0x557ffecf94b4 in std::_MakeUniq<nod::signal_type<nod::multithread_policy, void ()> >::__single_object std::make_unique<nod::signal_type<nod::multithread_policy, void ()>>() /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include/g++-v10/bits/unique_ptr.h:962
    #2 0x557ffecf94b4 in main /tmp/nod_move_fail.cpp:5

SUMMARY: AddressSanitizer: heap-use-after-free /tmp/nod.hpp:657 in nod::connection::disconnect()
Shadow bytes around the buggy address:
  0x0c167fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c167fff8000: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x0c167fff8010: fd fd fd fd fd fa fa fa fa fa fa fa fa fa fd fd
=>0x0c167fff8020: fd fd fd fd fd fd fd[fd]fd fd fd fa fa fa fa fa
  0x0c167fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c167fff8070: fa fa fa fa fa fa fa fa fa fa fa fa 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

I don't know how this can be fixed without creating the disconnector on the heap.

Thank you for this issue report.
I am aware of a few thread safety issues at this time, but I currently have not figured out any elegant solutions. I'll keep the issue open as a reminder to what I do need to fix eventually.