On Fri, 20 Dec 2024, Patrick Palka wrote:

> On Fri, 20 Dec 2024, Giuseppe D'Angelo wrote:
> 
> > Hello,
> > 
> > On 20/12/2024 13:23, Giuseppe D'Angelo wrote:
> > > Hi,
> > > 
> > > The implementation of ranges::is_permutation may create a dangling
> > > reference, which then results (sometimes) in a crash. A minimal example
> > > that shows the problem under ASAN is https://gcc.godbolt.org/z/7bP9nE8fK
> 
> D'oh, good catch..
> 
> > > 
> > > The attached patch fixes it. I've tested on Linux x86-64. Adding a
> > > negative test for this is somehow challenging (how do you test you're
> > > not using a dangling reference?), but running the modified test under
> > > ASAN shows the fix in place.
> 
> I'd expect a constexpr version of the test to reliably fail as soon as
> it encounters the UB.
> 
> > > 
> > > Do you need me to create a report on bugzilla and cross-reference it
> > > from the patch?
> 
> That'd be good since we'll probably want to backport the fix to the
> release branches and the PR could reflect that.
> 
> > Better patch attached.
> > 
> > Thanks,
> > -- 
> > Giuseppe D'Angelo
> > 
> 
> > Subject: [PATCH] libstdc++: fix a dangling reference crash in
> >  ranges::is_permutation
> > 
> > The code was caching the result of `invoke(proj, *it)` in a local
> > `auto &&` variable. The problem is that this may create dangling
> > references, for instance in case `proj` is `std::identity` (the common
> > case) and `*it` produces a prvalue.
> > 
> > Rather than finding a "creative" solution (e.g. use the value category
> > of `*it`, and the return type of calling the projection on that, to
> > determine whether we can keep a reference or we need a value), get rid
> > of the caching and call `invoke` as needed. In the common case the
> > projection is cheap, and we are allowed to dereference the same iterator
> > more than once (they're forward). This also sounds more correct because
> > we pass the correct value category (obtained from applying the
> > projection to the iterator) to the comparator.
> 
> Makes sense, but it might be preferable for sake of QoI to fix this
> without introducing extra dereferences or projection applications
> if possible.
> 
> auto&& is supposed to perform lifetime extension, but that only happens
> for an outermost temporary and not any temporaries within a
> subexpression IIUC.  So how about if we use a second auto&& for the
> *__scan subexpression so that lifetime extension occurs there?
> 
> > 
> > libstdc++-v3/ChangeLog:
> > 
> >     * include/bits/ranges_algo.h (__is_permutation_fn): Do not cache
> >     the projection result in a local variable, as that may create
> >     dangling references.
> >     * testsuite/25_algorithms/is_permutation/constined.cc: Add a
> >     test with a range returning prvalues.
> > 
> > Signed-off-by: Giuseppe D'Angelo <giuseppe.dang...@kdab.com>
> > ---
> >  libstdc++-v3/include/bits/ranges_algo.h               |  3 +--
> >  .../25_algorithms/is_permutation/constrained.cc       | 11 +++++++++++
> >  2 files changed, 12 insertions(+), 2 deletions(-)
> > 
> > diff --git a/libstdc++-v3/include/bits/ranges_algo.h 
> > b/libstdc++-v3/include/bits/ranges_algo.h
> > index 772bf4dd997..4d3c4325e2c 100644
> > --- a/libstdc++-v3/include/bits/ranges_algo.h
> > +++ b/libstdc++-v3/include/bits/ranges_algo.h
> > @@ -567,9 +567,8 @@ namespace ranges
> >  
> >     for (auto __scan = __first1; __scan != __last1; ++__scan)
> >       {
> > -       auto&& __proj_scan = std::__invoke(__proj1, *__scan);
> >         auto __comp_scan = [&] <typename _Tp> (_Tp&& __arg) -> bool {
> > -         return std::__invoke(__pred, __proj_scan,
> 
> If we go with the second auto&& approach then we should perfect forward
> __proj_scan here as you alluded to.  That might seem unsafe at first
> glance (if say the project returns an rvalue) but since the predicate is
> regular_invocable it mustn't modify its arguments IIUC.

N.B. that we currently don't perfect forward __proj_scan here is a
minor bug separate from the dangling reference bug.

The predicate's constraints permit invocability with
iter_reference_t<I1> not necessarily with iter_reference_t<I1>&,
but here we're currently invoking it with the latter.

> 
> > +         return std::__invoke(__pred, std::__invoke(__proj1, *__scan),
> >                                std::forward<_Tp>(__arg));
> >         };
> >         if (__scan != ranges::find_if(__first1, __scan,
> > diff --git 
> > a/libstdc++-v3/testsuite/25_algorithms/is_permutation/constrained.cc 
> > b/libstdc++-v3/testsuite/25_algorithms/is_permutation/constrained.cc
> > index 2fbebe37609..4981d2c07ce 100644
> > --- a/libstdc++-v3/testsuite/25_algorithms/is_permutation/constrained.cc
> > +++ b/libstdc++-v3/testsuite/25_algorithms/is_permutation/constrained.cc
> > @@ -19,6 +19,7 @@
> >  
> >  #include <algorithm>
> >  #include <iterator>
> > +#include <ranges>
> >  #include <testsuite_hooks.h>
> >  #include <testsuite_iterators.h>
> >  
> > @@ -76,10 +77,20 @@ test03()
> >    while (std::next_permutation(std::begin(cx), std::end(cx)));
> >  }
> >  
> > +void
> > +test04()
> > +{
> > +  int x[] = { 4, 3, 2, 1 };
> > +  auto y = std::views::iota(1, 5);
> > +  VERIFY( ranges::is_permutation(x, y) );
> > +  VERIFY( ranges::is_permutation(y, x) );
> > +}
> > +
> >  int
> >  main()
> >  {
> >    test01();
> >    test02();
> >    test03();
> > +  test04();
> >  }
> > -- 
> > 2.34.1
> > 
> 

Reply via email to