On Fri, May 2, 2025 at 1:12 PM Jonathan Wakely <jwak...@redhat.com> wrote:

> When concatenating a path we reallocate the left operand's storage to
> make room for the new components being added. When the two operands are
> the same object, or the right operand is one of the components of the
> left operand, the reallocation invalidates the pointers that refer
> into the right operand's storage.
>
> The solution in this commit is to detect these aliasing cases and just
> do the concatenation in terms of the contained string, as that code
> already handles the case where the string aliases the path. The standard
> specifies the concatenation in terms of the native() string, so all this
> change does is disable the optimized implementation of concatenation for
> path objects which attempts to avoid re-parsing the path from the
> concatenated string.
>
> The potential loss of performance for this case isn't likely to be an
> issue, because concatenating a path with itself (or one of its existing
> components) probably isn't a common use case.
>
> The Filesystem TS implementation doesn't have the optimized form of
> concatenation and always does it in terms of the native string and
> reparsing the whole thing, so doesn't have this bug. A test is added to
> confirm that anyway (that test has some slightly different results due
> to different behaviour for trailing slashes and implicit "." filenames
> in the TS spec).
>
> libstdc++-v3/ChangeLog:
>
>         PR libstdc++/120029
>         * src/c++17/fs_path.cc (path::operator+=(const path&)): Handle
>         parameters that alias the path or one of its components.
>         * testsuite/27_io/filesystem/path/concat/120029.cc: New test.
>         * testsuite/experimental/filesystem/path/concat/120029.cc: New
>         test.
> ---
>
> Tested x86_64-linux.
>
>  libstdc++-v3/src/c++17/fs_path.cc             | 10 +++
>  .../27_io/filesystem/path/concat/120029.cc    | 72 ++++++++++++++++++
>  .../filesystem/path/concat/120029.cc          | 74 +++++++++++++++++++
>  3 files changed, 156 insertions(+)
>  create mode 100644
> libstdc++-v3/testsuite/27_io/filesystem/path/concat/120029.cc
>  create mode 100644
> libstdc++-v3/testsuite/experimental/filesystem/path/concat/120029.cc
>
> diff --git a/libstdc++-v3/src/c++17/fs_path.cc
> b/libstdc++-v3/src/c++17/fs_path.cc
> index 6582f10209a..cae16ed607b 100644
> --- a/libstdc++-v3/src/c++17/fs_path.cc
> +++ b/libstdc++-v3/src/c++17/fs_path.cc
> @@ -880,6 +880,16 @@ path::operator+=(const path& p)
>        return *this;
>      }
>
> +  // Handle p += p which would otherwise access dangling pointers after
> +  // reallocating _M_cmpts and _M_pathname.
> +  if (&p == this) [[unlikely]]
> +    return *this += p.native();
> +  // Handle p += *i where i is in [p.begin(),p.end()), for the same
> reason.
> +  if (_M_type() == _Type::_Multi && p._M_type() != _Type::_Multi)
> +    for (const path& cmpt : *this)
> +      if (&cmpt == &p)
>
Do we want [[unlikely]] also here. Or place these two checks in the
separate function, and then do:
if (_M_overlap(_&p)) [[unlikely]]
  return *this += p.native();

> +       return *this += p.native();
> +
>  #if _GLIBCXX_FILESYSTEM_IS_WINDOWS
>    if (_M_type() == _Type::_Root_name
>        || (_M_type() == _Type::_Filename && _M_pathname.size() == 1))
> diff --git a/libstdc++-v3/testsuite/27_io/filesystem/path/concat/120029.cc
> b/libstdc++-v3/testsuite/27_io/filesystem/path/concat/120029.cc
> new file mode 100644
> index 00000000000..5153d594b50
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/27_io/filesystem/path/concat/120029.cc
> @@ -0,0 +1,72 @@
> +// { dg-do run { target c++17 } }
> +
> +// Bug libstdc++/120029
> +// Dangling iterator usage in path::operator+=(const path& p) when this
> == p
> +
> +#include <filesystem>
> +#include <testsuite_hooks.h>
> +
> +namespace fs = std::filesystem;
> +
> +void
> +test_root_dir()
> +{
> +  fs::path p = "/";
> +  p += p;
> +  p += p;
> +  VERIFY( p == "////" );
> +  p += p.filename();
> +  VERIFY( p == "////" );
> +  p += *std::prev(p.end());
> +  VERIFY( p == "////" );
> +}
> +
> +void
> +test_root_name()
> +{
> +  fs::path p = "C:/";
> +  p += p;
> +  p += p;
> +  VERIFY( p == "C:/C:/C:/C:/" );
> +  p += p.filename();
> +  VERIFY( p == "C:/C:/C:/C:/" );
> +  p += *std::prev(p.end());
> +  VERIFY( p == "C:/C:/C:/C:/" );
> +}
> +
> +void
> +test_filename()
> +{
> +  fs::path p = "file";
> +  p += p;
> +  p += p;
> +  VERIFY( p == "filefilefilefile" );
> +  p += p.filename();
> +  VERIFY( p == "filefilefilefilefilefilefilefile" );
> +  p += *std::prev(p.end());
> +  VERIFY( p ==
> "filefilefilefilefilefilefilefilefilefilefilefilefilefilefilefile" );
> +}
> +
> +void
> +test_multi()
> +{
> +  fs::path p = "/home/username/Documents/mu";
> +  p += p;
> +  p += p;
> +  VERIFY( p ==
> "/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mu"
> );
> +  p += p.filename();
> +  VERIFY( p ==
> "/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mumu"
> );
> +  p += *std::prev(p.end());
> +  VERIFY( p ==
> "/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mumumumu"
> );
> +  auto n = std::distance(p.begin(), p.end());
> +  for (int i = 0; i < n; ++i)
> +    p += *std::next(p.begin(), i);
> +}
> +
> +int main()
> +{
> +  test_root_dir();
> +  test_root_name();
> +  test_filename();
> +  test_multi();
> +}
> diff --git
> a/libstdc++-v3/testsuite/experimental/filesystem/path/concat/120029.cc
> b/libstdc++-v3/testsuite/experimental/filesystem/path/concat/120029.cc
> new file mode 100644
> index 00000000000..209d96806c6
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/experimental/filesystem/path/concat/120029.cc
> @@ -0,0 +1,74 @@
> +// { dg-options "-DUSE_FILESYSTEM_TS -lstdc++fs" }
> +// { dg-do run { target c++11 } }
> +// { dg-require-filesystem-ts "" }
> +
> +// Bug libstdc++/120029
> +// Dangling iterator usage in path::operator+=(const path& p) when this
> == p
> +
> +#include <experimental/filesystem>
> +#include <testsuite_hooks.h>
> +
> +namespace fs = std::experimental::filesystem;
> +
> +void
> +test_root_dir()
> +{
> +  fs::path p = "/";
> +  p += p;
> +  p += p;
> +  VERIFY( p == "////" );
> +  p += p.filename();
> +  VERIFY( p == "////////" );
> +  p += *std::prev(p.end());
> +  VERIFY( p == "////////////////" );
> +}
> +
> +void
> +test_root_name()
> +{
> +  fs::path p = "C:/";
> +  p += p;
> +  p += p;
> +  VERIFY( p == "C:/C:/C:/C:/" );
> +  p += p.filename(); // For Filesystem TS the filename is "."
> +  VERIFY( p == "C:/C:/C:/C:/." );
> +  p += *std::prev(p.end());
> +  VERIFY( p == "C:/C:/C:/C:/.." );
> +}
> +
> +void
> +test_filename()
> +{
> +  fs::path p = "file";
> +  p += p;
> +  p += p;
> +  VERIFY( p == "filefilefilefile" );
> +  p += p.filename();
> +  VERIFY( p == "filefilefilefilefilefilefilefile" );
> +  p += *std::prev(p.end());
> +  VERIFY( p ==
> "filefilefilefilefilefilefilefilefilefilefilefilefilefilefilefile" );
> +}
> +
> +void
> +test_multi()
> +{
> +  fs::path p = "/home/username/Documents/mu";
> +  p += p;
> +  p += p;
> +  VERIFY( p ==
> "/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mu"
> );
> +  p += p.filename();
> +  VERIFY( p ==
> "/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mumu"
> );
> +  p += *std::prev(p.end());
> +  VERIFY( p ==
> "/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mu/home/username/Documents/mumumumu"
> );
> +  auto n = std::distance(p.begin(), p.end());
> +  for (int i = 0; i < n; ++i)
> +    p += *std::next(p.begin(), i);
> +}
> +
> +int main()
> +{
> +  test_root_dir();
> +  test_root_name();
> +  test_filename();
> +  test_multi();
> +}
> --
> 2.49.0
>
>

Reply via email to