-
Notifications
You must be signed in to change notification settings - Fork 595
Add __attribute__nonnull__() for non-DEBUGGING builds #23641
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
base: blead
Are you sure you want to change the base?
Conversation
f27e796
to
bb147b4
Compare
tried
Looks like a bug with POSIX Perl's Imagine is the wrong word b/c I've done this b4 for private biz XS. So lets imagine, I am a CPAN XS module running on ithread-ed WinPerl, with only 1 Perl thread ( So I am an XS->PP event handler executing inside a TP Thd, The root thread is frozen (blocked until I release control back to the root thread manually). At the end of my TP thread runner, after the I'm avoiding an accident if some enumeration API gets smart (not really) and runs my C callback fn ptr asynchronously, in parallel, on multiple cores on multiple TP OS threads. Bad hygiene to leave your de allocated void ptrs in TLS. So I would definitely want Or this is an optimization to const fold away all machine code associated with PERL_SET_CONTEXT() on single-threaded perls builds. But then the question is, why was You also wrote, or were the last person to clean it up. but the null test existed before the commit above, ill stop git blaming at this point.
|
regen/embed.pl
Outdated
} | ||
else { | ||
push @asserts, "PERL_ASSUME_NON_NULL($argname)"; | ||
push @attrs, "__attribute__nonnull__($n)"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this can make the compiler optimise away the assert()s we generate, and from looking at the generated code that appears to be the case.
I had a look, with ./Configure -des -Dusedevel -DDEBUGGING && make mg.s
Perl_mg_magical
(first name I saw it in the proto.h
diff).
blead:
Perl_mg_magical:
subq $8, %rsp
testq %rdi, %rdi ; check sv
je .L136 ; jump to assert code if it is NULL
...
.L136:
leaq __PRETTY_FUNCTION__.110(%rip), %rcx
movl $136, %edx
leaq .LC0(%rip), %rsi
leaq .LC1(%rip), %rdi
call __assert_fail@PLT
This PR:
Perl_mg_magical:
movl 12(%rdi), %eax ; note directly fetches from the sv flags without the check
movl %eax, %edx
andl $-14680065, %edx
movl %edx, 12(%rdi)
I think we'd need to only generate the __attribute__nonnull__
for non-DEBUGGING builds.
I'll admit to being a little uncomfortable with automatically generated ASSUME()s, since they produce runtime UB if the assumption is false, but I think it's reasonable here, assuming we fix the assert() problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we'd need to only generate the attribute__nonnull for non-DEBUGGING builds.
I don't follow this. What's the downside of generating it for all builds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the downside of generating it for all builds?
It turns all the asserts into no-ops.
void foo(int *) __attribute__((__nonnull__(1)));
void foo(int *ptr) {
assert(ptr);
...
}
Since ptr
is marked "nonnull", the compiler "knows" that ptr
cannot be null in the function body, so it turns assert(ptr)
into a no-op.
Or as Tony wrote:
this can make the compiler optimise away the assert()s we generate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that was exactly my concern. This attribute having both internal and an external effect is most unfortunate. We only want one of them here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't see the problem. Part of the motivation here is to optimize away those asserts. Since these are known at compile time, shouldn't compilations fail if called wrongly, so the asserts aren't needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally agree with you, but the nonnull information is available across translation units because it's in the prototype. GCC can and does make (limited) use of it. Especially with -fanalyzer
, but even -Wnonnull
will catch some cases, especially with optimisations enabled. Optimisations are important here, the compiler does more analysis when they're enabled.
For example, this warns with -fanalyzer
:
#include <stdlib.h>
__attribute__((nonnull(1)))
int foo(char *foo);
int getint();
char *getstring(int arg) {
if (arg)
return "abc";
else
return NULL;
}
int main () {
foo(getstring(getint()));
return 0;
}
This simpler case warns with just -O2 -Wall
:
#include <stdlib.h>
__attribute__((nonnull(1)))
int foo(char *foo);
int getint() {
return 0;
}
char *getstring(int arg) {
if (arg)
return "abc";
else
return NULL;
}
int main () {
foo(getstring(getint()));
return 0;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did over-react, sorry.
I do think we don't want assert()s optimised away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about optimizing them away for functions that aren't visible outside the perl core? If that is ok, what about those that are visible only in perl extensions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why optimise them away at all? debugging builds (and I often use -O0
for debugging builds) aren't that painfully slow.
Why take the risk?
For xenu's example, if you move the definition of getstring() and foo() to another CU, you'll only get a warning from -fanalyzer -flto
builds, the detection isn't perfect:
tony@venus:.../perl/git$ cat 23641c.c
#include <stdlib.h>
__attribute__((nonnull(1)))
int foo(char *foo);
char *getstring(int arg);
int getint() {
return 0;
}
int main () {
foo(getstring(getint()));
return 0;
}
tony@venus:.../perl/git$ cat 23641d.c
#include <stdlib.h>
#include <stdio.h>
__attribute__((nonnull(1)))
int foo(char *foo);
char *getstring(int arg) {
if (arg)
return "abc";
else
return NULL;
}
int foo(char *s) {
puts(s);
return 1;
}
tony@venus:.../perl/git$ gcc -Wall -O2 23641c.c 23641d.c
tony@venus:.../perl/git$ gcc -flto -Wall -O2 23641c.c 23641d.c
tony@venus:.../perl/git$ gcc -fanalyzer -Wall -O2 23641c.c 23641d.c
tony@venus:.../perl/git$ gcc -flto -fanalyzer -Wall -O2 23641c.c 23641d.c
23641c.c: In function ‘main’:
23641c.c:13:5: warning: use of NULL where non-null expected [CWE-476] [-Wanalyzer-null-argument]
13 | foo(getstring(getint()));
| ^
‘main’: events 1-2
|
...
I tried a build with -Dcc='gcc -fanalyzer -flto'
, I killed the link step for miniperl after several minutes because it was using over 55GB of resident space (around which point the machine started to swap.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I'll note that nonnull
is used by UBSAN as well to trap (not just the path isolation mentioned).)
Sort of related https://www.youtube.com/watch?v=3zQ4zw4GNV0 which talks about static analysis of the clang nullability attributes. |
bb147b4
to
738ad2e
Compare
I changed to use the attribute_nonnull only for non-DEBUGGING builds. I also removed the change to use ASSUME. On DEBUGGING builds, the asserts() give clues to the compiler; and on non-DEBUGGING ones, the attribute_nonnull lines give the same clues. There are other assertions besides the non-NULL ones that go away in non-DEBUGGING, but they are insignificant in comparison with the NULL issues. |
Are you aware of the |
All they do is let you use |
Interestingly there's already two functions that use I expect the nonnull make no optimisation difference to either for gcc/clang - they both dereference the pointer without a check. It would optimise away the assert() in S_is_dup_mode() though. There's only one caller to S_is_dup_mode() so I expect it's inlined anyway, and that caller ensures the pointer is nonnull. |
738ad2e
to
2e3e92f
Compare
I also added an attribute_nonnull for the aTHX parameter, even for DEBUGGING builds. That better not be NULL or else things really break; so I don't see any optimization problem arising as a result of this |
ed8616a
to
85a1136
Compare
This code now generates several hundred lines of warnings for compiling the code base. Some are for comparing NULL vs non-NULL. Some are more serious, where a function is being called with a NULL but expects it to not be so. This is a real bug that our asserts haven't caught, which tells me it is code that doesn't get exercised in the test suite. It could be unreachable, I suppose. |
Indeed! The warnings appear to have begun to be emitted with this commit:
It would be better to silence them before merging than wait until later. |
I think most of the warnings is from macros (PM_GETRE for example) testing for a validity rather than asserting that case, so PM_GETRE() returns NULL on a non-PMOP. So the compiler sees PM_GETRE() has a conditional that can return NULL and complains when it's value (only called on a PMOP where we always return non-NULL) is supplied to a nonnull parameter. Similarly for the complaints about various There's also some complaints where non-pointer parameters have meaningless NN markers. (regtail() for one) |
Many of the warnings are from this. My reaction to seeing this warning is "so what?". I don't want to complicate our code by adding macros just to get around this. If we add the nonnull function attribute to a function, I think this warning should be turned off. I have no idea about other compilers |
85a1136
to
f786745
Compare
The latest push generates no warnings on my box's gcc nor clang. The main source of the warnings can be turned off with Some of the warnings resulted from careless coding, where the same macro was called multiple times in a row instead of storing the result of the first call, and using that instead of re-calling the macro. In one case, there already existed a macro to use when it was known that the input is sane;I just changed to use that instead of the more general macro that has a check. And in another case, I added a macro that assumes the input is sane and called that in the three cases where warnings about the general version were raised. The general version was changed to check, then call the non-general version. |
It's saying any such check would be optimised out anyway, because you promised any such args will be nonnull (unless UBSAN is used, in which case it'll trap when entering the function). i.e. It's again the annoying dual meaning of the |
f786745
to
11fe518
Compare
peep.c
Outdated
|
||
assert(OpSIBLING(kid)); | ||
name = op_varname(OpSIBLING(kid)); | ||
name = op_varname(OpSIBLING_nocheck(kid)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of removing the duplicate check but keeping the duplicate fetch why not use an intermediate variable:
OP *varop = OpSIBLING(kid);
assert(varop);
name = op_varname(varop);
This avoids the reader having to check both OpSIBLING(kid)
are the same (and now that OpSIBLING(kid)
and OpSIBLING_nocheck(kid)
are the same).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggested change suppresses the warning.
op_null(kid); | ||
op_null(OpSIBLING(kid)); /* const */ | ||
op_null(OpSIBLING_nocheck(kid)); /* const */ | ||
if (o != topop) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we know kid has a sibling here? There's no logic here to indicate it does.
Note that if the op doesn't have a sibling OpSIBLING_nocheck() will return the wrong OP, not NULL, and in a DEBUGGING build the op_null() parameter assertion will pass, modifying the wrong OP instead of throwing an assertion failure.
Now we do know that there is currently always a sibling, since the code doesn't currently throw, but this change means that if some bug leads to there not being a sibling this is going to fail much later, rather than on entry to op_null().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
op_null(kid);
OP * const const_op = OpSIBLING(kid);
ASSUME(const_op);
op_null(const_op); /* const */
if (o != topop) {
suppresses the warning
effect. */ | ||
#define r1_ PERL_UNIQUE_NAME(r) | ||
#define PM_SETRE(o,r) STMT_START { \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This replaces any instance of r1_
globally, which seems heavily name polluting.
|
||
/* handle the empty pattern */ | ||
if (!RX_PRELEN(PM_GETRE(pm)) && PL_curpm) { | ||
if (!RX_PRELEN(PM_GETRE_raw(pm)) && PL_curpm) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't this just be
if (!RX_PRELEN(new_re) && PL_curpm) {
which saves someone checking the code from having to trace that?
op.c
Outdated
assert(OpSIBLING(kid)); | ||
name = op_varname(OpSIBLING(kid)); | ||
name = op_varname(OpSIBLING_nocheck(kid)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly to the change in peep.c this could use an intermediate variable.
This thought the string '0' meant empty; it should be testing that the string isn't empty.
This got duplicated in recent rebasing
The next commit will want this to be available earlier.
Though code later strips this off, it's best to not put it in in the first place
Instead store the result of the first one in a variable, and use that going forward.
Instead, store the result of the first one in a variable, and use that going forward
There are two sets of macros here, the raw version does no checking; and the non-raw, which does. Instead of duplicating the complicated expressions, use the raw version inside the non-raw one to make it a bit easier to read
This allows a variable name in a macro to be used that doesn't have much extra verbiage that otherwise would be required to make it distinct from other variables.
There are two versions of this macro; the more general one which checks for sanity; and another, to be used when it's already known that things are sane. This commit converts to use the latter, as we do know it's sane here.
gcc emits a bunch of warnings under -Wall for functions that have declared that parameter 'n' is never going to be NULL, and then have the temerity to make sure that that is true. The problem is that we have many macros that are generalized enough to handle the case where the parameter is NULL. It just happens that sometimes they get called as well from code where it is known to be not NULL. We could write additional macros that are known to take a non-NULL parameter and don't do the test. The existing macros would be rewritten to just call the new ones after checking for non-NULL. But that would complicate our code, and a main point of compilers doing optimization is to figure out and remove impossible cases, without the programmers having to be concerned with that. Just turn off these warnings
This makes sure these macros exist for every current usage.
proto.h contains a generated PERL_ARGS_ASSERT macro for every function. It asserts that each parameter that isn't allowed to be NULL actually isn't. These asserts are disabled when not DEBUGGING. But many compilers allow a compile-time assertion to be made for this situation, so we can add an extra measure of protection for free. And this gives hints to the compiler for optimizations when the asserts() aren't there.
This parameter is always non-null when built with MULTIPLICITY. At no runtime cost, make sure the compiler knows that.
11fe518
to
b484be9
Compare
I did the following to suppress most of the warnings:
I expect the remaining warnings could be resolved similarly. |
proto.h contains a generated PERL_ARGS_ASSERT macro for every function. It asserts that each parameter that isn't allowed to be NULL actually isn't (except there are no asserts for the aTHX parameter.)
These asserts are disabled when not DEBUGGING. But many compilers allow a compile-time assertion to be made for this situation, so we can add an extra measure of protection for free. And this gives hints to the compiler for optimizations when the asserts() aren't there.
In addition it adds a compile time assertion for the aTHX parameter.