diff mbox series

linux/const.h: Explain how __is_constexpr() works

Message ID 20220131204357.1133674-1-keescook@chromium.org (mailing list archive)
State Changes Requested
Headers show
Series linux/const.h: Explain how __is_constexpr() works | expand

Commit Message

Kees Cook Jan. 31, 2022, 8:43 p.m. UTC
The __is_constexpr() macro is dark magic. Shed some light on it with
a comment to explain how and why it works.

Cc: Jonathan Corbet <corbet@lwn.net>
Cc: Linus Torvalds <torvalds@linux-foundation.org>
Cc: Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
Cc: Ingo Molnar <mingo@kernel.org>
Cc: Miguel Ojeda <miguel.ojeda.sandonis@gmail.com>
Cc: Rikard Falkeborn <rikard.falkeborn@gmail.com>
Cc: Arnd Bergmann <arnd@arndb.de>
Cc: linux-doc@vger.kernel.org
Signed-off-by: Kees Cook <keescook@chromium.org>
---
Jon, since this is pure comment, do you want to take it through the docs tree?
---
 include/linux/const.h | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

Comments

Gustavo A. R. Silva Jan. 31, 2022, 9:26 p.m. UTC | #1
On Mon, Jan 31, 2022 at 12:43:57PM -0800, Kees Cook wrote:
> The __is_constexpr() macro is dark magic. Shed some light on it with
> a comment to explain how and why it works.
> 
> Cc: Jonathan Corbet <corbet@lwn.net>
> Cc: Linus Torvalds <torvalds@linux-foundation.org>
> Cc: Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
> Cc: Ingo Molnar <mingo@kernel.org>
> Cc: Miguel Ojeda <miguel.ojeda.sandonis@gmail.com>
> Cc: Rikard Falkeborn <rikard.falkeborn@gmail.com>
> Cc: Arnd Bergmann <arnd@arndb.de>
> Cc: linux-doc@vger.kernel.org
> Signed-off-by: Kees Cook <keescook@chromium.org>

Acked-by: Gustavo A. R. Silva <gustavoars@kernel.org>

Thanks
--
Gustavo

> ---
> Jon, since this is pure comment, do you want to take it through the docs tree?
> ---
>  include/linux/const.h | 24 ++++++++++++++++++++++++
>  1 file changed, 24 insertions(+)
> 
> diff --git a/include/linux/const.h b/include/linux/const.h
> index 435ddd72d2c4..7122d6a1f8ce 100644
> --- a/include/linux/const.h
> +++ b/include/linux/const.h
> @@ -7,6 +7,30 @@
>   * This returns a constant expression while determining if an argument is
>   * a constant expression, most importantly without evaluating the argument.
>   * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
> + *
> + * Details:
> + * - sizeof() is an integer constant expression, and does not evaluate the
> + *   value of its operand; it only examines the type of its operand.
> + * - The results of comparing two integer constant expressions is also
> + *   an integer constant expression.
> + * - The use of literal "8" is to avoid warnings about unaligned pointers;
> + *   these could otherwise just be "1"s.
> + * - (long)(x) is used to avoid warnings about 64-bit types on 32-bit
> + *   architectures.
> + * - The C standard defines an "integer constant expression" as different
> + *   from a "null pointer constant" (an integer constant 0 pointer).
> + * - The conditional operator ("... ? ... : ...") returns the type of the
> + *   operand that isn't a null pointer constant. This behavior is the
> + *   central mechanism of the macro.
> + * - If (x) is an integer constant expression, then the "* 0l" resolves it
> + *   into a null pointer constant, which forces the conditional operator
> + *   to return the type of the last operand: "(int *)".
> + * - If (x) is not an integer constant expression, then the type of the
> + *   conditional operator is from the first operand: "(void *)".
> + * - sizeof(int) == 4 and sizeof(void) == 1.
> + * - The ultimate comparison to "sizeof(int)" chooses between either:
> + *     sizeof(*((int *) (8)) == sizeof(int)   (x was a constant expression)
> + *     sizeof(*((void *)(8)) == sizeof(void)  (x was not a constant expression)
>   */
>  #define __is_constexpr(x) \
>  	(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
> -- 
> 2.30.2
>
Jani Nikula Feb. 1, 2022, 12:01 p.m. UTC | #2
On Mon, 31 Jan 2022, Kees Cook <keescook@chromium.org> wrote:
> The __is_constexpr() macro is dark magic. Shed some light on it with
> a comment to explain how and why it works.
>
> Cc: Jonathan Corbet <corbet@lwn.net>
> Cc: Linus Torvalds <torvalds@linux-foundation.org>
> Cc: Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
> Cc: Ingo Molnar <mingo@kernel.org>
> Cc: Miguel Ojeda <miguel.ojeda.sandonis@gmail.com>
> Cc: Rikard Falkeborn <rikard.falkeborn@gmail.com>
> Cc: Arnd Bergmann <arnd@arndb.de>
> Cc: linux-doc@vger.kernel.org
> Signed-off-by: Kees Cook <keescook@chromium.org>
> ---
> Jon, since this is pure comment, do you want to take it through the docs tree?
> ---
>  include/linux/const.h | 24 ++++++++++++++++++++++++
>  1 file changed, 24 insertions(+)
>
> diff --git a/include/linux/const.h b/include/linux/const.h
> index 435ddd72d2c4..7122d6a1f8ce 100644
> --- a/include/linux/const.h
> +++ b/include/linux/const.h
> @@ -7,6 +7,30 @@
>   * This returns a constant expression while determining if an argument is
>   * a constant expression, most importantly without evaluating the argument.
>   * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
> + *
> + * Details:
> + * - sizeof() is an integer constant expression, and does not evaluate the
> + *   value of its operand; it only examines the type of its operand.
> + * - The results of comparing two integer constant expressions is also
> + *   an integer constant expression.
> + * - The use of literal "8" is to avoid warnings about unaligned pointers;
> + *   these could otherwise just be "1"s.

I thought the first literal 8 was just for looks, and it mattered only
for the last literal 8.

It's been a while when I looked all of this up, but this pretty much
matches what I remember. LGTM.

BR,
Jani.


> + * - (long)(x) is used to avoid warnings about 64-bit types on 32-bit
> + *   architectures.
> + * - The C standard defines an "integer constant expression" as different
> + *   from a "null pointer constant" (an integer constant 0 pointer).
> + * - The conditional operator ("... ? ... : ...") returns the type of the
> + *   operand that isn't a null pointer constant. This behavior is the
> + *   central mechanism of the macro.
> + * - If (x) is an integer constant expression, then the "* 0l" resolves it
> + *   into a null pointer constant, which forces the conditional operator
> + *   to return the type of the last operand: "(int *)".
> + * - If (x) is not an integer constant expression, then the type of the
> + *   conditional operator is from the first operand: "(void *)".
> + * - sizeof(int) == 4 and sizeof(void) == 1.
> + * - The ultimate comparison to "sizeof(int)" chooses between either:
> + *     sizeof(*((int *) (8)) == sizeof(int)   (x was a constant expression)
> + *     sizeof(*((void *)(8)) == sizeof(void)  (x was not a constant expression)
>   */
>  #define __is_constexpr(x) \
>  	(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
Rasmus Villemoes Feb. 1, 2022, 1:05 p.m. UTC | #3
On 31/01/2022 21.43, Kees Cook wrote:
> The __is_constexpr() macro is dark magic. Shed some light on it with
> a comment to explain how and why it works.
> 
> Cc: Jonathan Corbet <corbet@lwn.net>
> Cc: Linus Torvalds <torvalds@linux-foundation.org>
> Cc: Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
> Cc: Ingo Molnar <mingo@kernel.org>
> Cc: Miguel Ojeda <miguel.ojeda.sandonis@gmail.com>
> Cc: Rikard Falkeborn <rikard.falkeborn@gmail.com>
> Cc: Arnd Bergmann <arnd@arndb.de>
> Cc: linux-doc@vger.kernel.org
> Signed-off-by: Kees Cook <keescook@chromium.org>
> ---
> Jon, since this is pure comment, do you want to take it through the docs tree?
> ---
>  include/linux/const.h | 24 ++++++++++++++++++++++++
>  1 file changed, 24 insertions(+)
> 
> diff --git a/include/linux/const.h b/include/linux/const.h
> index 435ddd72d2c4..7122d6a1f8ce 100644
> --- a/include/linux/const.h
> +++ b/include/linux/const.h
> @@ -7,6 +7,30 @@
>   * This returns a constant expression while determining if an argument is
>   * a constant expression, most importantly without evaluating the argument.
>   * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
> + *
> + * Details:
> + * - sizeof() is an integer constant expression, and does not evaluate the
> + *   value of its operand; it only examines the type of its operand.
> + * - The results of comparing two integer constant expressions is also
> + *   an integer constant expression.
> + * - The use of literal "8" is to avoid warnings about unaligned pointers;
> + *   these could otherwise just be "1"s.

Just the second 8, the first could be a 0 or 12345 or whatever.

> + * - (long)(x) is used to avoid warnings about 64-bit types on 32-bit
> + *   architectures.
> + * - The C standard defines an "integer constant expression" as different
> + *   from a "null pointer constant" (an integer constant 0 pointer).

I don't see the point of this bullet. Yes, an ICE is a distinct concept
from a null pointer constant, obviously. One is defined in terms of the
other - and your parenthesis is not an accurate paraphrase of the
definition of a null pointer constant.

> + * - The conditional operator ("... ? ... : ...") returns the type of the
> + *   operand that isn't a null pointer constant. This behavior is the
> + *   central mechanism of the macro.
> + * - If (x) is an integer constant expression, then the "* 0l" resolves it
> + *   into a null pointer constant

yes, because then it becomes "An integer constant expression with the
value 0,".

, which forces the conditional operator
> + *   to return the type of the last operand: "(int *)".
> + * - If (x) is not an integer constant expression, then the type of the
> + *   conditional operator is from the first operand: "(void *)".

Not entirely correct (and by "first" you probably meant second). It's
better to just quote chapter-and-verse.

C99, 6.5.15.6:

[...]
if one operand is a
null pointer constant, the result has the type of the other operand;
otherwise, one operand
is a pointer to void or a qualified version of void, in which case the
result type is a
pointer to an appropriately qualified version of void.

I.e., the second and third operands are treated symmetrically in the
standard.

> + * - sizeof(int) == 4 and sizeof(void) == 1.
> + * - The ultimate comparison to "sizeof(int)" chooses between either:
> + *     sizeof(*((int *) (8)) == sizeof(int)   (x was a constant expression)
> + *     sizeof(*((void *)(8)) == sizeof(void)  (x was not a constant expression)

Actually, since the first operand (the condition) is a non-zero number,
the _value_ of the whole expression is the value of the _second_
operand, but with a _type_ determined by the above rules. So the whole
ternary operator evalutes to either

  (void *)((void *)((long)(x) * 0l))

or

  (int *)((void *)((long)(x) * 0l))


I don't think adding slightly inaccurate comments would help a future
reader at all. Then it's better to just stick to Linus' "it's art, and
art should not be explained".

Rasmus
Matthew Wilcox Feb. 1, 2022, 3:09 p.m. UTC | #4
On Tue, Feb 01, 2022 at 02:05:53PM +0100, Rasmus Villemoes wrote:
> I don't think adding slightly inaccurate comments would help a future
> reader at all. Then it's better to just stick to Linus' "it's art, and
> art should not be explained".

/* You are not expected to understand this */
David Laight Feb. 2, 2022, 8:49 a.m. UTC | #5
From: Rasmus Villemoes
> Sent: 01 February 2022 13:06
...
> > + * - The C standard defines an "integer constant expression" as different
> > + *   from a "null pointer constant" (an integer constant 0 pointer).
> 
> I don't see the point of this bullet. Yes, an ICE is a distinct concept
> from a null pointer constant, obviously. One is defined in terms of the
> other - and your parenthesis is not an accurate paraphrase of the
> definition of a null pointer constant.

From what I remember a "null pointer constant" is in "integer constant
expression with value 0 cast to a pointer type".
So (void *)(1-1) is just as valid as (void *)0.

Not sure any of it is relevant here.

	David

-
Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
Registration No: 1397386 (Wales)
Uecker, Martin Feb. 2, 2022, 3:43 p.m. UTC | #6
Am Mittwoch, den 02.02.2022, 08:49 +0000 schrieb David Laight:
> From: Rasmus Villemoes
> > Sent: 01 February 2022 13:06
> ...
> > > + * - The C standard defines an "integer constant expression" as different
> > > + *   from a "null pointer constant" (an integer constant 0 pointer).
> > 
> > I don't see the point of this bullet. Yes, an ICE is a distinct concept
> > from a null pointer constant, obviously. One is defined in terms of the
> > other - and your parenthesis is not an accurate paraphrase of the
> > definition of a null pointer constant.
> 
> From what I remember a "null pointer constant" is in "integer constant
> expression with value 0 cast to a pointer type".
> So (void *)(1-1) is just as valid as (void *)0.
> 
> Not sure any of it is relevant here.

The C standard (at least public drafts essentially
identical to the actual standards) can be found here:

http://www.open-std.org/jtc1/sc22/wg14/www/projects#9899

"An integer constant expression with the value 0, or such
an expression cast to type void *, is called a null pointer
constant."

BTW: I think it would be very valuable if WG14 would get
proposals and/or comments from the kernel community.

Martin



> 	David
> 
> -
> Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
> Registration No: 1397386 (Wales)
>
David Laight Feb. 2, 2022, 4:19 p.m. UTC | #7
From: Kees Cook
> Sent: 31 January 2022 20:44
> 
> The __is_constexpr() macro is dark magic. Shed some light on it with
> a comment to explain how and why it works.
> 
...
> diff --git a/include/linux/const.h b/include/linux/const.h
> index 435ddd72d2c4..7122d6a1f8ce 100644
> --- a/include/linux/const.h
> +++ b/include/linux/const.h
> @@ -7,6 +7,30 @@
>   * This returns a constant expression while determining if an argument is
>   * a constant expression, most importantly without evaluating the argument.
>   * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
> + *
> + * Details:
> + * - sizeof() is an integer constant expression, and does not evaluate the
> + *   value of its operand; it only examines the type of its operand.
> + * - The results of comparing two integer constant expressions is also
> + *   an integer constant expression.
> + * - The use of literal "8" is to avoid warnings about unaligned pointers;
> + *   these could otherwise just be "1"s.
> + * - (long)(x) is used to avoid warnings about 64-bit types on 32-bit
> + *   architectures.
> + * - The C standard defines an "integer constant expression" as different
> + *   from a "null pointer constant" (an integer constant 0 pointer).
> + * - The conditional operator ("... ? ... : ...") returns the type of the
> + *   operand that isn't a null pointer constant. This behavior is the
> + *   central mechanism of the macro.
> + * - If (x) is an integer constant expression, then the "* 0l" resolves it
> + *   into a null pointer constant, which forces the conditional operator
> + *   to return the type of the last operand: "(int *)".
> + * - If (x) is not an integer constant expression, then the type of the
> + *   conditional operator is from the first operand: "(void *)".
> + * - sizeof(int) == 4 and sizeof(void) == 1.
> + * - The ultimate comparison to "sizeof(int)" chooses between either:
> + *     sizeof(*((int *) (8)) == sizeof(int)   (x was a constant expression)
> + *     sizeof(*((void *)(8)) == sizeof(void)  (x was not a constant expression)
>   */
>  #define __is_constexpr(x) \
>  	(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

This has been making my head hurt all day.
The above isn't really a true description - ?: doesn't work that way.
Try the following for size:

- The conditional operator (?:) requires that both expressions have the
  the same type (after numeric promotions).
  The type of the result is a compile time constant and doesn't depend on any
  variables.
- If the expressions have distinct non-NULL pointer types then they are both
  cast to (void *) and the result has type 'void *'.
- A NULL pointer can be made from any integer constant expression that
  evaluates to 0, not just a literal 0.
- So the type of (0 ? (void *)(x) : (int *)8) is 'int *' if (x) is zero
  (because of the NULL) and (void *) otherwise because the pointer types
  don't match.

You can test this by evaluating:
	sizeof *(0 ? (float *)4 : (int *)4)
This is 1 because of the implicit (void *) cast.

I'd also delete the l from the 0l - it isn't needed.
(Or at least use L)

	David

-
Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
Registration No: 1397386 (Wales)
Miguel Ojeda Feb. 2, 2022, 8:13 p.m. UTC | #8
On Wed, Feb 2, 2022 at 5:19 PM David Laight <David.Laight@aculab.com> wrote:
>
> - The conditional operator (?:) requires that both expressions have the
>   the same type (after numeric promotions).

I think you are referring to the arithmetic types case, but that one
does not apply here.

> - If the expressions have distinct non-NULL pointer types then they are both
>   cast to (void *) and the result has type 'void *'.

GCC and Clang give `void *` for this, but something like `float *` and
`int *` do not fit the constrains of the operator.

Cheers,
Miguel
Miguel Ojeda Feb. 2, 2022, 8:14 p.m. UTC | #9
On Wed, Feb 2, 2022 at 4:43 PM Uecker, Martin
<Martin.Uecker@med.uni-goettingen.de> wrote:
>
> BTW: I think it would be very valuable if WG14 would get
> proposals and/or comments from the kernel community.

Agreed! I am happy to help anyone do so.

(`memset_explicit` and `#once` were inspired from kernel discussions /
use cases)

Cheers,
Miguel
Miguel Ojeda Feb. 2, 2022, 8:43 p.m. UTC | #10
On Mon, Jan 31, 2022 at 9:43 PM Kees Cook <keescook@chromium.org> wrote:
>
> + * - The conditional operator ("... ? ... : ...") returns the type of the
> + *   operand that isn't a null pointer constant. This behavior is the

Perhaps clarify that this happens only if it fits that case? ...

> + * - If (x) is an integer constant expression, then the "* 0l" resolves it
> + *   into a null pointer constant, which forces the conditional operator
> + *   to return the type of the last operand: "(int *)".
> + * - If (x) is not an integer constant expression, then the type of the
> + *   conditional operator is from the first operand: "(void *)".

... i.e. this one happens because it is specified as returning a
pointer to void (one could read it as returning the type of the first
operand).

What about something like:

  - The behavior (including its return type) of the conditional
operator ("... ? ... : ...") depends on the kind of expressions given
for the second and third operands. This is the central mechanism of
the macro.
  - If (x) is an integer constant expression, then the "* 0l" resolves
it into a null pointer constant. When one operand is a null pointer
constant and the other is a pointer, the conditional operator returns
the type of the pointer operand; that is, "int *".
  - If (x) is not an integer constant expression, then that operand is
a pointer to void (but not a null pointer constant). When one operand
is a pointer to void and the other a pointer to an object type, the
conditional operator returns a "void *" type.

Cheers,
Miguel
Rasmus Villemoes Feb. 2, 2022, 8:44 p.m. UTC | #11
On 02/02/2022 17.19, David Laight wrote:
> From: Kees Cook
>> Sent: 31 January 2022 20:44
>>
>> The __is_constexpr() macro is dark magic. Shed some light on it with
>> a comment to explain how and why it works.
>>
> ...
>> diff --git a/include/linux/const.h b/include/linux/const.h
>> index 435ddd72d2c4..7122d6a1f8ce 100644
>> --- a/include/linux/const.h
>> +++ b/include/linux/const.h
>> @@ -7,6 +7,30 @@
>>   * This returns a constant expression while determining if an argument is
>>   * a constant expression, most importantly without evaluating the argument.
>>   * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
>> + *
>> + * Details:
>> + * - sizeof() is an integer constant expression, and does not evaluate the
>> + *   value of its operand; it only examines the type of its operand.
>> + * - The results of comparing two integer constant expressions is also
>> + *   an integer constant expression.
>> + * - The use of literal "8" is to avoid warnings about unaligned pointers;
>> + *   these could otherwise just be "1"s.
>> + * - (long)(x) is used to avoid warnings about 64-bit types on 32-bit
>> + *   architectures.
>> + * - The C standard defines an "integer constant expression" as different
>> + *   from a "null pointer constant" (an integer constant 0 pointer).
>> + * - The conditional operator ("... ? ... : ...") returns the type of the
>> + *   operand that isn't a null pointer constant. This behavior is the
>> + *   central mechanism of the macro.
>> + * - If (x) is an integer constant expression, then the "* 0l" resolves it
>> + *   into a null pointer constant, which forces the conditional operator
>> + *   to return the type of the last operand: "(int *)".
>> + * - If (x) is not an integer constant expression, then the type of the
>> + *   conditional operator is from the first operand: "(void *)".
>> + * - sizeof(int) == 4 and sizeof(void) == 1.
>> + * - The ultimate comparison to "sizeof(int)" chooses between either:
>> + *     sizeof(*((int *) (8)) == sizeof(int)   (x was a constant expression)
>> + *     sizeof(*((void *)(8)) == sizeof(void)  (x was not a constant expression)
>>   */
>>  #define __is_constexpr(x) \
>>  	(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
> 
> This has been making my head hurt all day.
> The above isn't really a true description - ?: doesn't work that way.
> Try the following for size:
> 
> - The conditional operator (?:) requires that both expressions have the
>   the same type (after numeric promotions).

No. Please read 6.5.15.3 for the preconditions, and 6.5.15.5 and
6.5.15.6 for the rules governing the type of the whole expression.

>   The type of the result is a compile time constant and doesn't depend on any
>   variables.

Yes, the type of any expression in C is known at compile time, and is
determined via the rules in the C standard. I wouldn't call it a
"compile time constant" though.

> - If the expressions have distinct non-NULL pointer types then they are both
>   cast to (void *) and the result has type 'void *'.

Wrong.

> - A NULL pointer can be made from any integer constant expression that
>   evaluates to 0, not just a literal 0.
> - So the type of (0 ? (void *)(x) : (int *)8) is 'int *' if (x) is zero
>   (because of the NULL) and (void *) otherwise because the pointer types
>   don't match.

That's basically how this macro works, but "So" is not warranted as it
does not follow from any of the previous, wrong, statements.

> You can test this by evaluating:
> 	sizeof *(0 ? (float *)4 : (int *)4)

That's an ill-formed conditional operator, and gcc says as much even
without any -Wall in effect.

warning: pointer type mismatch in conditional expression
    8 |  return sizeof(*(0 ? (float *)4 : (int *)4));


> This is 1 because of the implicit (void *) cast.

There is no such thing.

> I'd also delete the l from the 0l - it isn't needed.
> (Or at least use L)

That's probably true, I think it's a leftover from before the explicit
(long) cast was added, which was done to ensure the expression being
cast to (void*) wasn't a 64-bit type when void* is 32 bit. The 'l' was a
simple way to widen the expression to long in the case where x has a
type narrower than void*.

Rasmus
David Laight Feb. 2, 2022, 10:20 p.m. UTC | #12
From: Miguel Ojeda <miguel.ojeda.sandonis@gmail.com>
> Sent: 02 February 2022 20:14
> To: David Laight
> 
> On Wed, Feb 2, 2022 at 5:19 PM David Laight <David.Laight@aculab.com> wrote:
> >
> > - The conditional operator (?:) requires that both expressions have the
> >   the same type (after numeric promotions).
> 
> I think you are referring to the arithmetic types case, but that one
> does not apply here.

The type of the result depends on the type of the 2nd and 3rd arguments.
Not on the value of the first one.

> > - If the expressions have distinct non-NULL pointer types then they are both
> >   cast to (void *) and the result has type 'void *'.
> 
> GCC and Clang give `void *` for this, but something like `float *` and
> `int *` do not fit the constrains of the operator.

Compiling:

#define k(x) (sizeof(*(1 ? (float *)8 : (int *)8)))

int f1(void){ return k(6); }

gives:

f1:
        movl    $1, %eax
        ret

(see https://godbolt.org/z/e1MvYYxGz)

Change to #define k(x) (sizeof(*(1 ? (void *)(x) : (int *)0)))
and you only get 4 when x is a compile-time constant 0.

It has nothing to with the condition, the compiler is trying to 'sort out'
a suitable return type.

I suspect the mismatched pointer types might even be a gcc extension.

	David

-
Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
Registration No: 1397386 (Wales)
David Laight Feb. 2, 2022, 10:42 p.m. UTC | #13
From: Rasmus Villemoes
> Sent: 02 February 2022 20:44
> 
> On 02/02/2022 17.19, David Laight wrote:
> > From: Kees Cook
> >> Sent: 31 January 2022 20:44
> >>
> >> The __is_constexpr() macro is dark magic. Shed some light on it with
> >> a comment to explain how and why it works.
> >>
> > ...
> >> diff --git a/include/linux/const.h b/include/linux/const.h
> >> index 435ddd72d2c4..7122d6a1f8ce 100644
> >> --- a/include/linux/const.h
> >> +++ b/include/linux/const.h
> >> @@ -7,6 +7,30 @@
> >>   * This returns a constant expression while determining if an argument is
> >>   * a constant expression, most importantly without evaluating the argument.
> >>   * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
> >> + *
> >> + * Details:
> >> + * - sizeof() is an integer constant expression, and does not evaluate the
> >> + *   value of its operand; it only examines the type of its operand.
> >> + * - The results of comparing two integer constant expressions is also
> >> + *   an integer constant expression.
> >> + * - The use of literal "8" is to avoid warnings about unaligned pointers;
> >> + *   these could otherwise just be "1"s.
> >> + * - (long)(x) is used to avoid warnings about 64-bit types on 32-bit
> >> + *   architectures.
> >> + * - The C standard defines an "integer constant expression" as different
> >> + *   from a "null pointer constant" (an integer constant 0 pointer).
> >> + * - The conditional operator ("... ? ... : ...") returns the type of the
> >> + *   operand that isn't a null pointer constant. This behavior is the
> >> + *   central mechanism of the macro.
> >> + * - If (x) is an integer constant expression, then the "* 0l" resolves it
> >> + *   into a null pointer constant, which forces the conditional operator
> >> + *   to return the type of the last operand: "(int *)".
> >> + * - If (x) is not an integer constant expression, then the type of the
> >> + *   conditional operator is from the first operand: "(void *)".
> >> + * - sizeof(int) == 4 and sizeof(void) == 1.
> >> + * - The ultimate comparison to "sizeof(int)" chooses between either:
> >> + *     sizeof(*((int *) (8)) == sizeof(int)   (x was a constant expression)
> >> + *     sizeof(*((void *)(8)) == sizeof(void)  (x was not a constant expression)
> >>   */
> >>  #define __is_constexpr(x) \
> >>  	(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
> >
> > This has been making my head hurt all day.
> > The above isn't really a true description - ?: doesn't work that way.
> > Try the following for size:
> >
> > - The conditional operator (?:) requires that both expressions have the
> >   the same type (after numeric promotions).
> 
> No. Please read 6.5.15.3 for the preconditions, and 6.5.15.5 and
> 6.5.15.6 for the rules governing the type of the whole expression.
> 
> >   The type of the result is a compile time constant and doesn't depend on any
> >   variables.
> 
> Yes, the type of any expression in C is known at compile time, and is
> determined via the rules in the C standard. I wouldn't call it a
> "compile time constant" though.
> 
> > - If the expressions have distinct non-NULL pointer types then they are both
> >   cast to (void *) and the result has type 'void *'.
> 
> Wrong.

gah server me right for using godbolt to test it.

> 
> > - A NULL pointer can be made from any integer constant expression that
> >   evaluates to 0, not just a literal 0.
> > - So the type of (0 ? (void *)(x) : (int *)8) is 'int *' if (x) is zero
> >   (because of the NULL) and (void *) otherwise because the pointer types
> >   don't match.
> 
> That's basically how this macro works, but "So" is not warranted as it
> does not follow from any of the previous, wrong, statements.
> 
> > You can test this by evaluating:
> > 	sizeof *(0 ? (float *)4 : (int *)4)
> 
> That's an ill-formed conditional operator, and gcc says as much even
> without any -Wall in effect.
> 
> warning: pointer type mismatch in conditional expression
>     8 |  return sizeof(*(0 ? (float *)4 : (int *)4));
> 
> 
> > This is 1 because of the implicit (void *) cast.
> 
> There is no such thing.

Ok let's try again...
The compiler needs to find a 'compatible type' either for:
	(void *)x	and	(int *)8
or for:
	(void *)0	and	(int *)8
In the former it is 'void *' and the latter 'int *' because the (void *)0
is NULL and thus a valid 'int *' pointer.

In any case suggesting that it is based on the value before the ? is bogus.

That is probably a reasonable description.

	David

-
Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
Registration No: 1397386 (Wales)
Miguel Ojeda Feb. 2, 2022, 11:01 p.m. UTC | #14
On Wed, Feb 2, 2022 at 11:20 PM David Laight <David.Laight@aculab.com> wrote:
>
> The type of the result depends on the type of the 2nd and 3rd arguments.
> Not on the value of the first one.

I am not talking about the first operand. The behavior of the
conditional operator has a few cases. Since you mentioned promotions,
it looked like you were thinking about what happens for the arithmetic
types case, i.e.

"""If both the second and third operands have arithmetic type, the
result type that would be determined by the usual arithmetic
conversions, were they applied to those two operands, is the type of
the result."""

which could lead to thinking that the expressions need to have the
same type as you mentioned, but that is not true, and the arithmetic
types case is not used in the macro either. The cases used are the
null pointer constant vs. pointer and the pointer to void vs. pointer
to object type.

> It has nothing to with the condition, the compiler is trying to 'sort out'
> a suitable return type.
>
> I suspect the mismatched pointer types might even be a gcc extension.

That is why I said it does not fit the constraints of the operator.
The standard does not describe what happens in such a case.

Cheers,
Miguel
Nick Desaulniers Feb. 2, 2022, 11:08 p.m. UTC | #15
On Wed, Feb 2, 2022 at 3:01 PM Miguel Ojeda
<miguel.ojeda.sandonis@gmail.com> wrote:
>
> On Wed, Feb 2, 2022 at 11:20 PM David Laight <David.Laight@aculab.com> wrote:
> >
> > The type of the result depends on the type of the 2nd and 3rd arguments.
> > Not on the value of the first one.
>
> I am not talking about the first operand. The behavior of the
> conditional operator has a few cases. Since you mentioned promotions,
> it looked like you were thinking about what happens for the arithmetic
> types case, i.e.
>
> """If both the second and third operands have arithmetic type, the
> result type that would be determined by the usual arithmetic
> conversions, were they applied to those two operands, is the type of
> the result."""
>
> which could lead to thinking that the expressions need to have the
> same type as you mentioned, but that is not true, and the arithmetic
> types case is not used in the macro either. The cases used are the
> null pointer constant vs. pointer and the pointer to void vs. pointer
> to object type.
>
> > It has nothing to with the condition, the compiler is trying to 'sort out'
> > a suitable return type.
> >
> > I suspect the mismatched pointer types might even be a gcc extension.
>
> That is why I said it does not fit the constraints of the operator.
> The standard does not describe what happens in such a case.

Since this patch is a rephrasing of
https://stackoverflow.com/a/49481218, I think the relevant citation
from the C standard is below:

```
The key here is that the conditional operator returns a different type
depending on whether one of the operands is a null pointer constant
(6.5.15.6):

[...] if one operand is a null pointer constant, the result has the
type of the other operand; otherwise, one operand is a pointer to void
or a qualified version of void, in which case the result type is a
pointer to an appropriately qualified version of void.

So, if x was an integer constant expression, then the second operand
is a null pointer constant and therefore the type of the expression is
the type of the third operand, which is a pointer to int.
```
Miguel Ojeda Feb. 3, 2022, 12:28 a.m. UTC | #16
On Wed, Feb 2, 2022 at 11:42 PM David Laight <David.Laight@aculab.com> wrote:
>
> The compiler needs to find a 'compatible type' either for:
>         (void *)x       and     (int *)8
> or for:
>         (void *)0       and     (int *)8
>
> In the former it is 'void *' and the latter 'int *' because the (void *)0
> is NULL and thus a valid 'int *' pointer.

I think you are trying to come up with an explanation of how it works
based on compiler outputs (it makes sense to think that the compiler
has to find some reasonable "common" type). But the conditional
operator works case-by-case, slightly differently depending on what
kind of operands you give.

In the two cases involved, there is no "finding a compatible type" /
promotions going on -- the standard gives explicitly that it is a
pointer to void (former case), and the type of the other operand
(latter case). The value is still decided by the condition.

e.g. https://godbolt.org/z/zzE8dc7Ye

0 ?          void pointer (1) : pointer to object type (42) = (void *) 0x2a
1 ?          void pointer (1) : pointer to object type (42) = (void *) 0x1
0 ? null pointer constant (0) : pointer                (42) = ( int *) 0x2a
1 ? null pointer constant (0) : pointer                (42) = ( int *) (nil)

> In any case suggesting that it is based on the value before the ? is bogus.

What Rasmus was saying is that which value is selected still depends
on the condition, because the last paragraph of the explanation in the
commit is wrong. It should be something like:

  - The ultimate comparison to "sizeof(int)" reduces to either:
        sizeof(int) == sizeof(*(int *)0)  (x was a constant expression)
        sizeof(int) == sizeof(*(void *)0) (x was not a constant expression)

Cheers,
Miguel
David Laight Feb. 3, 2022, 9:25 a.m. UTC | #17
From: Miguel Ojeda
> Sent: 02 February 2022 20:43
> 
> On Mon, Jan 31, 2022 at 9:43 PM Kees Cook <keescook@chromium.org> wrote:
> >
> > + * - The conditional operator ("... ? ... : ...") returns the type of the
> > + *   operand that isn't a null pointer constant. This behavior is the
> 
> Perhaps clarify that this happens only if it fits that case? ...
> 
> > + * - If (x) is an integer constant expression, then the "* 0l" resolves it
> > + *   into a null pointer constant, which forces the conditional operator
> > + *   to return the type of the last operand: "(int *)".
> > + * - If (x) is not an integer constant expression, then the type of the
> > + *   conditional operator is from the first operand: "(void *)".
> 
> ... i.e. this one happens because it is specified as returning a
> pointer to void (one could read it as returning the type of the first
> operand).
> 
> What about something like:
> 
>   - The behavior (including its return type) of the conditional
> operator ("... ? ... : ...") depends on the kind of expressions given
> for the second and third operands. This is the central mechanism of
> the macro.
>   - If (x) is an integer constant expression, then the "* 0l" resolves
> it into a null pointer constant. When one operand is a null pointer
> constant and the other is a pointer, the conditional operator returns
> the type of the pointer operand; that is, "int *".
>   - If (x) is not an integer constant expression, then that operand is
> a pointer to void (but not a null pointer constant). When one operand
> is a pointer to void and the other a pointer to an object type, the
> conditional operator returns a "void *" type.

Nick's quote from the C standard actually sums it up nicely:

    The key here is that the conditional operator returns a different type
    depending on whether one of the operands is a null pointer constant
    (6.5.15.6):

    [...] if one operand is a null pointer constant, the result has the
    type of the other operand; otherwise, one operand is a pointer to void
    or a qualified version of void, in which case the result type is a
    pointer to an appropriately qualified version of void.

That followed by a reminder that "(void *)x is a null pointer constant if x
is a compile time integer constant expression" is enough.
All the rest is just TL;DR.

The '8' also just confuse things, they are not important at all.
So it can be:
#define __is_constexpr(x) \
 	(sizeof(*(0 ? ((void *)((long)(x) * 0)) : (int *)0)) == sizeof(int))

	David

-
Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
Registration No: 1397386 (Wales)
diff mbox series

Patch

diff --git a/include/linux/const.h b/include/linux/const.h
index 435ddd72d2c4..7122d6a1f8ce 100644
--- a/include/linux/const.h
+++ b/include/linux/const.h
@@ -7,6 +7,30 @@ 
  * This returns a constant expression while determining if an argument is
  * a constant expression, most importantly without evaluating the argument.
  * Glory to Martin Uecker <Martin.Uecker@med.uni-goettingen.de>
+ *
+ * Details:
+ * - sizeof() is an integer constant expression, and does not evaluate the
+ *   value of its operand; it only examines the type of its operand.
+ * - The results of comparing two integer constant expressions is also
+ *   an integer constant expression.
+ * - The use of literal "8" is to avoid warnings about unaligned pointers;
+ *   these could otherwise just be "1"s.
+ * - (long)(x) is used to avoid warnings about 64-bit types on 32-bit
+ *   architectures.
+ * - The C standard defines an "integer constant expression" as different
+ *   from a "null pointer constant" (an integer constant 0 pointer).
+ * - The conditional operator ("... ? ... : ...") returns the type of the
+ *   operand that isn't a null pointer constant. This behavior is the
+ *   central mechanism of the macro.
+ * - If (x) is an integer constant expression, then the "* 0l" resolves it
+ *   into a null pointer constant, which forces the conditional operator
+ *   to return the type of the last operand: "(int *)".
+ * - If (x) is not an integer constant expression, then the type of the
+ *   conditional operator is from the first operand: "(void *)".
+ * - sizeof(int) == 4 and sizeof(void) == 1.
+ * - The ultimate comparison to "sizeof(int)" chooses between either:
+ *     sizeof(*((int *) (8)) == sizeof(int)   (x was a constant expression)
+ *     sizeof(*((void *)(8)) == sizeof(void)  (x was not a constant expression)
  */
 #define __is_constexpr(x) \
 	(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))