Blocked Bytes

    /*
     * write, to the pipe connected to child's stdin, any input specified
     * after a % in the crontab entry.  while we copy, convert any
     * additional %'s to newlines.  when done, if some characters were
     * written and the last one wasn't a newline, write a newline.
     *
     * Note that if the input data won't fit into one pipe buffer (2K
     * or 4K on most BSD systems), and the child doesn't read its stdin,
     * we would block here.  thus we must fork again.
     */

This detail from /usr/src/usr.sbin/cron/do_command.c on OpenBSD seems worth some study, in particular whether we can make a parent process block as advertised. Because, uh, for science! Actionable insights! Etc.

The parent process will need to indicate whether it is blocked or not, probably by emitting periodic messages over time. For the child process we need something that ignores standard input, so probably a "source" program like ls(1). A "source" program here is anything that produces output, as opposed to something that modifies or filters input such as tr(1), and "sink" programs that save or stash input. sponge(1) of moreutils comes to mind as a sink. A program can act in some or even all of these roles.

    perl -E 'say "source"' |
    perl -ple 'y/a-z/A-Z/' |
    perl -e 'open O, ">", "out"; print O while readline'

    printf 'source\n' | tr a-z A-Z > out

By the way, it may be surprising when programs such as ssh(1) read from standard input.

    #!/bin/sh
    while read host; do
      printf 'working on %s...\n' "$host"
      ssh "$host" uptime
    done <<EOF
    localhost
    these lines are not seen by the outer loop
    which concerns people in the unix group
    EOF

uptime(1) is (at least on my system) not a "sink" program; it ignores standard input, which for the above was the remaining lines of input from the heredoc that was supposed to be a list of hosts to loop over. Change the uptime(1) to cat(1) and the missing lines should appear.

Test Code

    #include <sys/wait.h>
    #include <err.h>
    #include <stdio.h>
    #include <unistd.h>

    #define ALOT 1024
    #define CHILD 0
    #define PIPE_READER 0
    #define PIPE_WRITER 1
    // the limit for my OpenBSD 7.4 system (AMD64) is just below this
    #define TOPRINT 16385

    char buf[ALOT];

    int main(void) {
        int bpipe[2], status;
        pid_t pid;
        size_t amount = 0, counter = 0;
        if (pipe(bpipe) == -1) err(1, "pipe");
        pid = fork();
        if (pid < 0) err(1, "fork");
        if (pid == CHILD) {
            // wire up stdin to the pipe (only really needed if you
            // were to exec off to some other program)
            close(bpipe[PIPE_WRITER]);
            if (dup2(bpipe[PIPE_READER], STDIN_FILENO) != STDIN_FILENO)
                err(1, "dup");
            close(bpipe[PIPE_READER]);
            while (1) {
                fprintf(stderr, "child waiting... %zu\n", counter++);
                sleep(1);
            }
            errx(1, "fell off the loop??");
        }
        close(bpipe[PIPE_READER]);
        while (amount < TOPRINT) {
            write(bpipe[PIPE_WRITER], buf, ALOT);
            amount += ALOT;
        }
        while (1) {
            fprintf(stderr, "parent waiting... %zu\n", counter++);
            waitpid(pid, &status, WNOHANG);
            sleep(1);
        }
        errx(1, "child went away??");
    }

bufferblock.c

The limit is now a bit higher than when the cron comment was written. With more code you might write something that zeros in on the limit for a system. There may also be knobs available to tune this number up and down, for better or worse.

    $ make bufferblock && ./bufferblock
    cc -O2 -pipe     -o bufferblock bufferblock.c
    child waiting... 0
    child waiting... 1
    child waiting... 2
    child waiting... 3
    ^C

If the parent did not block then one would also see "parent waiting..." messages. So a blocked buffer can still be a problem if you have a parent process that ever pipes some amount of standard input (a lot, these days) to a client that never reads it.

/blog/2024/01/05/crontab-footguns.gmi

P.S. Masked signals can also impact pipes and buffers.

    < falsifian> phy1729: Mystery solved. My window manager ignores
                 SIGPIPE and passes that on to its children.
                 https://github.com/baskerville/bspwm/pull/1479
    < thrig> way to hound the baskervilles code