Skip to content

Conversation

khwilliamson
Copy link
Contributor

@khwilliamson khwilliamson commented Aug 24, 2025

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 set of changes does not require a perldelta entry.

@bulk88
Copy link
Contributor

bulk88 commented Aug 25, 2025

Doing this showed a small issue

util.c: In function ‘void Perl_set_context(void*)’:
perl.h:6412:26: warning: ‘nonnull’ argument ‘t’ compared to NULL [-Wnonnull-compare]
 6412 |             STMT_START { if (i) PERL_SET_LOCALE_CONTEXT(i); } STMT_END
      |                          ^~
util.c:3665:5: note: in expansion of macro ‘PERL_SET_NON_tTHX_CONTEXT’
 3665 |     PERL_SET_NON_tTHX_CONTEXT((PerlInterpreter *) t);

in which the compiler now catches that t can't be NULL. I don't know the best way to resolve this.

tried PTR2nat() or macro NUM2PTR(size_t,ptr)?

#  define PERL_SET_CONTEXT(t)                                               \
    STMT_START {                                                            \
        int _eC_;                                                           \
        if ((_eC_ = pthread_setspecific(PL_thr_key,                         \
                                        PL_current_context = (void *)(t)))) \
            Perl_croak_nocontext("panic: pthread_setspecific (%d) [%s:%d]", \
                                 _eC_, __FILE__, __LINE__);                 \
        PERL_SET_NON_tTHX_CONTEXT(t);                                       \
    } STMT_END
    /* In some Configurations there may be per-thread information that is
     * carried in a library instead of perl's tTHX structure.  This macro is to
     * be used to handle those when tTHX is changed.  Only locale handling is
     * currently known to be affected. */
#  define PERL_SET_NON_tTHX_CONTEXT(i)                                      \
            STMT_START { if (i) PERL_SET_LOCALE_CONTEXT(i); } STMT_END

Looks like a bug with POSIX Perl's PERL_SET_CONTEXT(t)''s internals which is macro PERL_SET_NON_tTHX_CONTEXT(i).

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 (my_perl) in the process, and I am using the Native OS's >= Win2000 Thread Pool feature with Perl.

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 call_sv(); but before I return control back to the OS, and send the kernel event to wakeup the root thread, I would be doing PERL_SET_CONTEXT((PerlInterpreter*)NULL); to detach a sleeping/frozen my_perl ptr from my current random OS TP thread, because I don't want that my_perl ptr to continue stay inside OS's Thread Local Storage for that totally random TID, random lifespan thread after I return control of my temporary thd back to the OS.

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 PERL_SET_CONTEXT(NULL); to work.

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 PERL_SET_CONTEXT() left compiled in, and not #if 0ed away in that XS module? Why is the Perl C API compatible with CPAN XS modules that ithread-aware but are not no-threads-aware and crash/hang/C syntax error on no-thread Perls??

You also wrote, or were the last person to clean it up.

6e13fe3

but the null test existed before the commit above, ill stop git blaming at this point.

diff --git a/locale.c b/locale.c
index 617119fdb8..20d49395fc 100644
--- a/locale.c
+++ b/locale.c
@@ -8538,19 +8538,38 @@ S_my_setlocale_debug_string_i(pTHX_
 #ifdef USE_PERL_SWITCH_LOCALE_CONTEXT
 
 void
-Perl_switch_locale_context()
+Perl_switch_locale_context(pTHX)
 {
     /* libc keeps per-thread locale status information in some configurations.
      * So, we can't just switch out aTHX to switch to a new thread.  libc has
      * to follow along.  This routine does that based on per-interpreter
-     * variables we keep just for this purpose */
-
-    /* Can't use pTHX, because we may be called from a place where that
-     * isn't available */
-    dTHX;
+     * variables we keep just for this purpose.
+     *
+     * There are two implementations where this is an issue.  For the other
+     * implementations, it doesn't matter because libc is using global values
+     * that all threads know about.
+     *
+     * The two implementations are where libc keeps thread-specific information
+     * on its own.  These are
+     *
+     * POSIX 2008:  The current locale is kept by libc as an object.  We save
+     *              a copy of that in the per-thread PL_cur_locale_obj, and so
+     *              this routine uses that copy to tell the thread it should be
+     *              operating with that object
+     * Windows thread-safe locales:  A given thread in Windows can be being run
+     *              with per-thread locales, or not.  When the thread context
+     *              changes, libc doesn't automatically know if the thread is
+     *              using per-thread locales, nor does it know what the new
+     *              thread's locale is.  We keep that information in the
+     *              per-thread variables:
+     *                  PL_controls_locale  indicates if this thread is using
+     *                                      per-thread locales or not
+     *                  PL_cur_LC_ALL       indicates what the the locale
+     *                                      should be if it is a per-thread
+     *                                      locale.
+     */
 
-    if (UNLIKELY(   aTHX == NULL
-                 || PL_veto_switch_non_tTHX_context
+    if (UNLIKELY(   PL_veto_switch_non_tTHX_context
                  || PL_phase == PERL_PHASE_CONSTRUCT))
     {
         return;

regen/embed.pl Outdated
}
else {
push @asserts, "PERL_ASSUME_NON_NULL($argname)";
push @attrs, "__attribute__nonnull__($n)";
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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

Copy link
Contributor

@Leont Leont Aug 29, 2025

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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?

As mauke points out the compiler won't always warn about this by default, but it also won't perform across compilation-unit (CU) checks, if f(NULLOK SV *sv) in a.c calls g(NN SV *sv) in b.c with that SV without checking for NULL the compiler won't have the information needed to complain.

Coverity can detect this type of issue across CUs, but I think it's only based on usage (it sees g() dereferences sv) rather than on these non-standard function attributes.

One big limitation with __attribute__nonnull__ is while we're marking parameters as not-null this doesn't indicate nullable for unmarked parameters - the compiler can't assume such parameters are nullable since not all code (think libraries) uses that attribute. clang's nullability attributes (_Nullable, _Nonnull, _Null_unspecified) let you provide that information, which may allow[3] the compiler to detect my f()/g() example, but we're not doing that.

None of these tools are perfect, we've seen Coverity go down strange logic chains (eg. assuming two contradictory conditions /cry), and IIRC codechecker[1] had some strange results too. (clang-tidy, cppcheck[2] don't support cross-CU checks), so I don't think we can remove (or optimize away) the asserts.

Also not all callers are perl itself, those asserts() aren't just for perl, they're also for XS and embedders.

For your PERL_SET_NON_tTHX_CONTEXT(i) warning, I suspect it would go away if you made that into an inline function with the parameter NULLOK, and the optimizer could still optimize away the check if the caller's value was nonnull.

[1] WebUI/cross-CU tool for the clang static analyzer
[2] one talk I saw on cppcheck suggested combining all of the CUs into one (all.c #includes every other .c) to perform cross-CU checks
[3] I don't know if it actually does, google felt the need to add the clang-SA checks mentioned in the C++now talk I linked to

Copy link
Member

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;
}

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

@tonycoz tonycoz Sep 2, 2025

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.)

@tonycoz
Copy link
Contributor

tonycoz commented Aug 28, 2025

Sort of related https://www.youtube.com/watch?v=3zQ4zw4GNV0 which talks about static analysis of the clang nullability attributes.

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
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.

Because of complications, this commit only does this for functions that
don't have a thread context.
@khwilliamson
Copy link
Contributor Author

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.

@khwilliamson khwilliamson changed the title Change ARGS_ASSERT to use ASSUME(), and __attribute__nonnull() Add __attribute__nonnull() for non-DEBUGGING buils Sep 3, 2025
@khwilliamson khwilliamson changed the title Add __attribute__nonnull() for non-DEBUGGING buils Add __attribute__nonnull__() for non-DEBUGGING buils Sep 3, 2025
@tonycoz
Copy link
Contributor

tonycoz commented Sep 4, 2025

Because of complications, this commit only does this for functions that
don't have a thread context.

Are you aware of the pTHX_1 .. pTHX_9 macros?

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 this pull request may close these issues.

6 participants