Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible use-after-move in join() #4231

Closed
mattgodbolt opened this issue Nov 8, 2024 · 5 comments · Fixed by #4236
Closed

Possible use-after-move in join() #4231

mattgodbolt opened this issue Nov 8, 2024 · 5 comments · Fixed by #4236

Comments

@mattgodbolt
Copy link

mattgodbolt commented Nov 8, 2024

The code at:

fmt/include/fmt/ranges.h

Lines 661 to 666 in 720da57

auto format(view_ref& value, FormatContext& ctx) const
-> decltype(ctx.out()) {
auto it = std::forward<view_ref>(value).begin;
auto out = ctx.out();
if (it == value.end) return out;
out = value_formatter_.format(*it, ctx);

Does a

auto it = std::forward<view_ref>(value).begin;

and later accesses value.end and value.sep.

In the case the iterator is not copy constructible then the view_ref is an rvalue-reference:

  using view_ref = conditional_t<std::is_copy_constructible<It>::value,
                                 const join_view<It, Sentinel, Char>&,
                                 join_view<It, Sentinel, Char>&&>;

which effectively turns the forward<> into a move.

clang-tidy warns on the accesses of end and sep as they are accesses on a moved-from object.

I'm not sure that this is really a problem in practice but it seems like something that should be avoided. I can't see an easy way to do so though. I'm not sure we can cast just the iterator easily? Taking a copy of sep works but for the same reasons as above we can't copy end out without a move either.

@mattgodbolt
Copy link
Author

mattgodbolt commented Nov 8, 2024

I'm a bit out of my depth but maybe something like:

    auto value_copy = std::forward<view_ref>(value);
    auto it = std::move(value_copy.begin);
    // rest of the code uses value_copy

though this forces a copy even in the good case.

@mattgodbolt
Copy link
Author

@Arghnews
Copy link
Contributor

Arghnews commented Nov 9, 2024

I had a look at this.

I believe this specialization is only used for exactly fmt:join_views.

If you try and pass an lvalue fmt::join_view, you hit a static assert saying we can only pass rvalue views (https://compiler-explorer.com/z/9jjPodzzo)

So now we can be sure that this code path only deals with receiving fmt::joins that are rvalues.

This format function is called from here, line 2224:

fmt/include/fmt/base.h

Lines 2214 to 2225 in 720da57

// Formats an argument of a custom type, such as a user-defined class.
template <typename T, typename Formatter>
static void format_custom(void* arg, parse_context<char_type>& parse_ctx,
Context& ctx) {
auto f = Formatter();
parse_ctx.advance_to(f.parse(parse_ctx));
using qualified_type =
conditional_t<has_formatter<const T, char_type>(), const T, T>;
// format must be const for compatibility with std::format and compilation.
const auto& cf = f;
ctx.advance_to(cf.format(*static_cast<qualified_type*>(arg), ctx));
}

qualified_type on line 2220 in that block is always non const (for a fmt::join_view). See:
https://compiler-explorer.com/z/cK3h3EzeP

This means that line 2224's first argument passed to format is:
*static_cast<qualified_type*>(arg) which will always deduce to fmt::join_view&

 

A minimal fix is to change fmt::join_view::format to only take lvalues, since as it stands, that's all it will ever receive, and change it to it&:

  template <typename FormatContext>
  auto format(join_view<It, Sentinel, Char>& value, FormatContext& ctx) const
      -> decltype(ctx.out()) {
    auto& it = value.begin;

This also saves a copy/move since we now take it by reference. The reference being into the original rvalue argument that is passed in the println call.

 

Note this code was originally put in in 10508a30ecd91e5d09a27e4c6c0a01a89fd4edc7 and changed to std::forward in d2473b7b73c0af2a3ed34c99e50ace0a1040581a

If I've missed anything or my assumptions or wrong, please point it out

 

I can put in a PR for this if vitaut agrees this is a good change to make
@vitaut

@vitaut
Copy link
Contributor

vitaut commented Nov 10, 2024

Thanks @mattgodbolt for reporting and @Arghnews for investigating the issue.

A minimal fix is to change fmt::join_view::format to only take lvalues, since as it stands, that's all it will ever receive, and change it to it&

I think this is a correct fix.

I can put in a PR for this if vitaut agrees this is a good change to make

Please do.

@mattgodbolt
Copy link
Author

Thank you both!

Arghnews added a commit to Arghnews/fmt that referenced this issue Nov 12, 2024
Should fix fmtlib#4231
Could instead use:
<!std::is_copy_constructible_v<It>, It&, It>;
Arghnews added a commit to Arghnews/fmt that referenced this issue Nov 14, 2024
vitaut pushed a commit that referenced this issue Nov 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants