Bug 77312 - Lambda that deletes itself accesses freed memory, but only if class is templated
Summary: Lambda that deletes itself accesses freed memory, but only if class is templated
Status: RESOLVED FIXED
Alias: None
Product: gcc
Classification: Unclassified
Component: c++ (show other bugs)
Version: 5.4.1
: P3 normal
Target Milestone: 8.0
Assignee: Not yet assigned to anyone
URL:
Keywords: wrong-code
: 77313 (view as bug list)
Depends on:
Blocks:
 
Reported: 2016-08-22 01:30 UTC by halliwell
Modified: 2021-08-23 09:52 UTC (History)
2 users (show)

See Also:
Host:
Target:
Build:
Known to work: 8.1.0
Known to fail: 7.5.0
Last reconfirmed: 2016-08-22 00:00:00


Attachments
Full preprocessor output using -save-temps when compiling (63.57 KB, text/plain)
2016-08-22 01:30 UTC, halliwell
Details

Note You need to log in before you can comment on or make changes to this bug.
Description halliwell 2016-08-22 01:30:02 UTC
Created attachment 39481 [details]
Full preprocessor output using -save-temps when compiling

Preprocessed .ii file is attached.  The bug is most easily reproduced using -fsanitize=address option (also running the program under valgrind).  I compiled with

g++ --std=c++11 -fsanitize=address -o test gcc-bug.cc

and then ran:
./test

Curiously, the bug only happens when class LambdaHolder is templated.  If you remove the template <class U> line and remove <int> from the instantiation line, then the bug is gone.


Output of gcc -v:
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.8/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.8.4-2ubuntu1~14.04.3' --with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.8 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.8 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-libmudflap --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.8-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.3)
Comment 1 Andreas Schwab 2016-08-22 07:00:39 UTC
*** Bug 77313 has been marked as a duplicate of this bug. ***
Comment 2 Andrew Pinski 2016-08-22 07:10:00 UTC
I don't see why you think this is valid thing to do.
You are in a function which calls delete on the object you are in currently.  That seems like this code is broken.
Comment 3 Jonathan Wakely 2016-08-22 09:54:26 UTC
(In reply to Andrew Pinski from comment #2)
> I don't see why you think this is valid thing to do.
> You are in a function which calls delete on the object you are in currently.
> That seems like this code is broken.

It's valid as long as nothing accesses any members of the class after it is destroyed. When using code you don't control, such as std::function and a lambda closure, you can't guarantee what it does or doesn't do with the memory.
Comment 4 Jonathan Wakely 2016-08-22 10:07:47 UTC
Reduced:

struct function_impl_base {
  virtual ~function_impl_base() = default;
  virtual void invoke() = 0;
};

template<typename F>
struct function_impl : function_impl_base {
  function_impl(F f) : f(f) { }
  void invoke() { f(); }
  F f;
};

struct function
{
  template<typename F>
    function(F f) : impl(new function_impl<F>{f}) { }
  ~function() { delete impl; }
  function(const function&) = delete;
  function_impl_base* impl;
  void operator()() { impl->invoke(); }
};

template <class U>
struct LambdaHolder {
    LambdaHolder() : lambda_{[this]() { delete this; }} {
    }
    void Run() {
        lambda_();
    }
    function lambda_;
};

int main() {
    LambdaHolder<int>* l = new LambdaHolder<int>();
    l->Run();
}


There's no reason this should need to access the freed memory. Clang and EDG don't.
Comment 5 Andrew Pinski 2016-08-22 16:30:47 UTC
It all comes down to this statement:
delete this;

The C++ front-end expands it as:

  [t33.cc:28:39] D.3592 = [t33.cc:28:39] __closure->__this;
  [t33.cc:28:41] LambdaHolder<int>::~LambdaHolder (D.3592);
  [t33.cc:28:39] D.3592 = [t33.cc:28:39] __closure->__this;
  [t33.cc:28:41] operator delete (D.3592, 360);


Notice how it reads from __closure here but __closure is deleted during the call to ~LambdaHolder.

So the question comes what is the semantics of:
delete XYZ;

Inside a lambda where XYZ is captured?  Do we need to read from the closure twice, once for the deconstructor and then again for the call to delete.
Comment 6 Jonathan Wakely 2016-08-22 17:09:20 UTC
(In reply to Andrew Pinski from comment #5)
> Inside a lambda where XYZ is captured?  Do we need to read from the closure
> twice, once for the deconstructor and then again for the call to delete.

I don't see why that should be necessary.
Comment 7 Andrew Pinski 2021-08-22 20:10:12 UTC
This is fixed in GCC 8:

  if (SAVE_EXPR <(struct LambdaHolder *) this> != 0B)
    {
      try
        {
          LambdaHolder<int>::~LambdaHolder (SAVE_EXPR <(struct LambdaHolder *) this>);
        }
      finally
        {
          operator delete ((void *) SAVE_EXPR <(struct LambdaHolder *) this>, 8);
        }
    }
  else
    {
      <<< Unknown tree: void_cst >>>
    } >>>>>;
Comment 8 Andrew Pinski 2021-08-22 20:24:21 UTC
Fixed.
Comment 9 Jonathan Wakely 2021-08-23 09:52:40 UTC
Fixed by r251433 "Reimplement handling of lambdas in templates."