Live data from Hacker News

Why isn't memset() async-signal-safe?

boston.conman.org

81–90 of 98 posts

Re: Why isn't memset() async-signal-safe?

#81
post #68

Earlier quoted context omitted.

I'm also not an expert, but I think the bigger cost here is memory latency. Size correlates to, but scales differently than the switching cost of registers because it's zero sum. A register saved is a register waited on, twice. There's also the cost of decoding and executing the instructions to store / restore the register values on both ends.

I don't get it, you have to save and restore all the state on context switches either way. The only question is whether you're doing it with a generic instruction or through some other more specialized instructions. It's not a question of whether they should be saved and restored at all. That said, I'm confused why you replied to my comment above, since it's totally off-topic. This thread chain was asking how much st…

> I don't get it, you have to save and restore all the state on context switches either way.

Actually, no, you don't, at least not on _all_ context switches. What you have to save is what the switched-to routine will overwrite. For something like an interrupt handler (where latency often matters very much), if you know it will only modify, for example, eflags and EAX, then you only need save on entry and restore on exit from the handler eflags and EAX. The registers that are not modified remain identical from entry to exit and time is saved by not pushing/popping them needlessly.

Re: Why isn't memset() async-signal-safe?

#82

This article seems flat out wrong. Since the direction flag is correctly saved and restored, everything is cool. When some code is interrupted and the signal or interrupt handler calls memmove, that memmove will set up the direction flag for itself correctly. Its entire execution is nested within the handler. If it is interrupted by a nested interrupt, that nested one will restore the flag. Now if an implementation o…

> The flag can have arbitrary value in on entry to memcpy in an ordinary situation not involving threads or signals. It can not have arbitrary value on entry. x86-64 ABI mandates that the flag is cleared before any function is called (3.2.1 Registers and the Stack Frame: "The direction flag in the %eflags register must be clear on function entry, and on function return.")

OK there we go, then. A compiler that conforms to the ABI generates code which clears the flag, if necessary. Interrupts preserve the flag. Thus, the flag should not be surprisingly set on entry into memcpy. If so, there is a bug.

Re: Why isn't memset() async-signal-safe?

#83
post #80
post #77

Earlier quoted context omitted.

POSIX doesn't guarantee that memset is async-signal-safe, that is clear enough, and flags like this are even a reasonable rationale. But it sounds like post-2008 memset is and should be async-signal-safe on Linux. Are there relevant systems on which it isn't? IME it's impractical to write code that will work on all POSIX-compliant systems as the standard leaves too much undefined and there is no testsuite that will l…

Welcome to POSIX. As for GNU/Linux, can you assure memset works that way in all hardware platforms supported by Linux? Even it does, no one intending to write portable UNIX code can rely on it anyway.

> As for GNU/Linux, can you assure memset works that way in all hardware platforms supported by Linux?

I don't know, that's why I'm asking.

I'm not going to try to write code portable to all theoretically possible unices - I doubt anyone has managed that for nontrivial programs, and it's too hard to tell. Hell, I'm content to exclude a fair few systems I know exist (CHAR_BIT != 8, non-IEEE FP). If it's supported on all the platforms I've heard of I'll do it - it's just not practical to hope to be POSIX-complient in a language-lawyer sense.

Re: Why isn't memset() async-signal-safe?

#84
post #83
post #80

Earlier quoted context omitted.

Welcome to POSIX. As for GNU/Linux, can you assure memset works that way in all hardware platforms supported by Linux? Even it does, no one intending to write portable UNIX code can rely on it anyway.

> As for GNU/Linux, can you assure memset works that way in all hardware platforms supported by Linux? I don't know, that's why I'm asking. I'm not going to try to write code portable to all theoretically possible unices - I doubt anyone has managed that for nontrivial programs, and it's too hard to tell. Hell, I'm content to exclude a fair few systems I know exist (CHAR_BIT != 8, non-IEEE FP). If it's supported on a…

> I doubt anyone has managed that for nontrivial programs, and it's too hard to tell.

In the early 2000's, the company I worked for was deploying UNIX based software for GNU/Linux, FreeBSD, HP-UX, Aix, Solaris and Windows (yes Windows, not a typo).

Re: Why isn't memset() async-signal-safe?

#85
Making sure the Direction flag is clear on entry and return is a somewhat hidden requirement of stdcall (Windows calling convention), but it didn't crash if you set it in WindowProc and forgot to clear it until Windows XP.

More recently, Vista had (accidentily?) 16-byte aligned stacks when using OpenMP with MingW, but the exact same binary crashes on Windows 10 when it tries to use unaligned SSE instructions.

Re: Why isn't memset() async-signal-safe?

#86

Earlier quoted context omitted.

> The flag can have arbitrary value in on entry to memcpy in an ordinary situation not involving threads or signals. It can not have arbitrary value on entry. x86-64 ABI mandates that the flag is cleared before any function is called (3.2.1 Registers and the Stack Frame: "The direction flag in the %eflags register must be clear on function entry, and on function return.")

OK there we go, then. A compiler that conforms to the ABI generates code which clears the flag, if necessary. Interrupts preserve the flag. Thus, the flag should not be surprisingly set on entry into memcpy. If so, there is a bug.

> A compiler that conforms to the ABI generates code which clears the flag, if necessary.

I'm sorry, I do not follow. The conforming compiler expects that the functions are called with the clear flag. The non-conforming kernel on the other hand does not clear the flag before calling the signal handler which breaks it.

Re: Why isn't memset() async-signal-safe?

#87
post #84
post #83

Earlier quoted context omitted.

> As for GNU/Linux, can you assure memset works that way in all hardware platforms supported by Linux? I don't know, that's why I'm asking. I'm not going to try to write code portable to all theoretically possible unices - I doubt anyone has managed that for nontrivial programs, and it's too hard to tell. Hell, I'm content to exclude a fair few systems I know exist (CHAR_BIT != 8, non-IEEE FP). If it's supported on a…

> I doubt anyone has managed that for nontrivial programs, and it's too hard to tell. In the early 2000's, the company I worked for was deploying UNIX based software for GNU/Linux, FreeBSD, HP-UX, Aix, Solaris and Windows (yes Windows, not a typo).

Sure, writing a program that works on 6 actually existing unices with test instances (or even just documentation) is relatively easy. Writing one for all possible unices including some that don't exist at the time of writing is a lot harder.

Re: Why isn't memset() async-signal-safe?

#88
post #75

Earlier quoted context omitted.

At the cost of extra bandwidth and interrupt latency.

Yes, but only marginally if they changed the size only 1 out of 1000 times.

Adding complexity (that could result in bugs) in the hope to prevent bugs by bad usage of the interface is usually a bad idea.

Re: Why isn't memset() async-signal-safe?

#89
post #87
post #84

Earlier quoted context omitted.

> I doubt anyone has managed that for nontrivial programs, and it's too hard to tell. In the early 2000's, the company I worked for was deploying UNIX based software for GNU/Linux, FreeBSD, HP-UX, Aix, Solaris and Windows (yes Windows, not a typo).

Sure, writing a program that works on 6 actually existing unices with test instances (or even just documentation) is relatively easy. Writing one for all possible unices including some that don't exist at the time of writing is a lot harder.

Those 6 were already enough to learn how "portable" POSIX actually is.

Re: Why isn't memset() async-signal-safe?

#90
post #79
post #78

What I've never seen discussions on signal safety address is: why must an OS have signals to begin with?

This (signal) is the de-facto communication mechanism to notify a program that an unexpected event just occurred (typically, a SEGV). This mechanism needs to interrupt the normal program flow (because you can not restart an invalid access most of the time), possibly interrupting a C-library call (such as malloc, reason why malloc is not async-signal-safe generally). What would you do instead ? On Windows systems, the…

> This is probably not such a better method.

It actually really, really is. At least from a technical perspective on x64. Having language-level lexical scoping of system-level exceptions is incredibly useful and once you've grokked how NT's trap handler, PE .xdata sections, prologues and epilogues all work in concert, signals just seem barbaric.

Here's an example trapping an access violation:

    //
    // Prefault the page.
    //

    TRY_MAPPED_MEMORY_OP {
        TraceStore->Rtl->PrefaultPages(PrefaultMemoryMap->NextAddress, 1);
    } CATCH_EXCEPTION_ACCESS_VIOLATION {

        //
        // This will happen if servicing the prefault off-core has taken longer
        // for the originating core (the one that submitted the prefault work)
        // to consume the entire memory map, then *another* memory map, which
        // will retire the memory map backing this prefault address, which
        // results in the address being invalidated, which results in an access
        // violation when we try and read/prefault it from the thread pool.
        //

        TraceStore->Stats->AccessViolationsEncounteredDuringAsyncPrefault++;
    }
Or an alignment fault:

    FORCEINLINE
    VOID
    StoreXmm(
        _In_ XMMWORD *Destination,
        _In_ XMMWORD  Source
        )
    {
        TRY_SSE42_ALIGNED {
    
            _mm_store_si128(Destination, Source);
    
        } CATCH_EXCEPTION_ACCESS_VIOLATION {
    
            _mm_storeu_si128(Destination, Source);
        }
    }
Or an illegal instruction:

    FORCEINLINE
    VOID
    StoreYmmFallbackXmm(
        _In_ PYMMWORD Destination,
        _In_ PXMMWORD Destination128Low,
        _In_ PXMMWORD Destination128High,
        _In_ YMMWORD  Source,
        _In_ XMMWORD  Source128Low,
        _In_ XMMWORD  Source128High
        )
    {
        TRY_AVX {
    
            TRY_AVX_ALIGNED {
    
                _mm256_store_si256(Destination, Source);
    
            } CATCH_EXCEPTION_ILLEGAL_INSTRUCTION {
    
                Store2Xmm(
                    Destination128Low,
                    Destination128High,
                    Source128Low,
                    Source128High
                );
    
            }
    
        } CATCH_EXCEPTION_ACCESS_VIOLATION {
    
            _mm256_storeu_si256(Destination, Source);
        }
Or a page fault that has occurred against a memory map backed file (because, say, the underlying network drive has been disconnected):

    TRY_MAPPED_MEMORY_OP {

        //
        // Copy the caller's address range structure over.
        //

        __movsq((PDWORD64)NewAddressRange,
                (PDWORD64)AddressRange,
                sizeof(*NewAddressRange) >> 3);

        //
        // If there's an existing address range set, update its ValidTo
        // timestamp.
        //

        if (TraceStore->AddressRange) {
            TraceStore->AddressRange->Timestamp.ValidTo.QuadPart = (
                Timestamp.QuadPart
            );
        }

        //
        // Update the trace store's address range pointer.
        //

        TraceStore->AddressRange = NewAddressRange;

    } CATCH_STATUS_IN_PAGE_ERROR {

        //
        // We'll leak the address range we just allocated here, but a copy
        // failure is indicative of much bigger issues (drive full, network
        // map disappearing) than leaking ~32 bytes, so we don't attempt to
        // roll back the allocation.
        //

        return FALSE;
    }
Relevant macro definitions:

    #define TRY_AVX __try
    #define TRY_AVX_ALIGNED __try
    #define TRY_AVX_UNALIGNED __try
    
    #define TRY_SSE42 __try
    #define TRY_SSE42_ALIGNED __try
    #define TRY_SSE42_UNALIGNED __try
    
    #define TRY_MAPPED_MEMORY_OP __try
    
    #define CATCH_EXCEPTION_ILLEGAL_INSTRUCTION __except(     \
        GetExceptionCode() == EXCEPTION_ILLEGAL_INSTRUCTION ? \
            EXCEPTION_EXECUTE_HANDLER :                       \
            EXCEPTION_CONTINUE_SEARCH                         \
        )
    
    #define CATCH_EXCEPTION_ACCESS_VIOLATION __except(     \
        GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? \
            EXCEPTION_EXECUTE_HANDLER :                    \
            EXCEPTION_CONTINUE_SEARCH                      \
        )
    
    #define CATCH_STATUS_IN_PAGE_ERROR __except(     \
        GetExceptionCode() == STATUS_IN_PAGE_ERROR ? \
            EXCEPTION_EXECUTE_HANDLER :              \
            EXCEPTION_CONTINUE_SEARCH                \
        )
Post reply on HN