diff mbox series

c++, coroutines: Instrument missing return_void UB.

Message ID 20240901161740.73814-1-iain@sandoe.co.uk
State New
Headers show
Series c++, coroutines: Instrument missing return_void UB. | expand

Commit Message

Iain Sandoe Sept. 1, 2024, 4:17 p.m. UTC
This came up in discussion of an earlier patch.

I'm in two minds as to whether it's a good idea or not - the underlying
issue being that libubsan does not yet (AFAICT) have the concept of a
coroutine, so that the diagnostics are not very specific and might appear
strange (i.e. "execution reached the end of a value-returning function
without returning a value" which is a bit of an odd diagnostic for
a missing return_void ()).

OTOH one might argue that some diagnostic is better than silent UB .. but
I do not have cycles to address improving this in upstream libsanitizer ...

The logic used here is intended to match cp_maybe_instrument_return ()
although it's not 100% clear that that is doing exactly what the comment
says - since it does not distinguish between -fno-sanitize=return and
the case that the user simply did not specify it.  So that
-fsanitize=unreachable is ignored for both fno-sanitize=return and the
unset case.

tested on x86_64-darwin and powerpc64le-linux, 
apply to trunk? Opinions?
thanks
Iain

--- 8< ---

[dcl.fct.def.coroutine] / 6 Note 1:
"If return_void is found, flowing off the end of a coroutine is equivalent
to a co_return with no operand. Otherwise, flowing off the end of a
coroutine results in undefined behavior."

Here we implement this as a check for sanitized returns and call the ubsan
instrumentation; if that is not enabled we mark this as unreachable (which
might trap depending on the target settings).

gcc/cp/ChangeLog:

	* coroutines.cc
	(cp_coroutine_transform::wrap_original_function_body): Instrument
	the case where control flows off the end of a coroutine and the
	user promise has no return_void entry.

Signed-off-by: Iain Sandoe <iain@sandoe.co.uk>
---
 gcc/cp/coroutines.cc | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

Comments

Jason Merrill Sept. 4, 2024, 4:21 p.m. UTC | #1
On 9/1/24 12:17 PM, Iain Sandoe wrote:
> This came up in discussion of an earlier patch.
> 
> I'm in two minds as to whether it's a good idea or not - the underlying
> issue being that libubsan does not yet (AFAICT) have the concept of a
> coroutine, so that the diagnostics are not very specific and might appear
> strange (i.e. "execution reached the end of a value-returning function
> without returning a value" which is a bit of an odd diagnostic for
> a missing return_void ()).
> 
> OTOH one might argue that some diagnostic is better than silent UB .. but
> I do not have cycles to address improving this in upstream libsanitizer ...
> 
> The logic used here is intended to match cp_maybe_instrument_return ()
> although it's not 100% clear that that is doing exactly what the comment
> says - since it does not distinguish between -fno-sanitize=return and
> the case that the user simply did not specify it.  So that
> -fsanitize=unreachable is ignored for both fno-sanitize=return and the
> unset case.

I think that's correct, what we care about is whether return 
sanitization is enabled, not which flags were used to specify that.

> --- 8< ---
> 
> [dcl.fct.def.coroutine] / 6 Note 1:
> "If return_void is found, flowing off the end of a coroutine is equivalent
> to a co_return with no operand. Otherwise, flowing off the end of a
> coroutine results in undefined behavior."
> 
> Here we implement this as a check for sanitized returns and call the ubsan
> instrumentation; if that is not enabled we mark this as unreachable (which
> might trap depending on the target settings).
> 
> gcc/cp/ChangeLog:
> 
> 	* coroutines.cc
> 	(cp_coroutine_transform::wrap_original_function_body): Instrument
> 	the case where control flows off the end of a coroutine and the
> 	user promise has no return_void entry.
> 
> Signed-off-by: Iain Sandoe <iain@sandoe.co.uk>
> ---
>   gcc/cp/coroutines.cc | 18 ++++++++++++++++++
>   1 file changed, 18 insertions(+)
> 
> diff --git a/gcc/cp/coroutines.cc b/gcc/cp/coroutines.cc
> index e709d02b5f7..b67f4e3ef88 100644
> --- a/gcc/cp/coroutines.cc
> +++ b/gcc/cp/coroutines.cc
> @@ -33,6 +33,9 @@ along with GCC; see the file COPYING3.  If not see
>   #include "gcc-rich-location.h"
>   #include "hash-map.h"
>   #include "coroutines.h"
> +#include "c-family/c-ubsan.h"
> +#include "attribs.h" /* lookup_attribute */

I don't see any use of lookup_attribute?

> +#include "asan.h" /* sanitize_flags_p */
>   
>   static bool coro_promise_type_found_p (tree, location_t);
>   
> @@ -4335,8 +4338,17 @@ cp_coroutine_transform::wrap_original_function_body ()
>         finish_expr_stmt (initial_await);
>         /* Append the original function body.  */
>         add_stmt (coroutine_body);
> +      /* Flowing off the end of a coroutine is equivalent to calling
> +	 promise.return_void () or is UB if the promise does not contain
> +	 that.  Do not add an unreachable unless the user has asked for
> +	 checking of such cases.  */

Let's mention that this is trying to parallel cp_maybe_instrument_return.

Or, actually, how about factoring out the actual statement building from 
that function so we can use it here as well?

Jason
Iain Sandoe Sept. 4, 2024, 8 p.m. UTC | #2
> On 4 Sep 2024, at 17:21, Jason Merrill <jason@redhat.com> wrote:
> 
> On 9/1/24 12:17 PM, Iain Sandoe wrote:
>> This came up in discussion of an earlier patch.
>> I'm in two minds as to whether it's a good idea or not - the underlying
>> issue being that libubsan does not yet (AFAICT) have the concept of a
>> coroutine, so that the diagnostics are not very specific and might appear
>> strange (i.e. "execution reached the end of a value-returning function
>> without returning a value" which is a bit of an odd diagnostic for
>> a missing return_void ()).
>> OTOH one might argue that some diagnostic is better than silent UB .. but
>> I do not have cycles to address improving this in upstream libsanitizer ...
>> The logic used here is intended to match cp_maybe_instrument_return ()
>> although it's not 100% clear that that is doing exactly what the comment
>> says - since it does not distinguish between -fno-sanitize=return and
>> the case that the user simply did not specify it.  So that
>> -fsanitize=unreachable is ignored for both fno-sanitize=return and the
>> unset case.
> 
> I think that's correct, what we care about is whether return sanitization is enabled, not which flags were used to specify that.
OK.
> 
>> --- 8< ---
>> [dcl.fct.def.coroutine] / 6 Note 1:
>> "If return_void is found, flowing off the end of a coroutine is equivalent
>> to a co_return with no operand. Otherwise, flowing off the end of a
>> coroutine results in undefined behavior."
>> Here we implement this as a check for sanitized returns and call the ubsan
>> instrumentation; if that is not enabled we mark this as unreachable (which
>> might trap depending on the target settings).
>> gcc/cp/ChangeLog:
>> 	* coroutines.cc
>> 	(cp_coroutine_transform::wrap_original_function_body): Instrument
>> 	the case where control flows off the end of a coroutine and the
>> 	user promise has no return_void entry.
>> Signed-off-by: Iain Sandoe <iain@sandoe.co.uk>
>> ---
>>  gcc/cp/coroutines.cc | 18 ++++++++++++++++++
>>  1 file changed, 18 insertions(+)
>> diff --git a/gcc/cp/coroutines.cc b/gcc/cp/coroutines.cc
>> index e709d02b5f7..b67f4e3ef88 100644
>> --- a/gcc/cp/coroutines.cc
>> +++ b/gcc/cp/coroutines.cc
>> @@ -33,6 +33,9 @@ along with GCC; see the file COPYING3.  If not see
>>  #include "gcc-rich-location.h"
>>  #include "hash-map.h"
>>  #include "coroutines.h"
>> +#include "c-family/c-ubsan.h"
>> +#include "attribs.h" /* lookup_attribute */
> 
> I don't see any use of lookup_attribute?

It’s needed by asan.h

>> +#include "asan.h" /* sanitize_flags_p */
>>    static bool coro_promise_type_found_p (tree, location_t);
>>  @@ -4335,8 +4338,17 @@ cp_coroutine_transform::wrap_original_function_body ()
>>        finish_expr_stmt (initial_await);
>>        /* Append the original function body.  */
>>        add_stmt (coroutine_body);
>> +      /* Flowing off the end of a coroutine is equivalent to calling
>> +	 promise.return_void () or is UB if the promise does not contain
>> +	 that.  Do not add an unreachable unless the user has asked for
>> +	 checking of such cases.  */
> 
> Let's mention that this is trying to parallel cp_maybe_instrument_return.
> 
> Or, actually, how about factoring out the actual statement building from that function so we can use it here as well?

cp_maybe_instrument_return is local to cxx gimplify at moment
- do you have a preference for where the factored code might live?

thanks
Iain

> 
> Jason
Jason Merrill Sept. 5, 2024, 8:48 p.m. UTC | #3
On 9/4/24 4:00 PM, Iain Sandoe wrote:
> 
> 
>> On 4 Sep 2024, at 17:21, Jason Merrill <jason@redhat.com> wrote:
>>
>> On 9/1/24 12:17 PM, Iain Sandoe wrote:
>>> This came up in discussion of an earlier patch.
>>> I'm in two minds as to whether it's a good idea or not - the underlying
>>> issue being that libubsan does not yet (AFAICT) have the concept of a
>>> coroutine, so that the diagnostics are not very specific and might appear
>>> strange (i.e. "execution reached the end of a value-returning function
>>> without returning a value" which is a bit of an odd diagnostic for
>>> a missing return_void ()).
>>> OTOH one might argue that some diagnostic is better than silent UB .. but
>>> I do not have cycles to address improving this in upstream libsanitizer ...
>>> The logic used here is intended to match cp_maybe_instrument_return ()
>>> although it's not 100% clear that that is doing exactly what the comment
>>> says - since it does not distinguish between -fno-sanitize=return and
>>> the case that the user simply did not specify it.  So that
>>> -fsanitize=unreachable is ignored for both fno-sanitize=return and the
>>> unset case.
>>
>> I think that's correct, what we care about is whether return sanitization is enabled, not which flags were used to specify that.
> OK.
>>
>>> --- 8< ---
>>> [dcl.fct.def.coroutine] / 6 Note 1:
>>> "If return_void is found, flowing off the end of a coroutine is equivalent
>>> to a co_return with no operand. Otherwise, flowing off the end of a
>>> coroutine results in undefined behavior."
>>> Here we implement this as a check for sanitized returns and call the ubsan
>>> instrumentation; if that is not enabled we mark this as unreachable (which
>>> might trap depending on the target settings).
>>> gcc/cp/ChangeLog:
>>> 	* coroutines.cc
>>> 	(cp_coroutine_transform::wrap_original_function_body): Instrument
>>> 	the case where control flows off the end of a coroutine and the
>>> 	user promise has no return_void entry.
>>> Signed-off-by: Iain Sandoe <iain@sandoe.co.uk>
>>> ---
>>>   gcc/cp/coroutines.cc | 18 ++++++++++++++++++
>>>   1 file changed, 18 insertions(+)
>>> diff --git a/gcc/cp/coroutines.cc b/gcc/cp/coroutines.cc
>>> index e709d02b5f7..b67f4e3ef88 100644
>>> --- a/gcc/cp/coroutines.cc
>>> +++ b/gcc/cp/coroutines.cc
>>> @@ -33,6 +33,9 @@ along with GCC; see the file COPYING3.  If not see
>>>   #include "gcc-rich-location.h"
>>>   #include "hash-map.h"
>>>   #include "coroutines.h"
>>> +#include "c-family/c-ubsan.h"
>>> +#include "attribs.h" /* lookup_attribute */
>>
>> I don't see any use of lookup_attribute?
> 
> It’s needed by asan.h
> 
>>> +#include "asan.h" /* sanitize_flags_p */
>>>     static bool coro_promise_type_found_p (tree, location_t);
>>>   @@ -4335,8 +4338,17 @@ cp_coroutine_transform::wrap_original_function_body ()
>>>         finish_expr_stmt (initial_await);
>>>         /* Append the original function body.  */
>>>         add_stmt (coroutine_body);
>>> +      /* Flowing off the end of a coroutine is equivalent to calling
>>> +	 promise.return_void () or is UB if the promise does not contain
>>> +	 that.  Do not add an unreachable unless the user has asked for
>>> +	 checking of such cases.  */
>>
>> Let's mention that this is trying to parallel cp_maybe_instrument_return.
>>
>> Or, actually, how about factoring out the actual statement building from that function so we can use it here as well?
> 
> cp_maybe_instrument_return is local to cxx gimplify at moment
> - do you have a preference for where the factored code might live?

Not particularly; I guess it might as well live in cp-gimplify.cc.

Jason
diff mbox series

Patch

diff --git a/gcc/cp/coroutines.cc b/gcc/cp/coroutines.cc
index e709d02b5f7..b67f4e3ef88 100644
--- a/gcc/cp/coroutines.cc
+++ b/gcc/cp/coroutines.cc
@@ -33,6 +33,9 @@  along with GCC; see the file COPYING3.  If not see
 #include "gcc-rich-location.h"
 #include "hash-map.h"
 #include "coroutines.h"
+#include "c-family/c-ubsan.h"
+#include "attribs.h" /* lookup_attribute */
+#include "asan.h" /* sanitize_flags_p */
 
 static bool coro_promise_type_found_p (tree, location_t);
 
@@ -4335,8 +4338,17 @@  cp_coroutine_transform::wrap_original_function_body ()
       finish_expr_stmt (initial_await);
       /* Append the original function body.  */
       add_stmt (coroutine_body);
+      /* Flowing off the end of a coroutine is equivalent to calling
+	 promise.return_void () or is UB if the promise does not contain
+	 that.  Do not add an unreachable unless the user has asked for
+	 checking of such cases.  */
       if (return_void)
 	add_stmt (return_void);
+      else if (sanitize_flags_p (SANITIZE_RETURN, orig_fn_decl))
+	add_stmt (ubsan_instrument_return (fn_start));
+      else if (flag_unreachable_traps
+	       && !sanitize_flags_p (SANITIZE_UNREACHABLE, orig_fn_decl))
+	add_stmt (build_builtin_unreachable (fn_start));
       TRY_STMTS (tcb) = pop_stmt_list (TRY_STMTS (tcb));
       TRY_HANDLERS (tcb) = push_stmt_list ();
       /* Mimic what the parser does for the catch.  */
@@ -4393,8 +4405,14 @@  cp_coroutine_transform::wrap_original_function_body ()
       finish_expr_stmt (initial_await);
       /* Append the original function body.  */
       add_stmt (coroutine_body);
+      /* See comment above.  */
       if (return_void)
 	add_stmt (return_void);
+      else if (sanitize_flags_p (SANITIZE_RETURN, orig_fn_decl))
+	add_stmt (ubsan_instrument_return (fn_start));
+      else if (flag_unreachable_traps
+	       && !sanitize_flags_p (SANITIZE_UNREACHABLE, orig_fn_decl))
+	add_stmt (build_builtin_unreachable (fn_start));
     }
 
   /* co_return branches to the final_suspend label, so declare that now.  */