Marge Bot pushed to branch wip/marge_bot_batch_merge_job at Glasgow Haskell Compiler / GHC

Commits:

6 changed files:

Changes:

  • changelog.d/24450
    1
    +section: rts
    
    2
    +synopsis:
    
    3
    +  Fix a hang when a program is started with a standard file descriptor closed
    
    4
    +description:
    
    5
    +  A process started with stdout or stderr closed -- ``prog >&-`` from a shell
    
    6
    +  does exactly that -- left that descriptor number free for the runtime to
    
    7
    +  take for its own use.  The ``Handle`` for the stream, built later when the
    
    8
    +  program first wrote to it, then took over a descriptor belonging to the
    
    9
    +  runtime: the program wrote its output into the runtime's own plumbing and
    
    10
    +  hung, with no error reported anywhere.  The runtime now opens ``/dev/null``
    
    11
    +  onto whichever of the descriptors 0, 1 and 2 are closed when it starts, so
    
    12
    +  such a stream is connected to ``/dev/null`` instead.
    
    13
    +mrs: !16710
    
    14
    +issues: #24450

  • changelog.d/fix-parallel-gc-selector-race-27477
    1 1
     section: rts
    
    2 2
     synopsis: Fix a parallel GC race on weakly-ordered architectures (AArch64) that could crash or silently corrupt the heap
    
    3 3
     description:
    
    4
    -  On weakly-ordered architectures such as AArch64 programs using
    
    4
    +  On weakly-ordered architectures such as AArch64, programs
    
    5 5
       could crash during parallel garbage collection (segmentation fault
    
    6 6
       or ``internal error: eval_thunk_selector: strange selectee <n>``) or
    
    7 7
       continue running with silently corrupted heap data, due to a missing
    

  • rts/RtsStartup.c
    ... ... @@ -69,6 +69,10 @@
    69 69
     #if defined(HAVE_LOCALE_H)
    
    70 70
     #include <locale.h>
    
    71 71
     #endif
    
    72
    +#if !defined(mingw32_HOST_OS)
    
    73
    +#include <errno.h>
    
    74
    +#include <fcntl.h>
    
    75
    +#endif
    
    72 76
     
    
    73 77
     // Count of how many outstanding hs_init()s there have been.
    
    74 78
     static StgWord hs_init_count = 0;
    
    ... ... @@ -81,6 +85,10 @@ static int64_t __codePage = -1;
    81 85
     
    
    82 86
     static void flushStdHandles(void);
    
    83 87
     
    
    88
    +#if !defined(mingw32_HOST_OS)
    
    89
    +static void sanitiseStandardFds(void);
    
    90
    +#endif
    
    91
    +
    
    84 92
     /* -----------------------------------------------------------------------------
    
    85 93
        Initialise floating point unit on x86 (currently disabled; See Note
    
    86 94
        [x86 Floating point precision] in compiler/GHC/CmmToAsm/X86/Instr.hs)
    
    ... ... @@ -238,6 +246,82 @@ hs_init_with_rtsopts(int *argc, char **argv[])
    238 246
     
    
    239 247
     void init_ghc_hs_iface(void);
    
    240 248
     
    
    249
    +#if !defined(mingw32_HOST_OS)
    
    250
    +/* See Note [Standard file descriptors must be open]. */
    
    251
    +static void sanitiseStandardFds(void)
    
    252
    +{
    
    253
    +    // The access mode each descriptor would have had, had the process been
    
    254
    +    // started with it open.
    
    255
    +    static const int access_mode[3] = { O_RDONLY, O_WRONLY, O_WRONLY };
    
    256
    +
    
    257
    +    for (int fd = 0; fd <= 2; fd++) {
    
    258
    +        if (fcntl(fd, F_GETFD) != -1 || errno != EBADF) {
    
    259
    +            continue;           // already open
    
    260
    +        }
    
    261
    +
    
    262
    +        int nullfd;
    
    263
    +        do {
    
    264
    +            nullfd = open("/dev/null", access_mode[fd]);
    
    265
    +        } while (nullfd == -1 && errno == EINTR);
    
    266
    +
    
    267
    +        if (nullfd == -1) {
    
    268
    +            // No /dev/null to be had.  There is nothing better to do than
    
    269
    +            // carry on; the hazard the Note describes is then still there,
    
    270
    +            // but refusing to start would be worse.
    
    271
    +            return;
    
    272
    +        }
    
    273
    +        if (nullfd != fd) {
    
    274
    +            // open() hands out the lowest free descriptor, so this cannot
    
    275
    +            // happen unless something in this process is opening descriptors
    
    276
    +            // concurrently.  Do not shuffle its descriptor around.
    
    277
    +            close(nullfd);
    
    278
    +            return;
    
    279
    +        }
    
    280
    +    }
    
    281
    +}
    
    282
    +
    
    283
    +/*
    
    284
    + * Note [Standard file descriptors must be open]
    
    285
    + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    286
    + * A process may be started with any of file descriptors 0, 1 and 2 closed --
    
    287
    + * from a shell, `prog >&-` does exactly that.  Nothing then reserves those
    
    288
    + * numbers, and open(), pipe() and socket() hand out the lowest free
    
    289
    + * descriptor, so the next descriptor the runtime creates for itself takes one
    
    290
    + * of them: the ticker's pipe, or a capability's IO manager control pipe.
    
    291
    + *
    
    292
    + * Nothing warns anybody.  'GHC.Internal.IO.StdHandles.stdout' is a CAF, so it
    
    293
    + * is built the first time the program writes to stdout, which is long after
    
    294
    + * that has happened, and it builds a 'Handle' over descriptor 1 -- which now
    
    295
    + * belongs to the runtime.  The program then writes its output into the
    
    296
    + * runtime's own plumbing, and at exit closes it.
    
    297
    + *
    
    298
    + * What this looks like from the outside is a hang, because the descriptor the
    
    299
    + * Handle has taken over is the read end of a pipe.  It is a perfectly good
    
    300
    + * descriptor, so nothing reports an error: poll() for POLLOUT on it simply
    
    301
    + * never says yes, epoll_ctl() accepts a registration for it, and the thread
    
    302
    + * waiting to write blocks for ever.  See #24450, where this is `prog >&-`
    
    303
    + * hanging after main has finished.
    
    304
    + *
    
    305
    + * Closing those descriptors is legal, so the runtime cannot refuse them; and
    
    306
    + * moving only the descriptors the runtime opens would not help, since the
    
    307
    + * first file the program itself opens would land on the free number instead.
    
    308
    + * The fix is to leave no free number: open /dev/null onto whichever of 0, 1
    
    309
    + * and 2 are closed, before the runtime creates any descriptor of its own.
    
    310
    + * Other runtimes do the same, and for the same reason.
    
    311
    + *
    
    312
    + * A program run this way therefore finds stdout connected to /dev/null rather
    
    313
    + * than failing with EBADF on the first write.  That is the price of the fix,
    
    314
    + * and it is the behaviour such a program would get from most other language
    
    315
    + * runtimes.
    
    316
    + *
    
    317
    + * Each substitute is opened for the access the descriptor would normally have:
    
    318
    + * read-only for 0, write-only for 1 and 2.  Opening all three for both would
    
    319
    + * make the substitute something a real stdin or stdout is not, and writing to
    
    320
    + * 'stdin' or reading from 'stdout' would then quietly succeed instead of
    
    321
    + * failing with EBADF.
    
    322
    + */
    
    323
    +#endif
    
    324
    +
    
    241 325
     void
    
    242 326
     hs_init_ghc(int *argc, char **argv[], RtsConfig rts_config)
    
    243 327
     {
    
    ... ... @@ -252,6 +336,12 @@ hs_init_ghc(int *argc, char **argv[], RtsConfig rts_config)
    252 336
             stg_exit(1);
    
    253 337
         }
    
    254 338
     
    
    339
    +#if !defined(mingw32_HOST_OS)
    
    340
    +    /* Before anything here opens a file descriptor of its own.
    
    341
    +       See Note [Standard file descriptors must be open]. */
    
    342
    +    sanitiseStandardFds();
    
    343
    +#endif
    
    344
    +
    
    255 345
     #if defined(wasm32_HOST_ARCH)
    
    256 346
         char *pwd = getenv("PWD");
    
    257 347
         if (pwd != NULL) {
    

  • testsuite/tests/rts/T24450.hs
    1
    +{-# LANGUAGE ScopedTypeVariables #-}
    
    2
    +-- A process may be started with stdout closed -- `prog >&-` from a shell does
    
    3
    +-- exactly that.  Nothing then reserves descriptor 1, so the next descriptor
    
    4
    +-- the runtime opens for itself takes it, and the 'Handle' for stdout, built
    
    5
    +-- later when the program first writes, takes over a descriptor belonging to
    
    6
    +-- the runtime.  The write then goes into the runtime's own plumbing, and what
    
    7
    +-- this looks like from the outside is a hang.  See #24450 and
    
    8
    +-- Note [Standard file descriptors must be open] in rts/RtsStartup.c.
    
    9
    +--
    
    10
    +-- This test is run with its stdin and stdout closed, so what it has to say
    
    11
    +-- goes to stderr.  The four writes are the reproducer from the ticket and must
    
    12
    +-- simply finish.  The lines after them are what tell the two outcomes apart:
    
    13
    +-- the descriptors the runtime opens for itself are pipes, so finding a pipe on
    
    14
    +-- descriptor 0 or 1 means it was handed to the runtime.
    
    15
    +module Main (main) where
    
    16
    +
    
    17
    +import Control.Exception (IOException, try)
    
    18
    +import System.IO
    
    19
    +import System.Posix.Files (getFdStatus, isCharacterDevice, isNamedPipe)
    
    20
    +import System.Posix.Types (Fd (..))
    
    21
    +
    
    22
    +main :: IO ()
    
    23
    +main = do
    
    24
    +    hPutChar stderr 'A'     -- unbuffered
    
    25
    +    hPutChar stdout 'X'     -- buffered; this is the one that used to hang
    
    26
    +    hPutChar stderr 'Z'     -- unbuffered
    
    27
    +    hPutChar stderr '\n'    -- unbuffered
    
    28
    +    mapM_ (\fd -> hPutStrLn stderr =<< describeFd fd) [0, 1]
    
    29
    +
    
    30
    +describeFd :: Fd -> IO String
    
    31
    +describeFd fd = do
    
    32
    +    r <- try (getFdStatus fd)
    
    33
    +    return $ "fd " ++ show fd ++ ": " ++ case r of
    
    34
    +      Left (_ :: IOException) -> "closed"
    
    35
    +      Right st
    
    36
    +        -- What the fix arranges: /dev/null, opened onto the free number
    
    37
    +        -- before the runtime could take it.
    
    38
    +        | isCharacterDevice st -> "character device"
    
    39
    +        -- What #24450 is: the runtime's ticker or control pipe.
    
    40
    +        | isNamedPipe st       -> "pipe"
    
    41
    +        | otherwise            -> "something else"

  • testsuite/tests/rts/T24450.stderr
    1
    +AZ
    
    2
    +fd 0: character device
    
    3
    +fd 1: character device

  • testsuite/tests/rts/all.T
    ... ... @@ -408,6 +408,18 @@ test('T10590', [ ignore_stderr
    408 408
                    , req_process
    
    409 409
                    ], compile_and_run, [''])
    
    410 410
     
    
    411
    +# Run with stdin and stdout closed -- that is what leaves those descriptors
    
    412
    +# free for the runtime to take, which is the whole of #24450.  What the test
    
    413
    +# has to say therefore goes to stderr and is checked against T24450.stderr.
    
    414
    +test('T24450',
    
    415
    +     [ when(opsys('mingw32'), skip) # POSIX only
    
    416
    +     , js_skip # JS backend has its own runtime
    
    417
    +     # The wrapper below would close the stdout of the wasm runtime rather
    
    418
    +     # than that of the program running under it, which tests nothing.
    
    419
    +     , when(arch('wasm32'), skip)
    
    420
    +     , cmd_wrapper(lambda c: c + ' <&- >&-')
    
    421
    +     ], compile_and_run, [''])
    
    422
    +
    
    411 423
     # 20000 was easily enough to trigger the bug with 7.10
    
    412 424
     test('T10904', [ extra_run_opts('20000'), req_c ],
    
    413 425
                    compile_and_run, ['T10904lib.c'])