Password generator in C: the fix that is not one

Three hunks, not one, and running each three times is what exposes them. Version A prints the same password on every launch, because unseeded rand() starts from the same state every time. Version B seeds it and does vary, so the visible bug is genuinely fixed. It is still not secure: the seed is the clock, so anyone who knows roughly when it ran has only a handful of seeds to try. The runs below are a second apart for that reason, on both sides: a second is long enough for the clock seed to move, so Version A repeating itself cannot be put down to the runs being too close together.

Companion to
LLMs Can Write Code, but Cannot Read Your Mind
Files
password-rand.c
password-srand.c
Language
C
password-rand.c
/*
 * Version A of the C password example: unseeded rand().
 *
 * Companion to "LLMs Can Write Code, but Cannot Read Your Mind"
 * https://hed.am/briefs/llms-can-write-code-but-cannot-read-your-mind/
 *
 *   cc -O2 -Wall -Wextra -o password-rand password-rand.c
 *   ./password-rand
 *   ./password-rand        # same password, every time
 *
 * The C standard specifies that calling rand() without calling srand() first
 * behaves as though srand(1) had been called. The sequence is therefore the
 * same on every run, determined by the C library rather than by anything in
 * this file. Run the program twice and the same password appears twice. That
 * is the visible defect, and the easy one to catch. password-srand.c corrects
 * it and is still not secure.
 */
// Version A - looks fine, absolutely wrong for security
#include <stdio.h>
#include <stdlib.h>
#define PASSWORD_LENGTH 16
#define CHARSET "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
int main(void) {
    const char charset[] = CHARSET;
    const int charset_size = sizeof(charset) - 1; // exclude null terminator
    for (int i = 0; i < PASSWORD_LENGTH; i++) {
        int index = rand() % charset_size;
        putchar(charset[index]);
    }
    putchar('\n');
    return 0;
}
Version A — insecure
$ cc --version
Apple clang version 17.0.0 (clang-1700.6.4.2)
Target: arm64-apple-darwin25.2.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
$ cc -O2 -Wall -Wextra -o password-rand password-rand.c
$ sleep 1; ./password-rand
FZXC0wg0LvaJ6atJ
$ sleep 1; ./password-rand
FZXC0wg0LvaJ6atJ
$ sleep 1; ./password-rand
FZXC0wg0LvaJ6atJ

Three hunks separate the two files; they are identical everywhere the diff does not mark. Their documentation blocks differ as well, compared above.

Version A — insecure Version B — still insecure
Documentation block: 10 lines removed, 14 added
Unified diff of the documentation blocks of password-rand.c and password-srand.c
/*
 * Version A of the C password example: unseeded rand().
 * Version B of the C password example: the obvious fix, still not secure.
 *
 * Companion to "LLMs Can Write Code, but Cannot Read Your Mind"
 * https://hed.am/briefs/llms-can-write-code-but-cannot-read-your-mind/
 *
 *   cc -O2 -Wall -Wextra -o password-rand password-rand.c
 *   ./password-rand
 *   ./password-rand        # same password, every time
 *   cc -O2 -Wall -Wextra -o password-srand password-srand.c
 *   ./password-srand
 *   ./password-srand       # different password now
 *
 * The C standard specifies that calling rand() without calling srand() first
 * behaves as though srand(1) had been called. The sequence is therefore the
 * same on every run, determined by the C library rather than by anything in
 * this file. Run the program twice and the same password appears twice. That
 * is the visible defect, and the easy one to catch. password-srand.c corrects
 * it and is still not secure.
 * Seeding from the clock removes the repetition, which is the part you can
 * see. Two problems survive the repair. The seed is a timestamp in whole
 * seconds, so an attacker who knows which day a password was generated has
 * 86,400 seeds to search, and one who knows the minute has 60. Separately,
 * rand() is not a cryptographic generator at any seed: its internal state can
 * be recovered from its output, which is a documented property of the
 * function rather than a quirk of one implementation (CWE-338).
 *
 * The remedy is a different generator rather than a better seed: getentropy()
 * or arc4random_uniform() in C, secrets in Python. See password-secrets.py.
 */

Difference: 1 line removed, 4 lines added.

Unified diff of password-rand.c and password-srand.c
// Version A - looks fine, absolutely wrong for security
// Version B - the obvious fix, and still not secure
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define PASSWORD_LENGTH 16
#define CHARSET "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
int main(void) {
    const char charset[] = CHARSET;
    const int charset_size = sizeof(charset) - 1; // exclude null terminator
    srand((unsigned) time(NULL));
    for (int i = 0; i < PASSWORD_LENGTH; i++) {
        int index = rand() % charset_size;
        putchar(charset[index]);
6 unchanged lines
Version B — still insecure
$ cc --version
Apple clang version 17.0.0 (clang-1700.6.4.2)
Target: arm64-apple-darwin25.2.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
$ cc -O2 -Wall -Wextra -o password-srand password-srand.c
$ sleep 1; ./password-srand
7RlqzsGfaeE7gNYc
$ sleep 1; ./password-srand
Cq7spdmVlOeFbnHk
$ sleep 1; ./password-srand
HFTtePIKv83OWD0t

This file accompanies LLMs Can Write Code, but Cannot Read Your Mind, which is where the claim it checks is made.