Skip to content

Fix Ctrl-R segfault and comma-in-name high-score wipe - #11

Merged
Critlist merged 3 commits into
mainfrom
fix/s1-base
Oct 11, 2026
Merged

Critlist merged 3 commits into
mainfrom
fix/s1-base

Conversation

@Critlist

Copy link
Copy Markdown
Owner

Two S1 crash fixes from the bug sweep that are live-demo risks. One commit each.

Ctrl-R segfault (P3-02, 1982)

^R calls pline(0). SMALL is not defined in the real build (hackfoo.h defines it, but nothing includes it), so the #ifndef SMALL branch runs. It prints the last message and then falls through to index(NULL,'%') → SIGSEGV. The fix adds the missing return, plus a guarded "No message." path for SMALL builds.

Comma in player name wipes the high-score file (P3-04, 1982)

o → name=x,AAAA… puts a comma, the record's field separator, into the name. done() reads the record back with an unbounded fscanf("%[^,],%[^\n]"), so the next game end overflows death[20] and crashes after the record was reopened for writing. The shared high-score file is left at 0 bytes. The fix bounds the fields (%99[^,], %19[^\n], then discards the rest of the line) and replaces , with _ in name=.

Verification

Fresh plain and ASan/UBSan builds, tmux playtest:

  • Ctrl-R: shows "No message." at start and the last message after one. The game survives play, save and restore.
  • Comma name: stored as x_AAAA…. Three games in a row end normally, and all 5 original entries are kept.
  • Malformed record: a hand-written malformed record no longer crashes the game.
  • Sanitizers: 0 bytes of output.

Known follow-up: a name over 99 characters could still make the parser drop later entries. That's only reachable through the getlin overflow, which the stacked S1 PR fixes (P3-05).

🤖 Generated with Claude Code

Critlist and others added 2 commits October 10, 2026 23:54
The ^R command in hack.do.c calls pline(0), expecting pline() to
redisplay the previous message. The 1982 pline() handled that case
(print the saved ptr, or "No message." if none), but then fell through
without returning and ran index(line,'%') on the null line pointer,
crashing with SIGSEGV. The #ifndef SMALL guard compiles that branch out
entirely when SMALL is defined, leaving no null handling at all.

Add a return after the existing #ifndef SMALL "No message." branch so it
no longer falls through, and add a matching #else branch for SMALL builds
that prints the same "No message." text instead of dereferencing NULL.
No new behaviour is invented and pline() is not restructured.

Verified in tmux on plain Debug and ASan/UBSan builds: ^R before any
message prints "No message."; after a message ("Unknown command ','")
^R redisplays it; the game keeps running afterwards and ASan stderr is
empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The record file stores lines as 'level points name,death' and done()
reads them back with fscanf splitting on the first comma. The o command
accepted name=x,AAAA... of any length, so the tail of the name landed in
the death field, and the unbounded %[^\n] overflowed death[20] (and
%[^,] could overflow the 100-byte buf). The next game end then crashed in
done() after the record had been reopened for writing, leaving the shared
high-score file at 0 bytes.

Bound the fscanf fields to their buffers (%99[^,], %19[^\n]) and discard
the rest of an over-long line, so a malformed record cannot overflow.
Replace ',' with '_' in a name set via name= so new entries never
contain the field separator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Critlist
Critlist merged commit 6698276 into main Oct 11, 2026
5 checks passed
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.

1 participant