[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1034636: unblock: sgt-puzzles/20230122.806ae71-2



Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
Usertags: unblock
X-Debbugs-Cc: sgt-puzzles@packages.debian.org
Control: affects -1 + src:sgt-puzzles

Please unblock package sgt-puzzles

The changes for this look large, but most patches only change a few
lines.  Combined diffstat for the new patches is:

 blackbox.c         |    2 +
 bridges.c          |   38 +++++++++++++++++++------
 cube.c             |   36 +++++++++++++++++++++---
 dominosa.c         |    6 ++--
 filling.c          |    2 +
 flood.c            |   45 +++++++++++++++++++-----------
 galaxies.c         |   15 ++++++----
 inertia.c          |    8 ++---
 keen.c             |   11 ++++++-
 latin.c            |   47 ++++++++++++++++++++-----------
 latin.h            |    9 ++++--
 loopy.c            |   16 ++++++++--
 magnets.c          |    4 ++
 map.c              |    2 -
 midend.c           |   14 ++++++---
 mines.c            |   22 ++++++++++++++
 mosaic.c           |   22 +++++++-------
 net.c              |    3 ++
 netslide.c         |    4 +-
 palisade.c         |   36 ++++++++++--------------
 pattern.c          |    5 ++-
 pearl.c            |   78 +++++++++++++++++++++++++++++++++++++++--------------
 pegs.c             |   11 ++++++-
 random.c           |    4 +-
 range.c            |    5 ++-
 rect.c             |    2 +
 slant.c            |    2 +
 solo.c             |    2 -
 tents.c            |   22 ++++++++++++--
 tracks.c           |    9 +++---
 twiddle.c          |    8 +++--
 undead.c           |    6 ++--
 unequal.c          |   44 ++++++++++++++++-------------
 unfinished/group.c |   15 +++++-----
 unruly.c           |    2 +
 untangle.c         |    4 ++
 36 files changed, 383 insertions(+), 178 deletions(-)

[ Reason ]
Fix security flaws in game loading.  Also fix one non-security
crasher.

[ Impact ]
Loading a crafted game description or save file could have security
impacts up to and including arbitrary code execution.

[ Tests ]
There are no automated tests for this package.

I have tested that:
1. Each puzzle is playable to completion.
2. Each puzzle can generate and start a game from each of its
size/shape/difficulty presets.
3. Each puzzle can generate 10 random game save files and load them
without error.

[ Risks ]
As I have cherry-picked fixes from upstream, there is a possibility
that I missed some dependency for the fixes.  However, I did review
all the upstream changes.

This is a leaf package.

[ Checklist ]
  [X] all changes are documented in the d/changelog
  [X] I reviewed all changes and I approve them
  [X] attach debdiff against the package in testing

[ Other info ]

unblock sgt-puzzles/20230122.806ae71-2
diff -Nru sgt-puzzles-20230122.806ae71/debian/changelog sgt-puzzles-20230122.806ae71/debian/changelog
--- sgt-puzzles-20230122.806ae71/debian/changelog	2023-01-24 03:09:26.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/changelog	2023-04-16 21:19:11.000000000 +0200
@@ -1,3 +1,69 @@
+sgt-puzzles (20230122.806ae71-2) unstable; urgency=medium
+
+  * Fix various security issues in game loading (Closes: #1034190):
+    - Black Box: reject negative ball counts in game_params.
+    - Add validate_params bounds checks in a few more games.
+    - Don't allow Bridges games with < 2 islands
+    - Forbid moves that fill with the current colour in Flood
+    - Cleanly reject ill-formed solve moves in Flood
+    - Don't segfault on premature solve moves in Mines
+    - Limit number of mines in Mines game description
+    - Validate the number of pegs and holes in a Pegs game ID
+    - Mines: forbid moves that flag or unflag an exposed square
+    - Mines: Don't check if the player has won if they've already lost
+    - Avoid invalid moves when solving Tracks
+    - Fix move validation in Netslide
+    - Tighten validation of Tents game descriptions
+    - Dominosa: require the two halves of a domino to be adjacent
+    - Forbid lines off the grid in Pearl
+    - Tolerate incorrect solutions in Inertia
+    - Palisade: replace dfs_dsf() with a simple iteration.
+    - latin_solver_alloc: handle clashing numbers in input grid.
+    - Pearl: fix assertion failure on bad puzzle.
+    - Pearl: fix bounds check in previous commit.
+    - Unequal: Don't insist that solve moves must actually solve
+    - Range: Don't fail an assertion on an all-black board
+    - Limit width and height to SHRT_MAX in Mines
+    - Mines: Add assertions to range-check conversions to short
+    - Unequal: fix sense error in latin_solver_alloc fix.
+    - Forbid impossible moves in Bridges
+    - Forbid game descriptions with joined islands in Bridges
+    - Check state is valid at the end of a move in Pearl
+    - Cleanly reject more ill-formed solve moves in Flood
+    - Don't allow moves that change the constraints in Unequal
+    - Fix memory leaks in Keen's validate_desc()
+    - Remember to free the actual_board array in Mosaic
+    - Don't leak grids in Loopy's validate_desc()
+    - Remember to free the to_draw member from Net's drawstate
+    - Undead: check the return value of sscanf() in execute_move()
+    - Don't leak duplicate edges in Untangle
+    - Remember to free the numcolours array from Pattern's drawstate
+    - Free new game_state properly in Mosaic's execute_move()
+    - Twiddle: don't read off the end of parameter strings ending 'm'
+    - Loopy: free the grid description string if it's invalid
+    - Mosaic: don't duplicate the description being validated
+    - Avoid division by zero in Cube grid-size checks
+    - Validate that save file values are ASCII (mostly)
+    - More validation of solve moves in Flood
+    - Make sure that moves in Flood use only valid colours
+    - Tighten grid-size limit in Mines
+    - Tracks: set drag_s{x,y} even if starting off-grid
+    - Undead: be a bit more careful about sprintf buffer sizes
+    - Fix memory leak in midend_game_id_int()
+    - Flood: don't read off the end of some parameter strings
+    - Be more careful with type of left operand of <<
+    - Map: reduce maximum size
+    - Correctly handle some short save files
+    - Inertia: insist that solutions must be non-empty
+    - Galaxies: fix recursion depth limit in solver.
+    - Correct a range check in Magnets' layout verification
+    - Magnets: add a check that magnets don't wrap between lines
+    - Net: assert that cx and cy are in range in compute_active()
+    - Don't allow zero clues in Pattern
+  * Solo: cope with pencil marks when tilesize == 1 (Closes: #905852)
+
+ -- Ben Hutchings <benh@debian.org>  Sun, 16 Apr 2023 21:19:11 +0200
+
 sgt-puzzles (20230122.806ae71-1) unstable; urgency=medium
 
   * New upstream version:
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0001-Black-Box-reject-negative-ball-counts-in-game_params.patch sgt-puzzles-20230122.806ae71/debian/patches/0001-Black-Box-reject-negative-ball-counts-in-game_params.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0001-Black-Box-reject-negative-ball-counts-in-game_params.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0001-Black-Box-reject-negative-ball-counts-in-game_params.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,36 @@
+From: Simon Tatham <anakin@pobox.com>
+Date: Sun, 22 Jan 2023 08:54:06 +0000
+Subject: [PATCH 001/159] Black Box: reject negative ball counts in
+ game_params.
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=5cac6a09c4db2b7e05c3e8dfd8920e2cdd1b8b03
+Bug-Debian: https://bugs.debian.org/1034190
+
+You can inject one via a game desc string such as "10x10M5m-1", and
+it's clearly silly.
+
+_Zero_ balls, on the other hand, are a perfectly fine number: there's
+nothing incoherent about a BB puzzle in which the possible numbers of
+balls vary from (say) 0 to 5 inclusive, so that part of the challenge
+is to work out as efficiently as possible whether there are even any
+balls at all.
+
+(We only need to check minballs, because once we know minballs >= 0,
+the subsequent check ensures that maxballs >= minballs, and hence, by
+transitivity, maxballs >= 0 too.)
+---
+ blackbox.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/blackbox.c b/blackbox.c
+index c93678a5..22ef4cb8 100644
+--- a/blackbox.c
++++ b/blackbox.c
+@@ -192,6 +192,8 @@ static const char *validate_params(const game_params *params, bool full)
+      * types, and could be worked around if required. */
+     if (params->w > 255 || params->h > 255)
+         return "Widths and heights greater than 255 are not supported";
++    if (params->minballs < 0)
++        return "Negative number of balls";
+     if (params->minballs > params->maxballs)
+         return "Minimum number of balls may not be greater than maximum";
+     if (params->minballs >= params->w * params->h)
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0002-Add-validate_params-bounds-checks-in-a-few-more-game.patch sgt-puzzles-20230122.806ae71/debian/patches/0002-Add-validate_params-bounds-checks-in-a-few-more-game.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0002-Add-validate_params-bounds-checks-in-a-few-more-game.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0002-Add-validate_params-bounds-checks-in-a-few-more-game.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,137 @@
+From: Simon Tatham <anakin@pobox.com>
+Date: Sun, 22 Jan 2023 09:30:57 +0000
+Subject: [PATCH 002/159] Add validate_params bounds checks in a few more
+ games.
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=b907e278751b740da7b9dc00c0cbdb93e7498919
+Bug-Debian: https://bugs.debian.org/1034190
+
+Ben tells me that his recent work in this area was entirely driven by
+fuzzing: he added bounds checks in validate_params when the fuzzer had
+managed to prove that the lack of them allowed something buggy to
+happen.
+
+It seemed worth doing an eyeball-review pass to complement that
+strategy, so in this commit I've gone through and added a few more
+checks that restrict the area of the grid to be less than INT_MAX.
+
+Notable in this commit: cube.c had to do something complicated because
+in the triangular-grid modes the area isn't calculated as easily as
+w*h, and Range's existing check that w+h-1 < SCHAR_MAX is sufficient
+to rule out w*h being overlarge _but_ should be done before w*h is
+ever computed.
+---
+ cube.c    | 24 ++++++++++++++++++++++++
+ filling.c |  2 ++
+ range.c   |  2 +-
+ rect.c    |  2 ++
+ slant.c   |  2 ++
+ unruly.c  |  2 ++
+ 6 files changed, 33 insertions(+), 1 deletion(-)
+
+diff --git a/cube.c b/cube.c
+index 32b7343f..21df46d3 100644
+--- a/cube.c
++++ b/cube.c
+@@ -542,12 +542,36 @@ static const char *validate_params(const game_params *params, bool full)
+     if (params->solid < 0 || params->solid >= lenof(solids))
+ 	return "Unrecognised solid type";
+ 
++    if (params->d1 < 0 || params->d2 < 0)
++        return "Grid dimensions may not be negative";
++
+     if (solids[params->solid]->order == 4) {
+ 	if (params->d1 <= 1 || params->d2 <= 1)
+ 	    return "Both grid dimensions must be greater than one";
++        if (params->d2 > INT_MAX / params->d1)
++	    return "Grid area must not be unreasonably large";
+     } else {
+ 	if (params->d1 <= 0 && params->d2 <= 0)
+ 	    return "At least one grid dimension must be greater than zero";
++
++        /*
++         * Check whether d1^2 + d2^2 + 4 d1 d2 > INT_MAX, without overflow:
++         *
++         * First check d1^2 doesn't overflow by itself.
++         *
++         * Then check d2^2 doesn't exceed the remaining space between
++         * d1^2 and INT_MAX.
++         *
++         * If that's all OK then we know both d1 and d2 are
++         * individually less than the square root of INT_MAX, so we
++         * can safely multiply them and compare against the
++         * _remaining_ space.
++         */
++        if ((params->d1 > INT_MAX / params->d1) ||
++            (params->d2 > (INT_MAX - params->d1*params->d1) / params->d2) ||
++            (params->d1*params->d2 > (INT_MAX - params->d1*params->d1 -
++                                      params->d2*params->d2) / params->d2))
++	    return "Grid area must not be unreasonably large";
+     }
+ 
+     for (i = 0; i < 4; i++)
+diff --git a/filling.c b/filling.c
+index 92939a14..7dec6560 100644
+--- a/filling.c
++++ b/filling.c
+@@ -188,6 +188,8 @@ static const char *validate_params(const game_params *params, bool full)
+ {
+     if (params->w < 1) return "Width must be at least one";
+     if (params->h < 1) return "Height must be at least one";
++    if (params->w > INT_MAX / params->h)
++        return "Width times height must not be unreasonably large";
+ 
+     return NULL;
+ }
+diff --git a/range.c b/range.c
+index d6d17ebb..a845b824 100644
+--- a/range.c
++++ b/range.c
+@@ -911,8 +911,8 @@ static const char *validate_params(const game_params *params, bool full)
+     int const w = params->w, h = params->h;
+     if (w < 1) return "Error: width is less than 1";
+     if (h < 1) return "Error: height is less than 1";
++    if (w > SCHAR_MAX - (h - 1)) return "Error: w + h is too big";
+     if (w * h < 1) return "Error: size is less than 1";
+-    if (w + h - 1 > SCHAR_MAX) return "Error: w + h is too big";
+     /* I might be unable to store clues in my puzzle_size *grid; */
+     if (full) {
+         if (w == 2 && h == 2) return "Error: can't create 2x2 puzzles";
+diff --git a/rect.c b/rect.c
+index e2141fda..51150184 100644
+--- a/rect.c
++++ b/rect.c
+@@ -218,6 +218,8 @@ static const char *validate_params(const game_params *params, bool full)
+ {
+     if (params->w <= 0 || params->h <= 0)
+ 	return "Width and height must both be greater than zero";
++    if (params->w > INT_MAX / params->h)
++        return "Width times height must not be unreasonably large";
+     if (params->w*params->h < 2)
+ 	return "Grid area must be greater than one";
+     if (params->expandfactor < 0.0F)
+diff --git a/slant.c b/slant.c
+index 25af496d..f7070d79 100644
+--- a/slant.c
++++ b/slant.c
+@@ -226,6 +226,8 @@ static const char *validate_params(const game_params *params, bool full)
+ 
+     if (params->w < 2 || params->h < 2)
+ 	return "Width and height must both be at least two";
++    if (params->w > INT_MAX / params->h)
++        return "Width times height must not be unreasonably large";
+ 
+     return NULL;
+ }
+diff --git a/unruly.c b/unruly.c
+index 7a5512ea..0a9403eb 100644
+--- a/unruly.c
++++ b/unruly.c
+@@ -286,6 +286,8 @@ static const char *validate_params(const game_params *params, bool full)
+         return "Width and height must both be even";
+     if (params->w2 < 6 || params->h2 < 6)
+         return "Width and height must be at least 6";
++    if (params->w2 > INT_MAX / params->h2)
++        return "Width times height must not be unreasonably large";
+     if (params->unique) {
+         static const long A177790[] = {
+             /*
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0006-Don-t-allow-Bridges-games-with-2-islands.patch sgt-puzzles-20230122.806ae71/debian/patches/0006-Don-t-allow-Bridges-games-with-2-islands.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0006-Don-t-allow-Bridges-games-with-2-islands.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0006-Don-t-allow-Bridges-games-with-2-islands.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,58 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 28 Jan 2023 00:45:38 +0000
+Subject: [PATCH 006/159] Don't allow Bridges games with < 2 islands
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=a98ac4bb428ab5c1ff665aa1def6cc14d02a4e19
+Bug-Debian: https://bugs.debian.org/1034190
+
+Technically, a game with no islands is always solved, but it causes a
+null-pointer dereference at startup because there's nowhere to put the
+cursor.  Games with one island are always insoluble because the island
+must have at least one bridge and there's nowhere for it to go.  So
+the minimum playable game has two islands.
+
+To demonstrate the segfault, try loading this save file:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :7:Bridges
+PARAMS  :1:3
+CPARAMS :1:3
+DESC    :1:i
+NSTATES :1:1
+STATEPOS:1:1
+---
+ bridges.c | 8 +++++---
+ 1 file changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/bridges.c b/bridges.c
+index 35cdcca6..510af443 100644
+--- a/bridges.c
++++ b/bridges.c
+@@ -2007,15 +2007,15 @@ static char *new_game_desc(const game_params *params, random_state *rs,
+ 
+ static const char *validate_desc(const game_params *params, const char *desc)
+ {
+-    int i, wh = params->w * params->h;
++    int i, wh = params->w * params->h, nislands = 0;
+ 
+     for (i = 0; i < wh; i++) {
+         if (*desc >= '1' && *desc <= '9')
+-            /* OK */;
++            nislands++;
+         else if (*desc >= 'a' && *desc <= 'z')
+             i += *desc - 'a'; /* plus the i++ */
+         else if (*desc >= 'A' && *desc <= 'G')
+-            /* OK */;
++            nislands++;
+         else if (!*desc)
+             return "Game description shorter than expected";
+         else
+@@ -2024,6 +2024,8 @@ static const char *validate_desc(const game_params *params, const char *desc)
+     }
+     if (*desc || i > wh)
+         return "Game description longer than expected";
++    if (nislands < 2)
++        return "Game description has too few islands";
+ 
+     return NULL;
+ }
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0007-Forbid-moves-that-fill-with-the-current-colour-in-Fl.patch sgt-puzzles-20230122.806ae71/debian/patches/0007-Forbid-moves-that-fill-with-the-current-colour-in-Fl.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0007-Forbid-moves-that-fill-with-the-current-colour-in-Fl.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0007-Forbid-moves-that-fill-with-the-current-colour-in-Fl.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,38 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 28 Jan 2023 18:49:47 +0000
+Subject: [PATCH 007/159] Forbid moves that fill with the current colour in
+ Flood
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=eb1ae3f3d041f9ff0c11b04613a695be11bda706
+Bug-Debian: https://bugs.debian.org/1034190
+
+This avoids an assertion failure, "oldcolour != newcolour" in fill(),
+by catching it it execute_move().  As far as I know this couldn't be
+triggered from the UI, but it could be demonstrated with this save
+file:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :5:Flood
+PARAMS  :1:3
+CPARAMS :1:3
+DESC    :12:231353400,11
+NSTATES :1:3
+STATEPOS:1:3
+MOVE    :2:M3
+MOVE    :2:M3
+---
+ flood.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/flood.c b/flood.c
+index 1b0fa6e8..08410ba2 100644
+--- a/flood.c
++++ b/flood.c
+@@ -887,6 +887,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+     if (move[0] == 'M' &&
+         sscanf(move+1, "%d", &c) == 1 &&
+         c >= 0 &&
++        c != state->grid[FILLY * state->w + FILLX] &&
+         !state->complete) {
+         int *queue = snewn(state->w * state->h, int);
+ 	ret = dup_game(state);
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0008-Cleanly-reject-ill-formed-solve-moves-in-Flood.patch sgt-puzzles-20230122.806ae71/debian/patches/0008-Cleanly-reject-ill-formed-solve-moves-in-Flood.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0008-Cleanly-reject-ill-formed-solve-moves-in-Flood.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0008-Cleanly-reject-ill-formed-solve-moves-in-Flood.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,43 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 28 Jan 2023 19:06:24 +0000
+Subject: [PATCH 008/159] Cleanly reject ill-formed solve moves in Flood
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=e4112b322e299a461ddc46daee741c73733e186d
+Bug-Debian: https://bugs.debian.org/1034190
+
+A solve move containing characters other than digits and commas would
+cause an assertion failure, "*p == ','", in execute_move().  Such a move
+can't as far as I know be generated in play, but can be read from a
+corrupt save file.
+
+Here's a sample of such a save file:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :5:Flood
+PARAMS  :7:3x3c6m5
+CPARAMS :7:3x3c6m5
+DESC    :12:403011503,10
+NSTATES :1:2
+STATEPOS:1:2
+SOLVE   :2:SA
+---
+ flood.c | 6 +++++-
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+diff --git a/flood.c b/flood.c
+index 08410ba2..c015c7e8 100644
+--- a/flood.c
++++ b/flood.c
+@@ -942,7 +942,11 @@ static game_state *execute_move(const game_state *state, const char *move)
+             sol->moves[i] = atoi(p);
+             p += strspn(p, "0123456789");
+             if (*p) {
+-                assert(*p == ',');
++                if (*p != ',') {
++                    sfree(sol->moves);
++                    sfree(sol);
++                    return NULL;
++                }
+                 p++;
+             }
+         }
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0009-Don-t-segfault-on-premature-solve-moves-in-Mines.patch sgt-puzzles-20230122.806ae71/debian/patches/0009-Don-t-segfault-on-premature-solve-moves-in-Mines.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0009-Don-t-segfault-on-premature-solve-moves-in-Mines.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0009-Don-t-segfault-on-premature-solve-moves-in-Mines.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,40 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 28 Jan 2023 19:34:28 +0000
+Subject: [PATCH 009/159] Don't segfault on premature solve moves in Mines
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=28671e76b736aeb860b1f725898c45fe70ae6212
+Bug-Debian: https://bugs.debian.org/1034190
+
+If a save file contained a solve move as the first move, Mines would
+dereference a null pointer trying to look up the (at that point
+undetermined) mine locations.  Now execute_move() politely returns
+NULL instead.
+
+This save file demonstrates the problem:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :5:Mines
+PARAMS  :5:3x3n0
+CPARAMS :5:3x3n0
+DESC    :127:r0,u,7a142789cabddc3fc4dcb7d2baa4a4937b33c9613ea870ac098e217981ad339930af585557d62048ea745d05b01475d9699596b394cc0adeebf0440a02
+UI      :2:D0
+TIME    :1:0
+NSTATES :1:2
+STATEPOS:1:2
+SOLVE   :1:S
+---
+ mines.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/mines.c b/mines.c
+index 8af7c206..325c850d 100644
+--- a/mines.c
++++ b/mines.c
+@@ -2637,6 +2637,7 @@ static game_state *execute_move(const game_state *from, const char *move)
+     if (!strcmp(move, "S")) {
+ 	int yy, xx;
+ 
++        if (!from->layout->mines) return NULL; /* Game not started. */
+ 	ret = dup_game(from);
+         if (!ret->dead) {
+             /*
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0010-Limit-number-of-mines-in-Mines-game-description.patch sgt-puzzles-20230122.806ae71/debian/patches/0010-Limit-number-of-mines-in-Mines-game-description.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0010-Limit-number-of-mines-in-Mines-game-description.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0010-Limit-number-of-mines-in-Mines-game-description.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,28 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 28 Jan 2023 23:12:52 +0000
+Subject: [PATCH 010/159] Limit number of mines in Mines game description
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=75e8a1a9cabe7567f6019b1226783b61ba1ac42f
+Bug-Debian: https://bugs.debian.org/1034190
+
+Without this, it's possible to specify a description that has more
+mines than there are places on the board to place them, which
+eventually leads to a division by zero.  This can be demonstrated by
+entering a game description of "3:r8,u," and then clicking anywhere on
+the board.
+---
+ mines.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/mines.c b/mines.c
+index 325c850d..56748491 100644
+--- a/mines.c
++++ b/mines.c
+@@ -2006,6 +2006,8 @@ static const char *validate_desc(const game_params *params, const char *desc)
+         desc++;
+ 	if (!*desc || !isdigit((unsigned char)*desc))
+ 	    return "No initial mine count in game description";
++	if (atoi(desc) > wh - 9)
++            return "Too many mines for grid size";
+ 	while (*desc && isdigit((unsigned char)*desc))
+ 	    desc++;		       /* skip over mine count */
+ 	if (*desc != ',')
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0011-Validate-the-number-of-pegs-and-holes-in-a-Pegs-game.patch sgt-puzzles-20230122.806ae71/debian/patches/0011-Validate-the-number-of-pegs-and-holes-in-a-Pegs-game.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0011-Validate-the-number-of-pegs-and-holes-in-a-Pegs-game.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0011-Validate-the-number-of-pegs-and-holes-in-a-Pegs-game.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,44 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 28 Jan 2023 23:45:48 +0000
+Subject: [PATCH 011/159] Validate the number of pegs and holes in a Pegs game
+ ID
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=4359f59dd22770a94e29b2ddd54b533ad1713550
+Bug-Debian: https://bugs.debian.org/1034190
+
+Without this, "1:O" causes an assertion violation, '!"new_ui found
+nowhere for cursor"'.  We may as well require two pegs and one hole,
+since that's the minimum for a game in which there are any moves to
+make.
+---
+ pegs.c | 11 ++++++++++-
+ 1 file changed, 10 insertions(+), 1 deletion(-)
+
+diff --git a/pegs.c b/pegs.c
+index 54d1a21a..2fa9a2a3 100644
+--- a/pegs.c
++++ b/pegs.c
+@@ -663,7 +663,7 @@ static char *new_game_desc(const game_params *params, random_state *rs,
+ 
+ static const char *validate_desc(const game_params *params, const char *desc)
+ {
+-    int len;
++    int len, i, npeg = 0, nhole = 0;
+ 
+     len = params->w * params->h;
+ 
+@@ -671,6 +671,15 @@ static const char *validate_desc(const game_params *params, const char *desc)
+ 	return "Game description is wrong length";
+     if (len != strspn(desc, "PHO"))
+ 	return "Invalid character in game description";
++    for (i = 0; i < len; i++) {
++        npeg += desc[i] == 'P';
++        nhole += desc[i] == 'H';
++    }
++    /* The minimal soluble game has two pegs and a hole: "3x1:PPH". */
++    if (npeg < 2)
++        return "Too few pegs in game description";
++    if (nhole < 1)
++        return "Too few holes in game description";
+ 
+     return NULL;
+ }
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0017-Mines-forbid-moves-that-flag-or-unflag-an-exposed-sq.patch sgt-puzzles-20230122.806ae71/debian/patches/0017-Mines-forbid-moves-that-flag-or-unflag-an-exposed-sq.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0017-Mines-forbid-moves-that-flag-or-unflag-an-exposed-sq.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0017-Mines-forbid-moves-that-flag-or-unflag-an-exposed-sq.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,30 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Wed, 1 Feb 2023 17:07:12 +0000
+Subject: [PATCH 017/159] Mines: forbid moves that flag or unflag an exposed
+ square
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=17364455186ae61e015d0f0de3f09423f78d0727
+Bug-Debian: https://bugs.debian.org/1034190
+
+interpret_move() couldn't generate them, but execute_move() also needs
+to forbid them to defend against corrupt save files.  I don't think this
+actually caused any crashes, but it did cause unexpected "1" squares not
+adjacent to mines.
+---
+ mines.c | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/mines.c b/mines.c
+index 711fc502..d40bb9fb 100644
+--- a/mines.c
++++ b/mines.c
+@@ -2701,7 +2701,9 @@ static game_state *execute_move(const game_state *from, const char *move)
+ 	while (*move) {
+ 	    if (move[0] == 'F' &&
+ 		sscanf(move+1, "%d,%d", &cx, &cy) == 2 &&
+-		cx >= 0 && cx < from->w && cy >= 0 && cy < from->h) {
++		cx >= 0 && cx < from->w && cy >= 0 && cy < from->h &&
++                (ret->grid[cy * from->w + cx] == -1 ||
++                 ret->grid[cy * from->w + cx] == -2)) {
+ 		ret->grid[cy * from->w + cx] ^= (-2 ^ -1);
+ 	    } else if (move[0] == 'O' &&
+ 		       sscanf(move+1, "%d,%d", &cx, &cy) == 2 &&
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0018-Mines-Don-t-check-if-the-player-has-won-if-they-ve-a.patch sgt-puzzles-20230122.806ae71/debian/patches/0018-Mines-Don-t-check-if-the-player-has-won-if-they-ve-a.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0018-Mines-Don-t-check-if-the-player-has-won-if-they-ve-a.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0018-Mines-Don-t-check-if-the-player-has-won-if-they-ve-a.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,44 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Wed, 1 Feb 2023 20:12:29 +0000
+Subject: [PATCH 018/159] Mines: Don't check if the player has won if they've
+ already lost
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=2a9be2b89df3e6a07a1d90a06f8ac00a92d789e5
+Bug-Debian: https://bugs.debian.org/1034190
+
+It can't happen in normal play, but if a save file had a "C" (clear
+around) move that set off a mine, Mines could end up hitting an
+assertion failure, "ncovered >= nmines".  This was because it would
+check if the player had won by counting covered squares and mines, and
+of course an uncovered mine is no longer a covered square but is still a
+mine.
+
+Since winning after you're dead isn't possible (at least in Mines), we
+now skip the check entirely if the player has already died.
+
+This save file demonstrates the problem:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :5:Mines
+PARAMS  :1:7
+CPARAMS :1:7
+DESC    :22:r31,u,0000C000d0000020
+NSTATES :1:2
+STATEPOS:1:1
+MOVE    :4:C6,2
+---
+ mines.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/mines.c b/mines.c
+index d40bb9fb..16075aaf 100644
+--- a/mines.c
++++ b/mines.c
+@@ -2146,6 +2146,8 @@ static int open_square(game_state *state, int x, int y)
+ 	    break;
+     }
+ 
++    /* If the player has already lost, don't let them win as well. */
++    if (state->dead) return 0;
+     /*
+      * Finally, scan the grid and see if exactly as many squares
+      * are still covered as there are mines. If so, set the `won'
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0019-Avoid-invalid-moves-when-solving-Tracks.patch sgt-puzzles-20230122.806ae71/debian/patches/0019-Avoid-invalid-moves-when-solving-Tracks.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0019-Avoid-invalid-moves-when-solving-Tracks.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0019-Avoid-invalid-moves-when-solving-Tracks.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,66 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Wed, 1 Feb 2023 21:28:35 +0000
+Subject: [PATCH 019/159] Avoid invalid moves when solving Tracks
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=875f0af21fbced5cbf6cf63b86fe3dc51682c863
+Bug-Debian: https://bugs.debian.org/1034190
+
+The solver, when it decided that an edge or square should be both TRACK
+and NOTRACK, would correctly decide that the puzzle was insoluble, but
+would also mark the edge with both flags in its working copy.  This
+could then lead to assertion violations if that working copy of the
+board was used for something else, for instance if it was fed back into
+the solver.  This couldn't happen in normal play, since failed solutions
+just cause the solve command to fail, but the diagnostic "H" command
+could trigger it from a save file, causing an assertion failure:
+"state->sflags[y*state->p.w + x] & S_CLUE".
+
+Now when the solver runs into this situation, it marks the puzzle as
+insoluble but doesn't set the invalid flag, so the board remains valid
+and future solve operations are safe.
+
+This save file is the one that demonstrated the problem:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :12:Train Tracks
+PARAMS  :5:6x6t0
+CPARAMS :5:6x6t0
+DESC    :31:b0t9l,,S0,00,0,0,4,0,0,S0,0,0,0
+NSTATES :1:8
+STATEPOS:1:2
+MOVE    :1:H
+MOVE    :1:H
+MOVE    :1:H
+MOVE    :1:H
+MOVE    :1:H
+MOVE    :1:H
+MOVE    :1:H
+---
+ tracks.c | 8 ++++----
+ 1 file changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/tracks.c b/tracks.c
+index 53754107..6948b42c 100644
+--- a/tracks.c
++++ b/tracks.c
+@@ -925,8 +925,8 @@ static int solve_set_sflag(game_state *state, int x, int y,
+     if (state->sflags[i] & (f == S_TRACK ? S_NOTRACK : S_TRACK)) {
+         solverdebug(("opposite flag already set there, marking IMPOSSIBLE"));
+         state->impossible = true;
+-    }
+-    state->sflags[i] |= f;
++    } else
++        state->sflags[i] |= f;
+     return 1;
+ }
+ 
+@@ -943,8 +943,8 @@ static int solve_set_eflag(game_state *state, int x, int y, int d,
+     if (sf & (f == E_TRACK ? E_NOTRACK : E_TRACK)) {
+         solverdebug(("opposite flag already set there, marking IMPOSSIBLE"));
+         state->impossible = true;
+-    }
+-    S_E_SET(state, x, y, d, f);
++    } else
++        S_E_SET(state, x, y, d, f);
+     return 1;
+ }
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0020-Fix-move-validation-in-Netslide.patch sgt-puzzles-20230122.806ae71/debian/patches/0020-Fix-move-validation-in-Netslide.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0020-Fix-move-validation-in-Netslide.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0020-Fix-move-validation-in-Netslide.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,42 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Wed, 1 Feb 2023 23:00:14 +0000
+Subject: [PATCH 020/159] Fix move validation in Netslide
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=ed0e4c304bed990948541fc0cf87309d75653806
+Bug-Debian: https://bugs.debian.org/1034190
+
+The maximum length of a column move in Netslide is the height of the
+puzzle, and the maximum length of a row move is the width, not the
+other way around.
+
+Moves of absolute length more than 1 can't be generated by
+interpret_move(), but they can come from save files.  On non-square
+grids, the incorrect check led to assertion failures: "0 <= tx && tx <
+w" and "0 <= ty && ty < h".  This save file demonstrates the problem:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :8:Netslide
+PARAMS  :3:4x9
+CPARAMS :3:4x9
+DESC    :39:0000000000000h0h0000000000000000000000v
+NSTATES :1:2
+STATEPOS:1:2
+MOVE    :4:R0,5
+---
+ netslide.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/netslide.c b/netslide.c
+index 1a01be2e..be83b7cc 100644
+--- a/netslide.c
++++ b/netslide.c
+@@ -1137,8 +1137,8 @@ static game_state *execute_move(const game_state *from, const char *move)
+     if ((move[0] == 'C' || move[0] == 'R') &&
+ 	sscanf(move+1, "%d,%d", &c, &d) == 2 &&
+ 	c >= 0 && c < (move[0] == 'C' ? from->width : from->height) &&
+-        d <= (move[0] == 'C' ? from->width : from->height) &&
+-        d >= -(move[0] == 'C' ? from->width : from->height) && d != 0) {
++        d <= (move[0] == 'C' ? from->height : from->width) &&
++        d >= -(move[0] == 'C' ? from->height : from->width) && d != 0) {
+ 	col = (move[0] == 'C');
+     } else if (move[0] == 'S' &&
+ 	       strlen(move) == from->width * from->height + 1) {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0021-Tighten-validation-of-Tents-game-descriptions.patch sgt-puzzles-20230122.806ae71/debian/patches/0021-Tighten-validation-of-Tents-game-descriptions.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0021-Tighten-validation-of-Tents-game-descriptions.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0021-Tighten-validation-of-Tents-game-descriptions.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,57 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Thu, 2 Feb 2023 21:58:10 +0000
+Subject: [PATCH 021/159] Tighten validation of Tents game descriptions
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=ed682bd5c608156d12ebaa2d84c4ce2e2877c10a
+Bug-Debian: https://bugs.debian.org/1034190
+
+Specifically, TENT and NONTENT markers ('!' and '-') cannot appear as
+the first or last character of a description, because that would
+attempt to place them on squares outside the grid.  This was caught by
+assertions in new_game(), as can be demonstrated by feeding these
+descriptions to older versions of Tents: "4:-p,0,0,0,0,0,0,0,0"
+("new_game: Assertion `i >= 0 && i <= w*h' failed.") and
+4:p-,0,0,0,0,0,0,0,0 ("new_game: Assertion `*desc == ','' failed.").
+---
+ tents.c | 22 +++++++++++++++++++---
+ 1 file changed, 19 insertions(+), 3 deletions(-)
+
+diff --git a/tents.c b/tents.c
+index 9e8349c8..f08a0e36 100644
+--- a/tents.c
++++ b/tents.c
+@@ -1191,6 +1191,21 @@ static char *new_game_desc(const game_params *params_in, random_state *rs,
+     return ret;
+ }
+ 
++/*
++ * Grid description format:
++ * 
++ * _ = tree
++ * a = 1 BLANK then TREE
++ * ...
++ * y = 25 BLANKs then TREE
++ * z = 25 BLANKs
++ * ! = set previous square to TENT
++ * - = set previous square to NONTENT
++ *
++ * Last character must be one that would insert a tree as the first
++ * square after the grid.
++ */
++
+ static const char *validate_desc(const game_params *params, const char *desc)
+ {
+     int w = params->w, h = params->h;
+@@ -1204,9 +1219,10 @@ static const char *validate_desc(const game_params *params, const char *desc)
+             area += *desc - 'a' + 2;
+ 	else if (*desc == 'z')
+             area += 25;
+-        else if (*desc == '!' || *desc == '-')
+-            /* do nothing */;
+-        else
++        else if (*desc == '!' || *desc == '-') {
++            if (area == 0 || area > w * h)
++                return "Tent or non-tent placed off the grid";
++        } else
+             return "Invalid character in grid specification";
+ 
+ 	desc++;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0022-Dominosa-require-the-two-halves-of-a-domino-to-be-ad.patch sgt-puzzles-20230122.806ae71/debian/patches/0022-Dominosa-require-the-two-halves-of-a-domino-to-be-ad.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0022-Dominosa-require-the-two-halves-of-a-domino-to-be-ad.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0022-Dominosa-require-the-two-halves-of-a-domino-to-be-ad.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,51 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Thu, 2 Feb 2023 22:26:24 +0000
+Subject: [PATCH 022/159] Dominosa: require the two halves of a domino to be
+ adjacent
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=294a3ac6e703c2820ceb7b28a1a5492b61e9a531
+Bug-Debian: https://bugs.debian.org/1034190
+
+Also that a line indicating no domino be between adjacent squares.
+Without this, execute_move would allow you to place dominos and edges
+between any pair ot squares, and then generate assertion failures
+("execute_move: Assertion `d2 - w >= 0' failed." and "execute_move:
+Assertion `d1 - w >= 0' failed.") when a domino was placed over an
+invalid edge.  This example save file demonstrates the problem:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :8:Dominosa
+PARAMS  :1:6
+CPARAMS :1:6
+DESC    :56:55521461210004364611033535444421636022603153156422620503
+NSTATES :1:3
+STATEPOS:1:3
+MOVE    :4:E0,2
+MOVE    :4:D0,2
+---
+ dominosa.c | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/dominosa.c b/dominosa.c
+index e60cb462..61b345e8 100644
+--- a/dominosa.c
++++ b/dominosa.c
+@@ -2896,7 +2896,8 @@ static game_state *execute_move(const game_state *state, const char *move)
+             move++;
+         } else if (move[0] == 'D' &&
+                    sscanf(move+1, "%d,%d%n", &d1, &d2, &p) == 2 &&
+-                   d1 >= 0 && d1 < wh && d2 >= 0 && d2 < wh && d1 < d2) {
++                   d1 >= 0 && d1 < wh && d2 >= 0 && d2 < wh && d1 < d2 &&
++                   (d2 - d1 == 1 || d2 - d1 == w)) {
+ 
+             /*
+              * Toggle domino presence between d1 and d2.
+@@ -2964,7 +2965,8 @@ static game_state *execute_move(const game_state *state, const char *move)
+         } else if (move[0] == 'E' &&
+                    sscanf(move+1, "%d,%d%n", &d1, &d2, &p) == 2 &&
+                    d1 >= 0 && d1 < wh && d2 >= 0 && d2 < wh && d1 < d2 &&
+-                   ret->grid[d1] == d1 && ret->grid[d2] == d2) {
++                   ret->grid[d1] == d1 && ret->grid[d2] == d2 &&
++                   (d2 - d1 == 1 || d2 - d1 == w)) {
+ 
+             /*
+              * Toggle edge presence between d1 and d2.
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0023-Forbid-lines-off-the-grid-in-Pearl.patch sgt-puzzles-20230122.806ae71/debian/patches/0023-Forbid-lines-off-the-grid-in-Pearl.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0023-Forbid-lines-off-the-grid-in-Pearl.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0023-Forbid-lines-off-the-grid-in-Pearl.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,50 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Thu, 2 Feb 2023 23:09:19 +0000
+Subject: [PATCH 023/159] Forbid lines off the grid in Pearl
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=15f4fa851a5781cf77984a6046405ffa758e7b33
+Bug-Debian: https://bugs.debian.org/1034190
+
+While they couldn't be generated in normal play, execute_move() would
+permit lines and marks across the edge of the grid that would then
+generate assertion failures ("dsf_update_completion: Assertion
+`INGRID(state, bx, by)' failed.").
+
+I've added a check to execute_move() that after updating a square, the
+square doesn't have any lines or marks that leave the grid.
+
+This save file demonstrated the problem:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSZON :1:1
+GAME    :5:Pearl
+PARAMS  :5:5x6dt
+CPARAMS :5:5x6dt
+DESC    :6:eeeeee
+NSTATES :1:2
+STATEPOS:1:1
+MOVE    :6:F1,4,2
+---
+ pearl.c | 10 ++++++++++
+ 1 file changed, 10 insertions(+)
+
+diff --git a/pearl.c b/pearl.c
+index 72f36f7d..f79a6dc2 100644
+--- a/pearl.c
++++ b/pearl.c
+@@ -2278,6 +2278,16 @@ static game_state *execute_move(const game_state *state, const char *move)
+                 (ret->marks[y*w + x] & (char)l))
+                 goto badmove;
+ 
++            /*
++             * Similarly, if we've ended up with a line or mark going
++             * off the board, that's not acceptable.
++             */
++            for (l = 1; l <= 8; l <<= 1)
++                if (((ret->lines[y*w + x] & (char)l) ||
++                     (ret->marks[y*w + x] & (char)l)) &&
++                    !INGRID(state, x+DX(l), y+DY(l)))
++                    goto badmove;
++
+             move += n;
+         } else if (strcmp(move, "H") == 0) {
+             pearl_solve(ret->shared->w, ret->shared->h,
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0024-Tolerate-incorrect-solutions-in-Inertia.patch sgt-puzzles-20230122.806ae71/debian/patches/0024-Tolerate-incorrect-solutions-in-Inertia.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0024-Tolerate-incorrect-solutions-in-Inertia.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0024-Tolerate-incorrect-solutions-in-Inertia.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,52 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Fri, 3 Feb 2023 20:52:05 +0000
+Subject: [PATCH 024/159] Tolerate incorrect solutions in Inertia
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=843d4ca17def11671809786f2a5aebd75f230dd9
+Bug-Debian: https://bugs.debian.org/1034190
+
+The "solve" operation in Inertia generates a proposed solution as a
+move string.  But if such a move string is loaded from a save file it
+might not actually describe a solution.  If that happens then it's
+possible to reach the end of the "solution" without winning, and doing
+so should probably cause a recalculation of the solution rather than
+an assertion failure ("execute_move: Assertion `ret->solnpos <
+ret->soln->len' failed.").
+
+I am a little concerned by the way that normal solve operations end up
+encoded in the save file, but the re-solvings caused by going off
+course don't, but I haven't got a good answer to that.
+
+Here's a save file that demonstrates the assertion failure:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :7:Inertia
+PARAMS  :3:8x8
+CPARAMS :3:8x8
+DESC    :64:sbgwsmswwgggwggmmbwgwbssbwbsbwbbwsSmwbbsbbmggbmssgmgwbmmwmbmmwsw
+NSTATES :2:3
+STATEPOS:1:1
+MOVE 000:2:S0
+MOVE 000:2:00
+---
+ inertia.c | 7 +++----
+ 1 file changed, 3 insertions(+), 4 deletions(-)
+
+diff --git a/inertia.c b/inertia.c
+index 2f352d31..b47d58e3 100644
+--- a/inertia.c
++++ b/inertia.c
+@@ -1741,11 +1741,10 @@ static game_state *execute_move(const game_state *state, const char *move)
+     if (ret->soln) {
+ 	if (ret->dead || ret->gems == 0)
+ 	    discard_solution(ret);
+-	else if (ret->soln->list[ret->solnpos] == dir) {
++	else if (ret->soln->list[ret->solnpos] == dir &&
++            ret->solnpos+1 < ret->soln->len)
+ 	    ++ret->solnpos;
+-	    assert(ret->solnpos < ret->soln->len); /* or gems == 0 */
+-	    assert(!ret->dead); /* or not a solution */
+-	} else {
++	else {
+ 	    const char *error = NULL;
+             char *soln = solve_game(NULL, ret, NULL, &error);
+ 	    if (!error) {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0025-Palisade-replace-dfs_dsf-with-a-simple-iteration.patch sgt-puzzles-20230122.806ae71/debian/patches/0025-Palisade-replace-dfs_dsf-with-a-simple-iteration.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0025-Palisade-replace-dfs_dsf-with-a-simple-iteration.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0025-Palisade-replace-dfs_dsf-with-a-simple-iteration.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,111 @@
+From: Simon Tatham <anakin@pobox.com>
+Date: Fri, 3 Feb 2023 23:12:38 +0000
+Subject: [PATCH 025/159] Palisade: replace dfs_dsf() with a simple iteration.
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=517b14e666b0b71fc0bcd5da1b22cdc90d3434c9
+Bug-Debian: https://bugs.debian.org/1034190
+
+The whole purpose of a dsf is that you can traverse the edges of your
+graph in any order you feel like. So if you want to build the
+connected components of a graph you can just loop over all the edges
+once. There's no need to run a depth-first search.
+
+In fact there were an amazing number of things wrong with this 10-line
+function:
+
+ - As Ben points out in commit 21193eaf9308ace, it didn't bother with
+   bounds checking when searching the grid, instead relying on the
+   never-removed grid boundary to stop the search - which was fragile in
+   the face of other bugs.
+
+ - The recursion uses linear stack, which is much worse than linear
+   heap, since stacks are often much more limited. (And the dsf _also_
+   used linear heap.)
+
+ - The recursion was completely unnecessary.
+
+ - The function used internal knowledge about dsf.c in order to define
+   the value UNVISITED to match what would happen to work.
+
+ - The name 'dfs_dsf' is totally confusing and almost impossible to
+   type!
+---
+ palisade.c | 36 ++++++++++++++++--------------------
+ 1 file changed, 16 insertions(+), 20 deletions(-)
+
+diff --git a/palisade.c b/palisade.c
+index f405efde..6aff0a4d 100644
+--- a/palisade.c
++++ b/palisade.c
+@@ -505,19 +505,20 @@ static bool solver_equivalent_edges(solver_ctx *ctx)
+     return changed;
+ }
+ 
+-#define UNVISITED 6
+-
+ /* build connected components in `dsf', along the lines of `borders'. */
+-static void dfs_dsf(int i, int w, borderflag *border, int *dsf, bool black)
++static void build_dsf(int w, int h, borderflag *border, int *dsf, bool black)
+ {
+-    int dir;
+-    for (dir = 0; dir < 4; ++dir) {
+-        int ii = i + dx[dir] + w*dy[dir], bdir = BORDER(dir);
+-        if (black ? (border[i] & bdir) : !(border[i] & DISABLED(bdir)))
+-            continue;
+-        if (dsf[ii] != UNVISITED) continue;
+-        dsf_merge(dsf, i, ii);
+-        dfs_dsf(ii, w, border, dsf, black);
++    int x, y;
++
++    for (y = 0; y < h; y++) {
++        for (x = 0; x < w; x++) {
++            if (x+1 < w && (black ? !(border[y*w+x] & BORDER_R) :
++                            (border[y*w+x] & DISABLED(BORDER_R))))
++                dsf_merge(dsf, y*w+x, y*w+(x+1));
++            if (y+1 < h && (black ? !(border[y*w+x] & BORDER_D) :
++                            (border[y*w+x] & DISABLED(BORDER_D))))
++                dsf_merge(dsf, y*w+x, (y+1)*w+x);
++        }
+     }
+ }
+ 
+@@ -528,7 +529,7 @@ static bool is_solved(const game_params *params, clue *clues,
+     int i, x, y;
+     int *dsf = snew_dsf(wh);
+ 
+-    assert (dsf[0] == UNVISITED); /* check: UNVISITED and dsf.c match up */
++    build_dsf(w, h, border, dsf, true);
+ 
+     /*
+      * A game is solved if:
+@@ -539,7 +540,6 @@ static bool is_solved(const game_params *params, clue *clues,
+      *  - the borders also satisfy the clue set
+      */
+     for (i = 0; i < wh; ++i) {
+-        if (dsf[i] == UNVISITED) dfs_dsf(i, params->w, border, dsf, true);
+         if (dsf_size(dsf, i) != k) goto error;
+         if (clues[i] == EMPTY) continue;
+         if (clues[i] != bitcount[border[i] & BORDER_MASK]) goto error;
+@@ -1179,7 +1179,7 @@ static void game_redraw(drawing *dr, game_drawstate *ds,
+                         float animtime, float flashtime)
+ {
+     int w = state->shared->params.w, h = state->shared->params.h, wh = w*h;
+-    int r, c, i, flash = ((int) (flashtime * 5 / FLASH_TIME)) % 2;
++    int r, c, flash = ((int) (flashtime * 5 / FLASH_TIME)) % 2;
+     int *black_border_dsf = snew_dsf(wh), *yellow_border_dsf = snew_dsf(wh);
+     int k = state->shared->params.k;
+ 
+@@ -1200,12 +1200,8 @@ static void game_redraw(drawing *dr, game_drawstate *ds,
+         status_bar(dr, buf);
+     }
+ 
+-    for (i = 0; i < wh; ++i) {
+-        if (black_border_dsf[i] == UNVISITED)
+-            dfs_dsf(i, w, state->borders, black_border_dsf, true);
+-        if (yellow_border_dsf[i] == UNVISITED)
+-            dfs_dsf(i, w, state->borders, yellow_border_dsf, false);
+-    }
++    build_dsf(w, h, state->borders, black_border_dsf, true);
++    build_dsf(w, h, state->borders, yellow_border_dsf, false);
+ 
+     for (r = 0; r < h; ++r)
+         for (c = 0; c < w; ++c) {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0026-latin_solver_alloc-handle-clashing-numbers-in-input-.patch sgt-puzzles-20230122.806ae71/debian/patches/0026-latin_solver_alloc-handle-clashing-numbers-in-input-.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0026-latin_solver_alloc-handle-clashing-numbers-in-input-.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0026-latin_solver_alloc-handle-clashing-numbers-in-input-.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,223 @@
+From: Simon Tatham <anakin@pobox.com>
+Date: Sun, 5 Feb 2023 10:29:42 +0000
+Subject: [PATCH 026/159] latin_solver_alloc: handle clashing numbers in input
+ grid.
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=5030d87903191d581586ecda2382ad5bcd70f63d
+Bug-Debian: https://bugs.debian.org/1034190
+
+In the setup phase of the centralised latin.c solver, we start by
+going over the input grid containing already-placed clue numbers, and
+calling latin_solver_place to enter each on into the solver's data
+structure. This has the side effect of ruling out each number from the
+rest of the row and column, and _also_ checking by assertion that the
+number being placed is not ruled out.
+
+Those are a bad combination, because it means that if you give an
+obviously inconsistent input grid to latin_solver_alloc (e.g. with two
+identical numbers in a row already), it will fail an assertion. In
+that situation, you want the solver run as a whole to return
+diff_impossible so that the error is reported cleanly.
+
+This assertion failure could be provoked by giving either Towers or
+Group a manually-constructed game description inconsistent in that
+way, and hitting Solve. Worse, it could be provoked during live play
+in Unequal, by filling in a number clashing with a clue and then
+pressing 'h' to get hints.
+---
+ latin.c            | 47 +++++++++++++++++++++++++++++-----------------
+ latin.h            |  9 ++++++---
+ unequal.c          | 30 +++++++++++++++--------------
+ unfinished/group.c | 15 ++++++++-------
+ 4 files changed, 60 insertions(+), 41 deletions(-)
+
+diff --git a/latin.c b/latin.c
+index 39930166..59f306dd 100644
+--- a/latin.c
++++ b/latin.c
+@@ -563,7 +563,7 @@ void latin_solver_free_scratch(struct latin_solver_scratch *scratch)
+     sfree(scratch);
+ }
+ 
+-void latin_solver_alloc(struct latin_solver *solver, digit *grid, int o)
++bool latin_solver_alloc(struct latin_solver *solver, digit *grid, int o)
+ {
+     int x, y;
+ 
+@@ -577,14 +577,23 @@ void latin_solver_alloc(struct latin_solver *solver, digit *grid, int o)
+     memset(solver->row, 0, o*o);
+     memset(solver->col, 0, o*o);
+ 
+-    for (x = 0; x < o; x++)
+-	for (y = 0; y < o; y++)
+-	    if (grid[y*o+x])
+-		latin_solver_place(solver, x, y, grid[y*o+x]);
+-
+ #ifdef STANDALONE_SOLVER
+     solver->names = NULL;
+ #endif
++
++    for (x = 0; x < o; x++) {
++	for (y = 0; y < o; y++) {
++            int n = grid[y*o+x];
++            if (n) {
++                if (cube(x, y, n))
++                    latin_solver_place(solver, x, y, n);
++                else
++                    return false;      /* puzzle is already inconsistent */
++            }
++        }
++    }
++
++    return true;
+ }
+ 
+ void latin_solver_free(struct latin_solver *solver)
+@@ -810,15 +819,17 @@ static int latin_solver_recurse
+ 	    } else {
+ 		newctx = ctx;
+ 	    }
+-	    latin_solver_alloc(&subsolver, outgrid, o);
+ #ifdef STANDALONE_SOLVER
+ 	    subsolver.names = solver->names;
+ #endif
+-            ret = latin_solver_top(&subsolver, diff_recursive,
+-				   diff_simple, diff_set_0, diff_set_1,
+-				   diff_forcing, diff_recursive,
+-				   usersolvers, valid, newctx,
+-                                   ctxnew, ctxfree);
++	    if (latin_solver_alloc(&subsolver, outgrid, o))
++                ret = latin_solver_top(&subsolver, diff_recursive,
++                                       diff_simple, diff_set_0, diff_set_1,
++                                       diff_forcing, diff_recursive,
++                                       usersolvers, valid, newctx,
++                                       ctxnew, ctxfree);
++            else
++                ret = diff_impossible;
+ 	    latin_solver_free(&subsolver);
+ 	    if (ctxnew)
+ 		ctxfree(newctx);
+@@ -1059,11 +1070,13 @@ int latin_solver(digit *grid, int o, int maxdiff,
+     struct latin_solver solver;
+     int diff;
+ 
+-    latin_solver_alloc(&solver, grid, o);
+-    diff = latin_solver_main(&solver, maxdiff,
+-			     diff_simple, diff_set_0, diff_set_1,
+-			     diff_forcing, diff_recursive,
+-			     usersolvers, valid, ctx, ctxnew, ctxfree);
++    if (latin_solver_alloc(&solver, grid, o))
++        diff = latin_solver_main(&solver, maxdiff,
++                                 diff_simple, diff_set_0, diff_set_1,
++                                 diff_forcing, diff_recursive,
++                                 usersolvers, valid, ctx, ctxnew, ctxfree);
++    else
++        diff = diff_impossible;
+     latin_solver_free(&solver);
+     return diff;
+ }
+diff --git a/latin.h b/latin.h
+index bb172ec3..96a04804 100644
+--- a/latin.h
++++ b/latin.h
+@@ -61,10 +61,13 @@ int latin_solver_forcing(struct latin_solver *solver,
+ /* --- Solver allocation --- */
+ 
+ /* Fills in (and allocates members for) a latin_solver struct.
+- * Will allocate members of snew, but not snew itself
++ * Will allocate members of solver, but not solver itself
+  * (allowing 'struct latin_solver' to be the first element in a larger
+- * struct, for example). */
+-void latin_solver_alloc(struct latin_solver *solver, digit *grid, int o);
++ * struct, for example).
++ *
++ * latin_solver_alloc returns false if the digits already in the grid
++ * could not be legally placed. */
++bool latin_solver_alloc(struct latin_solver *solver, digit *grid, int o);
+ void latin_solver_free(struct latin_solver *solver);
+ 
+ /* Allocates scratch space (for _set and _forcing) */
+diff --git a/unequal.c b/unequal.c
+index ed03c0be..f2328b68 100644
+--- a/unequal.c
++++ b/unequal.c
+@@ -890,13 +890,14 @@ static int solver_state(game_state *state, int maxdiff)
+     struct latin_solver solver;
+     int diff;
+ 
+-    latin_solver_alloc(&solver, state->nums, state->order);
+-
+-    diff = latin_solver_main(&solver, maxdiff,
+-			     DIFF_LATIN, DIFF_SET, DIFF_EXTREME,
+-			     DIFF_EXTREME, DIFF_RECURSIVE,
+-			     unequal_solvers, unequal_valid, ctx,
+-                             clone_ctx, free_ctx);
++    if (!latin_solver_alloc(&solver, state->nums, state->order))
++        diff = latin_solver_main(&solver, maxdiff,
++                                 DIFF_LATIN, DIFF_SET, DIFF_EXTREME,
++                                 DIFF_EXTREME, DIFF_RECURSIVE,
++                                 unequal_solvers, unequal_valid, ctx,
++                                 clone_ctx, free_ctx);
++    else
++        diff = DIFF_IMPOSSIBLE;
+ 
+     memcpy(state->hints, solver.cube, state->order*state->order*state->order);
+ 
+@@ -2256,13 +2257,14 @@ static int solve(game_params *p, char *desc, int debug)
+     solver_show_working = debug;
+     game_debug(state);
+ 
+-    latin_solver_alloc(&solver, state->nums, state->order);
+-
+-    diff = latin_solver_main(&solver, DIFF_RECURSIVE,
+-			     DIFF_LATIN, DIFF_SET, DIFF_EXTREME,
+-			     DIFF_EXTREME, DIFF_RECURSIVE,
+-			     unequal_solvers, unequal_valid, ctx,
+-                             clone_ctx, free_ctx);
++    if (latin_solver_alloc(&solver, state->nums, state->order))
++        diff = latin_solver_main(&solver, DIFF_RECURSIVE,
++                                 DIFF_LATIN, DIFF_SET, DIFF_EXTREME,
++                                 DIFF_EXTREME, DIFF_RECURSIVE,
++                                 unequal_solvers, unequal_valid, ctx,
++                                 clone_ctx, free_ctx);
++    else
++        diff = DIFF_IMPOSSIBLE;
+ 
+     free_ctx(ctx);
+ 
+diff --git a/unfinished/group.c b/unfinished/group.c
+index e72e3ce3..2eca7c40 100644
+--- a/unfinished/group.c
++++ b/unfinished/group.c
+@@ -580,13 +580,11 @@ static int solver(const game_params *params, digit *grid, int maxdiff)
+     int w = params->w;
+     int ret;
+     struct latin_solver solver;
++
+ #ifdef STANDALONE_SOLVER
+     char *p, text[100], *names[50];
+     int i;
+-#endif
+ 
+-    latin_solver_alloc(&solver, grid, w);
+-#ifdef STANDALONE_SOLVER
+     for (i = 0, p = text; i < w; i++) {
+ 	names[i] = p;
+ 	*p++ = TOCHAR(i+1, params->id);
+@@ -595,10 +593,13 @@ static int solver(const game_params *params, digit *grid, int maxdiff)
+     solver.names = names;
+ #endif
+ 
+-    ret = latin_solver_main(&solver, maxdiff,
+-			    DIFF_TRIVIAL, DIFF_HARD, DIFF_EXTREME,
+-			    DIFF_EXTREME, DIFF_UNREASONABLE,
+-			    group_solvers, group_valid, NULL, NULL, NULL);
++    if (latin_solver_alloc(&solver, grid, w))
++        ret = latin_solver_main(&solver, maxdiff,
++                                DIFF_TRIVIAL, DIFF_HARD, DIFF_EXTREME,
++                                DIFF_EXTREME, DIFF_UNREASONABLE,
++                                group_solvers, group_valid, NULL, NULL, NULL);
++    else
++        ret = diff_impossible;
+ 
+     latin_solver_free(&solver);
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0027-Pearl-fix-assertion-failure-on-bad-puzzle.patch sgt-puzzles-20230122.806ae71/debian/patches/0027-Pearl-fix-assertion-failure-on-bad-puzzle.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0027-Pearl-fix-assertion-failure-on-bad-puzzle.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0027-Pearl-fix-assertion-failure-on-bad-puzzle.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,87 @@
+From: Simon Tatham <anakin@pobox.com>
+Date: Sun, 5 Feb 2023 11:19:30 +0000
+Subject: [PATCH 027/159] Pearl: fix assertion failure on bad puzzle.
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=05c536e50d0c3114e1de5283eac97ff22bad0fc7
+Bug-Debian: https://bugs.debian.org/1034190
+
+Similarly to the previous commit, if you started Pearl with at least
+some kinds of invalid puzzle (e.g. "6x6:abBfWcWWrBa") and then pressed
+'h' to get hints, you could provoke an assertion failure. But this
+time the assertion wasn't in the solver itself; the solver gave up
+gracefully and didn't crash, but it _did_ leave the links between
+squares in the game_state in an inconsistent state, in that one square
+was marked as linking to its neighbour without the neighbour also
+linking back to it. This caused the /* should have reciprocal link */
+assertion in dsf_update_completion to fail, when that was called from
+check_completion after the solver had finished, to decide whether the
+puzzle was now solved.
+
+In this commit, I arrange that whether or not pearl_solve returns a
+grid layout that's legal by the rules of the _puzzle_, it at least
+returns one that's legal by the rules of the _data representation_, in
+that every link between squares is either bidirectional or absent.
+
+This is a better solution than just removing the assertion, because if
+the inconsistent data were allowed to persist, it would lead to
+further problems in gameplay. For example, if you just remove that
+assertion instead of this fix and press 'h' on the example puzzle id
+above, you'll find that the non-reciprocal links are actually visible,
+in the form of several thick lines that stop at a grid square boundary
+instead of connecting two square-centres. (It looks even sillier if
+you set PEARL_GUI_LOOPY=y.)
+
+That's a situation that can't be created by a normal move, and if you
+try to make normal moves after it (e.g. click one of the weird edges),
+you'll find that both sides of the half-link get toggled, so now it's
+a half-link the other way round. So not only can't you _create_ this
+situation in normal play, you can't get rid of it either!
+
+That assertion in dsf_update_completion was commented out at one
+point, and I put it back in commit c5500926bf7458a saying that if it
+failed I'd like to know about it. And indeed, I'm glad I did, because
+this kind of unfixable wrongness in the resulting game_state was worth
+noticing and getting rid of!
+---
+ pearl.c | 29 +++++++++++++++++++++++++++++
+ 1 file changed, 29 insertions(+)
+
+diff --git a/pearl.c b/pearl.c
+index f79a6dc2..04745f34 100644
+--- a/pearl.c
++++ b/pearl.c
+@@ -855,6 +855,35 @@ static int pearl_solve(int w, int h, char *clues, char *result,
+                if (ret == 1) assert(b < 0xD); /* we should have had a break by now */
+             }
+         }
++
++        /*
++         * Ensure we haven't left the _data structure_ inconsistent,
++         * regardless of the consistency of the _puzzle_. In
++         * particular, we should never have marked one square as
++         * linked to its neighbour if the neighbour is not
++         * reciprocally linked back to the original square.
++         *
++         * This can happen if we get part way through solving an
++         * impossible puzzle and then give up trying to make further
++         * progress. So here we fix it up to avoid confusing the rest
++         * of the game.
++         */
++        for (y = 0; y < h; y++) {
++            for (x = 0; x < w; x++) {
++                for (d = 1; d <= 8; d += d) {
++                    int nx = x + DX(d), ny = y + DY(d);
++                    int rlink;
++                    if (0 <= nx && nx < w && 0 <= ny && ny < w)
++                        rlink = result[ny*w+nx] & F(d);
++                    else
++                        rlink = 0;     /* off-board squares don't link back */
++
++                    /* If other square doesn't link to us, don't link to it */
++                    if (!rlink)
++                        result[y*w+x] &= ~d;
++                }
++            }
++        }
+     }
+ 
+     sfree(dsfsize);
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0028-Pearl-fix-bounds-check-in-previous-commit.patch sgt-puzzles-20230122.806ae71/debian/patches/0028-Pearl-fix-bounds-check-in-previous-commit.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0028-Pearl-fix-bounds-check-in-previous-commit.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0028-Pearl-fix-bounds-check-in-previous-commit.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,24 @@
+From: Simon Tatham <anakin@pobox.com>
+Date: Sun, 5 Feb 2023 12:05:28 +0000
+Subject: [PATCH 028/159] Pearl: fix bounds check in previous commit.
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=9ce0a6d93212ffc0986a2b399fd8e9e983f62904
+Bug-Debian: https://bugs.debian.org/1034190
+
+Ahem. That's what I get for testing the fix on a square puzzle.
+---
+ pearl.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/pearl.c b/pearl.c
+index 04745f34..f6617a28 100644
+--- a/pearl.c
++++ b/pearl.c
+@@ -873,7 +873,7 @@ static int pearl_solve(int w, int h, char *clues, char *result,
+                 for (d = 1; d <= 8; d += d) {
+                     int nx = x + DX(d), ny = y + DY(d);
+                     int rlink;
+-                    if (0 <= nx && nx < w && 0 <= ny && ny < w)
++                    if (0 <= nx && nx < w && 0 <= ny && ny < h)
+                         rlink = result[ny*w+nx] & F(d);
+                     else
+                         rlink = 0;     /* off-board squares don't link back */
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0029-Unequal-Don-t-insist-that-solve-moves-must-actually-.patch sgt-puzzles-20230122.806ae71/debian/patches/0029-Unequal-Don-t-insist-that-solve-moves-must-actually-.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0029-Unequal-Don-t-insist-that-solve-moves-must-actually-.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0029-Unequal-Don-t-insist-that-solve-moves-must-actually-.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,60 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 4 Feb 2023 16:18:27 +0000
+Subject: [PATCH 029/159] Unequal: Don't insist that solve moves must actually
+ solve
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=84ec2a0a77d63450311f7c25b36d4b9f7e3c53e1
+Bug-Debian: https://bugs.debian.org/1034190
+
+A corrupt save file can include an "S" move that doesn't give a valid
+solution.  An assertion failure ("execute_move: Assertion `rc > 0'
+failed.") at that point is rude, so now we just don't set the
+"completed" flag in that case.  We still set the "cheated" flag, to
+reward (lack of) effort.
+
+Here's a trivial test case:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :7:Unequal
+CPARAMS :1:3
+PARAMS  :1:3
+DESC    :17:0,0,0,0,0,0,0,0,0
+NSTATES :1:2
+STATEPOS:1:2
+MOVE    :10:S222222222
+---
+ unequal.c | 8 ++++----
+ 1 file changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/unequal.c b/unequal.c
+index f2328b68..0a2bafda 100644
+--- a/unequal.c
++++ b/unequal.c
+@@ -1652,7 +1652,7 @@ static char *interpret_move(const game_state *state, game_ui *ui,
+ static game_state *execute_move(const game_state *state, const char *move)
+ {
+     game_state *ret = NULL;
+-    int x, y, n, i, rc;
++    int x, y, n, i;
+ 
+     debug(("execute_move: %s", move));
+ 
+@@ -1677,7 +1677,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+         const char *p;
+ 
+         ret = dup_game(state);
+-        ret->completed = ret->cheated = true;
++        ret->cheated = true;
+ 
+         p = move+1;
+         for (i = 0; i < state->order*state->order; i++) {
+@@ -1688,8 +1688,8 @@ static game_state *execute_move(const game_state *state, const char *move)
+             p++;
+         }
+         if (*p) goto badmove;
+-        rc = check_complete(ret->nums, ret, true);
+-	assert(rc > 0);
++        if (!ret->completed && check_complete(ret->nums, ret, true) > 0)
++            ret->completed = true;
+         return ret;
+     } else if (move[0] == 'M') {
+         ret = dup_game(state);
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0030-Range-Don-t-fail-an-assertion-on-an-all-black-board.patch sgt-puzzles-20230122.806ae71/debian/patches/0030-Range-Don-t-fail-an-assertion-on-an-all-black-board.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0030-Range-Don-t-fail-an-assertion-on-an-all-black-board.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0030-Range-Don-t-fail-an-assertion-on-an-all-black-board.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,29 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 4 Feb 2023 16:50:55 +0000
+Subject: [PATCH 030/159] Range: Don't fail an assertion on an all-black board
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=ae73ad76ef95f0e40868436cb750126322051dd0
+Bug-Debian: https://bugs.debian.org/1034190
+
+If there are no white squares, then Range's check that all the white
+squares form a connected component goes wrong.  Skip the check in that
+case to avoid an assretion violation ("edsf_canonify: Assertion `index
+>= 0' failed.").  This can be demonstrated by starting a game with no
+clues (e.g. "range 3:i") and then filling in every square.
+---
+ range.c | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/range.c b/range.c
+index bc70c05b..c360b753 100644
+--- a/range.c
++++ b/range.c
+@@ -1494,7 +1494,8 @@ static bool find_errors(const game_state *state, bool *report)
+             if (state->grid[r*w+c] != BLACK &&
+                 state->grid[r*w+(c+1)] != BLACK)
+                 dsf_merge(dsf, r*w+c, r*w+(c+1));
+-    if (nblack + dsf_size(dsf, any_white_cell) < n) {
++    if (any_white_cell != -1 &&
++        nblack + dsf_size(dsf, any_white_cell) < n) {
+         int biggest, canonical;
+ 
+         if (!report) {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0031-Limit-width-and-height-to-SHRT_MAX-in-Mines.patch sgt-puzzles-20230122.806ae71/debian/patches/0031-Limit-width-and-height-to-SHRT_MAX-in-Mines.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0031-Limit-width-and-height-to-SHRT_MAX-in-Mines.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0031-Limit-width-and-height-to-SHRT_MAX-in-Mines.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,41 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 28 Jan 2023 22:27:21 +0000
+Subject: [PATCH 031/159] Limit width and height to SHRT_MAX in Mines
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=c0e08f308792b15425e10ad494263d77a45ad92d
+Bug-Debian: https://bugs.debian.org/1034190
+
+Mines' "struct set" stores co-ordinates within the grid in a pair of
+shorts, which leads to very bad behaviour (including heap-based buffer
+overruns) if the grid is bigger than SHRT_MAX in either dimension.  So
+now we don't allow that.
+
+The overrun can be demonstrated by loading this save file, though the
+precise crash is quite variable.  In particular, you seem to get
+better crashes if the file doesn't have a trailing newline.
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+PARAMS  :5:06000
+CPARAMS :7:6x60000
+NSTATES :1:3
+STATEPOS:1:2
+MOVE    :5:C0,00
+GAME    :5:Mines
+DESC    :22:r8,u,00000000000000000
+MOVE    ::
+---
+ mines.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/mines.c b/mines.c
+index 16075aaf..c6660010 100644
+--- a/mines.c
++++ b/mines.c
+@@ -263,6 +263,8 @@ static const char *validate_params(const game_params *params, bool full)
+ 	return "Width and height must both be greater than two";
+     if (params->w < 1 || params->h < 1)
+ 	return "Width and height must both be at least one";
++    if (params->w > SHRT_MAX || params->h > SHRT_MAX)
++        return "Neither width nor height may be unreasonably large";
+     if (params->w > INT_MAX / params->h)
+         return "Width times height must not be unreasonably large";
+     if (params->n < 0)
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0032-Mines-Add-assertions-to-range-check-conversions-to-s.patch sgt-puzzles-20230122.806ae71/debian/patches/0032-Mines-Add-assertions-to-range-check-conversions-to-s.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0032-Mines-Add-assertions-to-range-check-conversions-to-s.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0032-Mines-Add-assertions-to-range-check-conversions-to-s.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,37 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Tue, 31 Jan 2023 21:08:05 +0000
+Subject: [PATCH 032/159] Mines: Add assertions to range-check conversions to
+ short
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=49841bd0fc04490d94cf32c0e6f9d3f4ffabe098
+Bug-Debian: https://bugs.debian.org/1034190
+
+I think these should be adequately guarded by the new restrictions on
+grid size, but I'd prefer to be sure.
+---
+ mines.c | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/mines.c b/mines.c
+index c6660010..04364e5a 100644
+--- a/mines.c
++++ b/mines.c
+@@ -435,7 +435,9 @@ static void ss_add(struct setstore *ss, int x, int y, int mask, int mines)
+      * Create a set structure and add it to the tree.
+      */
+     s = snew(struct set);
++    assert(SHRT_MIN <= x && x <= SHRT_MAX);
+     s->x = x;
++    assert(SHRT_MIN <= y && y <= SHRT_MAX);
+     s->y = y;
+     s->mask = mask;
+     s->mines = mines;
+@@ -506,7 +508,9 @@ static struct set **ss_overlap(struct setstore *ss, int x, int y, int mask)
+ 	    /*
+ 	     * Find the first set with these top left coordinates.
+ 	     */
++            assert(SHRT_MIN <= xx && xx <= SHRT_MAX);
+ 	    stmp.x = xx;
++            assert(SHRT_MIN <= yy && yy <= SHRT_MAX);
+ 	    stmp.y = yy;
+ 	    stmp.mask = 0;
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0033-Unequal-fix-sense-error-in-latin_solver_alloc-fix.patch sgt-puzzles-20230122.806ae71/debian/patches/0033-Unequal-fix-sense-error-in-latin_solver_alloc-fix.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0033-Unequal-fix-sense-error-in-latin_solver_alloc-fix.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0033-Unequal-fix-sense-error-in-latin_solver_alloc-fix.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,30 @@
+From: Simon Tatham <anakin@pobox.com>
+Date: Wed, 8 Feb 2023 18:22:23 +0000
+Subject: [PATCH 033/159] Unequal: fix sense error in latin_solver_alloc fix.
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=bd5c0a37a019c540eda05f8291cad90ffd598134
+Bug-Debian: https://bugs.debian.org/1034190
+
+In commit 5030d87903191d5 I gave latin_solver_alloc a return value,
+and introduced a check of that value at every call site. One of the
+checks was backwards, with the effect that Unequal game generation now
+more or less always fails an assertion. For example:
+
+$ unequal --generate 1 4#12345
+unequal: unequal.c:1072: gg_best_clue: Assertion `best != -1' failed.
+---
+ unequal.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/unequal.c b/unequal.c
+index 0a2bafda..52041bbd 100644
+--- a/unequal.c
++++ b/unequal.c
+@@ -890,7 +890,7 @@ static int solver_state(game_state *state, int maxdiff)
+     struct latin_solver solver;
+     int diff;
+ 
+-    if (!latin_solver_alloc(&solver, state->nums, state->order))
++    if (latin_solver_alloc(&solver, state->nums, state->order))
+         diff = latin_solver_main(&solver, maxdiff,
+                                  DIFF_LATIN, DIFF_SET, DIFF_EXTREME,
+                                  DIFF_EXTREME, DIFF_RECURSIVE,
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0034-Forbid-impossible-moves-in-Bridges.patch sgt-puzzles-20230122.806ae71/debian/patches/0034-Forbid-impossible-moves-in-Bridges.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0034-Forbid-impossible-moves-in-Bridges.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0034-Forbid-impossible-moves-in-Bridges.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,48 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Fri, 10 Feb 2023 17:09:18 +0000
+Subject: [PATCH 034/159] Forbid impossible moves in Bridges
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=bf9abb2a127a4a81babe50ecb419527f8aeffe28
+Bug-Debian: https://bugs.debian.org/1034190
+
+Specifically, a bridge or a non-bridge must connect two islands that
+differ in precisely one co-ordinate.  Without this, a save file that
+tries to connect or disconnect two non-orthogonal islands will cause
+"island_join: Assertion `!"island_join: islands not orthogonal."'
+failed."
+
+Here's a save file demonstrating the problem:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :7:Bridges
+PARAMS  :13:3x3i30e10m2d0
+CPARAMS :13:3x3i30e10m2d0
+DESC    :6:b1c1a2
+NSTATES :1:2
+STATEPOS:1:2
+MOVE    :10:L0,2,2,0,1
+---
+ bridges.c | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/bridges.c b/bridges.c
+index db4a66e6..32263348 100644
+--- a/bridges.c
++++ b/bridges.c
+@@ -2571,6 +2571,8 @@ static game_state *execute_move(const game_state *state, const char *move)
+                 goto badmove;
+             if (!INGRID(ret, x1, y1) || !INGRID(ret, x2, y2))
+                 goto badmove;
++            /* Precisely one co-ordinate must differ between islands. */
++            if ((x1 != x2) + (y1 != y2) != 1) goto badmove;
+             is1 = INDEX(ret, gridi, x1, y1);
+             is2 = INDEX(ret, gridi, x2, y2);
+             if (!is1 || !is2) goto badmove;
+@@ -2582,6 +2584,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+                 goto badmove;
+             if (!INGRID(ret, x1, y1) || !INGRID(ret, x2, y2))
+                 goto badmove;
++            if ((x1 != x2) + (y1 != y2) != 1) goto badmove;
+             is1 = INDEX(ret, gridi, x1, y1);
+             is2 = INDEX(ret, gridi, x2, y2);
+             if (!is1 || !is2) goto badmove;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0035-Forbid-game-descriptions-with-joined-islands-in-Brid.patch sgt-puzzles-20230122.806ae71/debian/patches/0035-Forbid-game-descriptions-with-joined-islands-in-Brid.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0035-Forbid-game-descriptions-with-joined-islands-in-Brid.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0035-Forbid-game-descriptions-with-joined-islands-in-Brid.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,64 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Fri, 10 Feb 2023 18:28:36 +0000
+Subject: [PATCH 035/159] Forbid game descriptions with joined islands in
+ Bridges
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=ad2fb760fc881d1ceb1ac1151bf60b85deb16c71
+Bug-Debian: https://bugs.debian.org/1034190
+
+A game description with islands in adjacent grid squares, like
+"3x3:11g", shouldn't be allowed.  If it is, then bridges between the
+islands are invisible and clicking one of them causes an assertion
+failure: "Assertion `is_loop->adj.points[j].off > 1' failed."
+
+The code to check this is really rather complex, but I think the
+complexity is mostly necessary.
+---
+ bridges.c | 27 ++++++++++++++++++++-------
+ 1 file changed, 20 insertions(+), 7 deletions(-)
+
+diff --git a/bridges.c b/bridges.c
+index 32263348..c95b38c6 100644
+--- a/bridges.c
++++ b/bridges.c
+@@ -2007,21 +2007,34 @@ static char *new_game_desc(const game_params *params, random_state *rs,
+ 
+ static const char *validate_desc(const game_params *params, const char *desc)
+ {
+-    int i, wh = params->w * params->h, nislands = 0;
++    int i, j, wh = params->w * params->h, nislands = 0;
++    bool *last_row = snewn(params->w, bool);
+ 
++    memset(last_row, 0, params->w * sizeof(bool));
+     for (i = 0; i < wh; i++) {
+-        if (*desc >= '1' && *desc <= '9')
++        if ((*desc >= '1' && *desc <= '9') || (*desc >= 'A' && *desc <= 'G')) {
+             nislands++;
+-        else if (*desc >= 'a' && *desc <= 'z')
++            /* Look for other islands to the left and above. */
++            if ((i % params->w > 0 && last_row[i % params->w - 1]) ||
++                last_row[i % params->w]) {
++                sfree(last_row);
++                return "Game description contains joined islands";
++            }
++            last_row[i % params->w] = true;
++        } else if (*desc >= 'a' && *desc <= 'z') {
++            for (j = 0; j < *desc - 'a' + 1; j++)
++                last_row[(i + j) % params->w] = false;
+             i += *desc - 'a'; /* plus the i++ */
+-        else if (*desc >= 'A' && *desc <= 'G')
+-            nislands++;
+-        else if (!*desc)
++        } else if (!*desc) {
++            sfree(last_row);
+             return "Game description shorter than expected";
+-        else
++        } else {
++            sfree(last_row);
+             return "Game description contains unexpected character";
++        }
+         desc++;
+     }
++    sfree(last_row);
+     if (*desc || i > wh)
+         return "Game description longer than expected";
+     if (nislands < 2)
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0037-Check-state-is-valid-at-the-end-of-a-move-in-Pearl.patch sgt-puzzles-20230122.806ae71/debian/patches/0037-Check-state-is-valid-at-the-end-of-a-move-in-Pearl.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0037-Check-state-is-valid-at-the-end-of-a-move-in-Pearl.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0037-Check-state-is-valid-at-the-end-of-a-move-in-Pearl.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,131 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 11 Feb 2023 21:22:49 +0000
+Subject: [PATCH 037/159] Check state is valid at the end of a move in Pearl
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=c0b2f0fc98e87392dcb4dd8faf3076786fc49367
+Bug-Debian: https://bugs.debian.org/1034190
+
+A Pearl move string contains a sequence of sub-moves, each of which
+can affect the state of the connection between the centre of a square
+and one of its edges.  interpret_move() generates these in pairs so
+that the two halves of a connection between the centres of adjacent
+squares stay in the same state.
+
+If, however, a save file contains mismatched half-moves,
+execute_move() should ideally return NULL rather than causing an
+assertion failure.  This has to be checked at the end of the whole
+move string, so I've arranged for check_completion() to return a
+boolean indicating whether the current state (and hence the move
+preceding it) is valid.  It now returns 'false' when a connection
+stops at a square boundary or when it goes off the board.  These
+conditions used to be assertion failures, and now they just cause the
+move to be rejected.
+
+This supersedes the check for off-board connections added in 15f4fa8,
+since now check_completion() can check for off-board links for the
+whole board at once.
+
+This save file trivially demonstrates the problem, causing
+"dsf_update_completion: Assertion `state->lines[bc] & F(dir)' failed"
+without this fix:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :5:Pearl
+PARAMS  :5:6x6t0
+CPARAMS :5:6x6t0
+DESC    :17:BbBfWceBbWaBWWgWB
+NSTATES :1:2
+STATEPOS:1:2
+MOVE    :6:R1,0,0
+---
+ pearl.c | 37 ++++++++++++++++++-------------------
+ 1 file changed, 18 insertions(+), 19 deletions(-)
+
+diff --git a/pearl.c b/pearl.c
+index f6617a28..29dd9cd7 100644
+--- a/pearl.c
++++ b/pearl.c
+@@ -1525,25 +1525,30 @@ static char nbits[16] = { 0, 1, 1, 2,
+ 
+ #define ERROR_CLUE 16
+ 
+-static void dsf_update_completion(game_state *state, int ax, int ay, char dir,
++/* Returns false if the state is invalid. */
++static bool dsf_update_completion(game_state *state, int ax, int ay, char dir,
+                                  int *dsf)
+ {
+     int w = state->shared->w /*, h = state->shared->h */;
+     int ac = ay*w+ax, bx, by, bc;
+ 
+-    if (!(state->lines[ac] & dir)) return; /* no link */
++    if (!(state->lines[ac] & dir)) return true; /* no link */
+     bx = ax + DX(dir); by = ay + DY(dir);
+ 
+-    assert(INGRID(state, bx, by)); /* should not have a link off grid */
++    if (!INGRID(state, bx, by))
++        return false; /* should not have a link off grid */
+ 
+     bc = by*w+bx;
+-    assert(state->lines[bc] & F(dir)); /* should have reciprocal link */
+-    if (!(state->lines[bc] & F(dir))) return;
++    if (!(state->lines[bc] & F(dir)))
++        return false; /* should have reciprocal link */
++    if (!(state->lines[bc] & F(dir))) return true;
+ 
+     dsf_merge(dsf, ac, bc);
++    return true;
+ }
+ 
+-static void check_completion(game_state *state, bool mark)
++/* Returns false if the state is invalid. */
++static bool check_completion(game_state *state, bool mark)
+ {
+     int w = state->shared->w, h = state->shared->h, x, y, i, d;
+     bool had_error = false;
+@@ -1571,8 +1576,11 @@ static void check_completion(game_state *state, bool mark)
+     /* Build the dsf. */
+     for (x = 0; x < w; x++) {
+         for (y = 0; y < h; y++) {
+-            dsf_update_completion(state, x, y, R, dsf);
+-            dsf_update_completion(state, x, y, D, dsf);
++            if (!dsf_update_completion(state, x, y, R, dsf) ||
++                !dsf_update_completion(state, x, y, D, dsf)) {
++                sfree(dsf);
++                return false;
++            }
+         }
+     }
+ 
+@@ -1727,6 +1735,7 @@ static void check_completion(game_state *state, bool mark)
+         if (!had_error)
+             state->completed = true;
+     }
++    return true;
+ }
+ 
+ /* completion check:
+@@ -2307,16 +2316,6 @@ static game_state *execute_move(const game_state *state, const char *move)
+                 (ret->marks[y*w + x] & (char)l))
+                 goto badmove;
+ 
+-            /*
+-             * Similarly, if we've ended up with a line or mark going
+-             * off the board, that's not acceptable.
+-             */
+-            for (l = 1; l <= 8; l <<= 1)
+-                if (((ret->lines[y*w + x] & (char)l) ||
+-                     (ret->marks[y*w + x] & (char)l)) &&
+-                    !INGRID(state, x+DX(l), y+DY(l)))
+-                    goto badmove;
+-
+             move += n;
+         } else if (strcmp(move, "H") == 0) {
+             pearl_solve(ret->shared->w, ret->shared->h,
+@@ -2333,7 +2332,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+             goto badmove;
+     }
+ 
+-    check_completion(ret, true);
++    if (!check_completion(ret, true)) goto badmove;
+ 
+     return ret;
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0038-Cleanly-reject-more-ill-formed-solve-moves-in-Flood.patch sgt-puzzles-20230122.806ae71/debian/patches/0038-Cleanly-reject-more-ill-formed-solve-moves-in-Flood.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0038-Cleanly-reject-more-ill-formed-solve-moves-in-Flood.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0038-Cleanly-reject-more-ill-formed-solve-moves-in-Flood.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,47 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 11 Feb 2023 22:00:49 +0000
+Subject: [PATCH 038/159] Cleanly reject more ill-formed solve moves in Flood
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=896a73bd7ff8cbde44e97d89cef57346478f0072
+Bug-Debian: https://bugs.debian.org/1034190
+
+The fix in e4112b3 was incomplete: there was another assertion that could be failed by a save file with an ill-formed solve move.  That now gets rejected properly.  Here's an example save file to demonstrate the problem:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :5:Flood
+PARAMS  :7:6x6c6m0
+CPARAMS :7:6x6c6m0
+DESC    :39:000000000000000000000000000000000000,00
+NSTATES :1:2
+STATEPOS:1:2
+MOVE    :1:S
+---
+ flood.c | 13 +++++++------
+ 1 file changed, 7 insertions(+), 6 deletions(-)
+
+diff --git a/flood.c b/flood.c
+index 8f4df003..f1ac5e1c 100644
+--- a/flood.c
++++ b/flood.c
+@@ -938,15 +938,16 @@ static game_state *execute_move(const game_state *state, const char *move)
+ 
+         sol->moves = snewn(sol->nmoves, char);
+         for (i = 0, p = move; i < sol->nmoves; i++) {
+-            assert(*p);
++            if (!*p) {
++              badsolve:
++                sfree(sol->moves);
++                sfree(sol);
++                return NULL;
++            };
+             sol->moves[i] = atoi(p);
+             p += strspn(p, "0123456789");
+             if (*p) {
+-                if (*p != ',') {
+-                    sfree(sol->moves);
+-                    sfree(sol);
+-                    return NULL;
+-                }
++                if (*p != ',') goto badsolve;
+                 p++;
+             }
+         }
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0039-Don-t-allow-moves-that-change-the-constraints-in-Une.patch sgt-puzzles-20230122.806ae71/debian/patches/0039-Don-t-allow-moves-that-change-the-constraints-in-Une.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0039-Don-t-allow-moves-that-change-the-constraints-in-Une.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0039-Don-t-allow-moves-that-change-the-constraints-in-Une.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,53 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 11 Feb 2023 22:49:36 +0000
+Subject: [PATCH 039/159] Don't allow moves that change the constraints in
+ Unequal
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=97b03cc67a31c1d0869a21c50b9ca31f78775ff9
+Bug-Debian: https://bugs.debian.org/1034190
+
+Unequal has a flags word per cell.  Some of those flags are fixed,
+like the locations of the ">" signs, but others indicate errors and
+are used to allow the player to mark clues as "spent".  Move strings
+beginning with "F" allow the user to change the "spent" flags, but
+they shouldn't allow the user to change any other flags, especially
+those marking the constraints.
+
+Without this fix, the following save file gives a "solver_nminmax:
+Assertion `x >= 0 && y >= 0 && x < o && y < o' failed" after it adds a
+clue that points off the board:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+GAME    :7:Unequal
+PARAMS  :3:3e0
+CPARAMS :3:3e0
+DESC    :17:0,0,0,0,0,0,0,0,0
+NSTATES :2:3
+STATEPOS:1:3
+MOVE    :6:F2,0,4
+MOVE    :1:H
+---
+ unequal.c | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/unequal.c b/unequal.c
+index 52041bbd..e0e08a12 100644
+--- a/unequal.c
++++ b/unequal.c
+@@ -84,6 +84,7 @@ struct game_params {
+ #define ADJ_TO_SPENT(x) ((x) << 9)
+ 
+ #define F_ERROR_MASK (F_ERROR|F_ERROR_UP|F_ERROR_RIGHT|F_ERROR_DOWN|F_ERROR_LEFT)
++#define F_SPENT_MASK (F_SPENT_UP|F_SPENT_RIGHT|F_SPENT_DOWN|F_SPENT_LEFT)
+ 
+ struct game_state {
+     int order;
+@@ -1706,7 +1707,8 @@ static game_state *execute_move(const game_state *state, const char *move)
+         check_complete(ret->nums, ret, true);
+         return ret;
+     } else if (move[0] == 'F' && sscanf(move+1, "%d,%d,%d", &x, &y, &n) == 3 &&
+-	       x >= 0 && x < state->order && y >= 0 && y < state->order) {
++	       x >= 0 && x < state->order && y >= 0 && y < state->order &&
++               (n & ~F_SPENT_MASK) == 0) {
+ 	ret = dup_game(state);
+ 	GRID(ret, flags, x, y) ^= n;
+ 	return ret;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0041-Fix-memory-leaks-in-Keen-s-validate_desc.patch sgt-puzzles-20230122.806ae71/debian/patches/0041-Fix-memory-leaks-in-Keen-s-validate_desc.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0041-Fix-memory-leaks-in-Keen-s-validate_desc.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0041-Fix-memory-leaks-in-Keen-s-validate_desc.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,52 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 09:31:03 +0000
+Subject: [PATCH 041/159] Fix memory leaks in Keen's validate_desc()
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=bb31efdbc91495a4385ca0afffa4c8bb8f564d7b
+Bug-Debian: https://bugs.debian.org/1034190
+
+Keen uses a DSF to validate its game descriptions and almost always
+failed to free it, even when the validation succeeded.
+---
+ keen.c | 11 +++++++++--
+ 1 file changed, 9 insertions(+), 2 deletions(-)
+
+diff --git a/keen.c b/keen.c
+index be06013d..98ddc0c4 100644
+--- a/keen.c
++++ b/keen.c
+@@ -1310,8 +1310,10 @@ static const char *validate_desc(const game_params *params, const char *desc)
+ 	return ret;
+     }
+ 
+-    if (*p != ',')
++    if (*p != ',') {
++        sfree(dsf);
+ 	return "Expected ',' after block structure description";
++    }
+     p++;
+ 
+     /*
+@@ -1323,17 +1325,22 @@ static const char *validate_desc(const game_params *params, const char *desc)
+ 	    if (*p == 'a' || *p == 'm') {
+ 		/* these clues need no validation */
+ 	    } else if (*p == 'd' || *p == 's') {
+-		if (dsf_size(dsf, i) != 2)
++		if (dsf_size(dsf, i) != 2) {
++                    sfree(dsf);
+ 		    return "Subtraction and division blocks must have area 2";
++                }
+ 	    } else if (!*p) {
++                sfree(dsf);
+ 		return "Too few clues for block structure";
+ 	    } else {
++                sfree(dsf);
+ 		return "Unrecognised clue type";
+ 	    }
+ 	    p++;
+ 	    while (*p && isdigit((unsigned char)*p)) p++;
+ 	}
+     }
++    sfree(dsf);
+     if (*p)
+ 	return "Too many clues for block structure";
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0042-Remember-to-free-the-actual_board-array-in-Mosaic.patch sgt-puzzles-20230122.806ae71/debian/patches/0042-Remember-to-free-the-actual_board-array-in-Mosaic.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0042-Remember-to-free-the-actual_board-array-in-Mosaic.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0042-Remember-to-free-the-actual_board-array-in-Mosaic.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,22 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 09:36:27 +0000
+Subject: [PATCH 042/159] Remember to free the actual_board array in Mosaic
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=0f20b7226957a2fceff37a67b0d9874c7db3a7fc
+Bug-Debian: https://bugs.debian.org/1034190
+
+---
+ mosaic.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/mosaic.c b/mosaic.c
+index f3a32167..842c211c 100644
+--- a/mosaic.c
++++ b/mosaic.c
+@@ -925,6 +925,7 @@ static void free_game(game_state *state)
+     sfree(state->cells_contents);
+     state->cells_contents = NULL;
+     if (state->board->references <= 1) {
++        sfree(state->board->actual_board);
+         sfree(state->board);
+         state->board = NULL;
+     } else {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0043-Don-t-leak-grids-in-Loopy-s-validate_desc.patch sgt-puzzles-20230122.806ae71/debian/patches/0043-Don-t-leak-grids-in-Loopy-s-validate_desc.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0043-Don-t-leak-grids-in-Loopy-s-validate_desc.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0043-Don-t-leak-grids-in-Loopy-s-validate_desc.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,35 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 09:45:08 +0000
+Subject: [PATCH 043/159] Don't leak grids in Loopy's validate_desc()
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=11b631ea870355306c4b1d03458bb3cea8f29188
+Bug-Debian: https://bugs.debian.org/1034190
+
+---
+ loopy.c | 9 +++++++--
+ 1 file changed, 7 insertions(+), 2 deletions(-)
+
+diff --git a/loopy.c b/loopy.c
+index 960fafad..1e041f92 100644
+--- a/loopy.c
++++ b/loopy.c
+@@ -791,13 +791,18 @@ static const char *validate_desc(const game_params *params, const char *desc)
+             count += *desc - 'a' + 1;
+             continue;
+         }
++        grid_free(g);
+         return "Unknown character in description";
+     }
+ 
+-    if (count < g->num_faces)
++    if (count < g->num_faces) {
++        grid_free(g);
+         return "Description too short for board size";
+-    if (count > g->num_faces)
++    }
++    if (count > g->num_faces) {
++        grid_free(g);
+         return "Description too long for board size";
++    }
+ 
+     grid_free(g);
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0044-Remember-to-free-the-to_draw-member-from-Net-s-draws.patch sgt-puzzles-20230122.806ae71/debian/patches/0044-Remember-to-free-the-to_draw-member-from-Net-s-draws.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0044-Remember-to-free-the-to_draw-member-from-Net-s-draws.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0044-Remember-to-free-the-to_draw-member-from-Net-s-draws.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,23 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 09:48:16 +0000
+Subject: [PATCH 044/159] Remember to free the to_draw member from Net's
+ drawstate
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=493bf16ddbe2185664d6c3053f7891a9f232c75c
+Bug-Debian: https://bugs.debian.org/1034190
+
+---
+ net.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/net.c b/net.c
+index cff0c9c2..9ac22707 100644
+--- a/net.c
++++ b/net.c
+@@ -2464,6 +2464,7 @@ static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
+ static void game_free_drawstate(drawing *dr, game_drawstate *ds)
+ {
+     sfree(ds->visible);
++    sfree(ds->to_draw);
+     sfree(ds);
+ }
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0045-Undead-check-the-return-value-of-sscanf-in-execute_m.patch sgt-puzzles-20230122.806ae71/debian/patches/0045-Undead-check-the-return-value-of-sscanf-in-execute_m.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0045-Undead-check-the-return-value-of-sscanf-in-execute_m.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0045-Undead-check-the-return-value-of-sscanf-in-execute_m.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,31 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 10:04:47 +0000
+Subject: [PATCH 045/159] Undead: check the return value of sscanf() in
+ execute_move()
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=0a7c531e8f4c1970662f7c30aea006e65d5ff010
+Bug-Debian: https://bugs.debian.org/1034190
+
+sscanf() assigns its output in order, so if a conversion specifier fails
+to match, a later "%n" specifier will also not get its result assigned.
+In Undead's execute_move(), this led to the result of "%n" being used
+without being initialised.  That could cause it to try to parse
+arbitrary memory as part of the move string, which shouldn't be a
+security problem (since execute_move() handles untrusted input anyway),
+but could lead to a crash and certainly wasn't helpful.
+---
+ undead.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/undead.c b/undead.c
+index 40339ae0..6feca640 100644
+--- a/undead.c
++++ b/undead.c
+@@ -2083,7 +2083,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+         } else if (c == 'G' || c == 'V' || c == 'Z' || c == 'E' ||
+                    c == 'g' || c == 'v' || c == 'z') {
+             move++;
+-            sscanf(move, "%d%n", &x, &n);
++            if (sscanf(move, "%d%n", &x, &n) != 1) goto badmove;
+             if (x < 0 || x >= ret->common->num_total) goto badmove;
+             if (c == 'G') ret->guess[x] = 1;
+             if (c == 'V') ret->guess[x] = 2;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0046-Don-t-leak-duplicate-edges-in-Untangle.patch sgt-puzzles-20230122.806ae71/debian/patches/0046-Don-t-leak-duplicate-edges-in-Untangle.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0046-Don-t-leak-duplicate-edges-in-Untangle.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0046-Don-t-leak-duplicate-edges-in-Untangle.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,29 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 11:08:14 +0000
+Subject: [PATCH 046/159] Don't leak duplicate edges in Untangle
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=19401e95e0a75577103e9c1a877611234a0d8ab5
+Bug-Debian: https://bugs.debian.org/1034190
+
+Untangle game descriptions are allowed to contain duplicate edges, and
+add234() can handle deduping them.  However, when add234() reports that
+your newly-allocated edge is a duplicate, it's important to free it
+again.
+---
+ untangle.c | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/untangle.c b/untangle.c
+index 1dc4fb30..34924ae6 100644
+--- a/untangle.c
++++ b/untangle.c
+@@ -420,7 +420,9 @@ static void addedge(tree234 *edges, int a, int b)
+     e->a = min(a, b);
+     e->b = max(a, b);
+ 
+-    add234(edges, e);
++    if (add234(edges, e) != e)
++        /* Duplicate edge. */
++        sfree(e);
+ }
+ 
+ static bool isedge(tree234 *edges, int a, int b)
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0047-Remember-to-free-the-numcolours-array-from-Pattern-s.patch sgt-puzzles-20230122.806ae71/debian/patches/0047-Remember-to-free-the-numcolours-array-from-Pattern-s.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0047-Remember-to-free-the-numcolours-array-from-Pattern-s.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0047-Remember-to-free-the-numcolours-array-from-Pattern-s.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,23 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 11:13:03 +0000
+Subject: [PATCH 047/159] Remember to free the numcolours array from Pattern's
+ drawstate
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=1aa67e7a75ac21d15780d6aaab65a2c0f6f65198
+Bug-Debian: https://bugs.debian.org/1034190
+
+---
+ pattern.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/pattern.c b/pattern.c
+index 68862df8..1342cb64 100644
+--- a/pattern.c
++++ b/pattern.c
+@@ -1738,6 +1738,7 @@ static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
+ static void game_free_drawstate(drawing *dr, game_drawstate *ds)
+ {
+     sfree(ds->visible);
++    sfree(ds->numcolours);
+     sfree(ds->strbuf);
+     sfree(ds);
+ }
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0048-Free-new-game_state-properly-in-Mosaic-s-execute_mov.patch sgt-puzzles-20230122.806ae71/debian/patches/0048-Free-new-game_state-properly-in-Mosaic-s-execute_mov.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0048-Free-new-game_state-properly-in-Mosaic-s-execute_mov.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0048-Free-new-game_state-properly-in-Mosaic-s-execute_mov.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,48 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 11:21:11 +0000
+Subject: [PATCH 048/159] Free new game_state properly in Mosaic's
+ execute_move()
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=d577aaecab09506988a657fa257c4d0ab85d0cd6
+Bug-Debian: https://bugs.debian.org/1034190
+
+Using sfree() rather than free_game() in the error paths meant that
+various arrays referenced from the game_state weren't properly freed.
+Also one error path didn't free the game_state at all.
+---
+ mosaic.c | 8 +++++---
+ 1 file changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/mosaic.c b/mosaic.c
+index 842c211c..74a06d0f 100644
+--- a/mosaic.c
++++ b/mosaic.c
+@@ -1282,8 +1282,10 @@ static game_state *execute_move(const game_state *state, const char *move)
+         move_params[i] = atoi(p);
+         while (*p && isdigit((unsigned char)*p)) p++;
+         if (i+1 < nparams) {
+-            if (*p != ',')
++            if (*p != ',') {
++                free_game(new_state);
+                 return NULL;
++            }
+             p++;
+         }
+     }
+@@ -1299,7 +1301,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+         }
+         cell = get_coords(new_state, new_state->cells_contents, x, y);
+         if (cell == NULL) {
+-            sfree(new_state);
++            free_game(new_state);
+             return NULL;
+         }
+         if (*cell >= STATE_OK_NUM) {
+@@ -1369,7 +1371,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+             cell = get_coords(new_state, new_state->cells_contents,
+                               x + (dirX * i), y + (dirY * i));
+             if (cell == NULL) {
+-                sfree(new_state);
++                free_game(new_state);
+                 return NULL;
+             }
+             if ((*cell & STATE_OK_NUM) == 0) {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0049-Twiddle-don-t-read-off-the-end-of-parameter-strings-.patch sgt-puzzles-20230122.806ae71/debian/patches/0049-Twiddle-don-t-read-off-the-end-of-parameter-strings-.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0049-Twiddle-don-t-read-off-the-end-of-parameter-strings-.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0049-Twiddle-don-t-read-off-the-end-of-parameter-strings-.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,37 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 14:31:39 +0000
+Subject: [PATCH 049/159] Twiddle: don't read off the end of parameter strings
+ ending 'm'
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=73c7bc090155ab8c4661feaeea9e6a6e74ee6f77
+Bug-Debian: https://bugs.debian.org/1034190
+
+The overrun could be demonstrated by specifying a parameter string of
+"3x3m" to a build with AddressSanitizer.
+---
+ twiddle.c | 8 +++++---
+ 1 file changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/twiddle.c b/twiddle.c
+index 6d86264c..8c565a04 100644
+--- a/twiddle.c
++++ b/twiddle.c
+@@ -124,14 +124,16 @@ static void decode_params(game_params *ret, char const *string)
+     while (*string) {
+ 	if (*string == 'r') {
+ 	    ret->rowsonly = true;
++            string++;
+ 	} else if (*string == 'o') {
+ 	    ret->orientable = true;
++            string++;
+ 	} else if (*string == 'm') {
+             string++;
+ 	    ret->movetarget = atoi(string);
+-            while (string[1] && isdigit((unsigned char)string[1])) string++;
+-	}
+-	string++;
++            while (*string && isdigit((unsigned char)*string)) string++;
++	} else
++            string++;
+     }
+ }
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0050-Loopy-free-the-grid-description-string-if-it-s-inval.patch sgt-puzzles-20230122.806ae71/debian/patches/0050-Loopy-free-the-grid-description-string-if-it-s-inval.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0050-Loopy-free-the-grid-description-string-if-it-s-inval.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0050-Loopy-free-the-grid-description-string-if-it-s-inval.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,31 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 20:17:58 +0000
+Subject: [PATCH 050/159] Loopy: free the grid description string if it's
+ invalid
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=e336513be755159158c5ba017c91b018ad4cd36c
+Bug-Debian: https://bugs.debian.org/1034190
+
+---
+ loopy.c | 7 +++++--
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+diff --git a/loopy.c b/loopy.c
+index 1e041f92..18da2e82 100644
+--- a/loopy.c
++++ b/loopy.c
+@@ -777,10 +777,13 @@ static const char *validate_desc(const game_params *params, const char *desc)
+      * know is the precise number of faces. */
+     grid_desc = extract_grid_desc(&desc);
+     ret = grid_validate_desc(grid_types[params->type], params->w, params->h, grid_desc);
+-    if (ret) return ret;
++    if (ret) {
++        sfree(grid_desc);
++        return ret;
++    }
+ 
+     g = loopy_generate_grid(params, grid_desc);
+-    if (grid_desc) sfree(grid_desc);
++    sfree(grid_desc);
+ 
+     for (; *desc; ++desc) {
+         if ((*desc >= '0' && *desc <= '9') || (*desc >= 'A' && *desc <= 'Z')) {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0051-Mosaic-don-t-duplicate-the-description-being-validat.patch sgt-puzzles-20230122.806ae71/debian/patches/0051-Mosaic-don-t-duplicate-the-description-being-validat.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0051-Mosaic-don-t-duplicate-the-description-being-validat.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0051-Mosaic-don-t-duplicate-the-description-being-validat.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,45 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 21:00:11 +0000
+Subject: [PATCH 051/159] Mosaic: don't duplicate the description being
+ validated
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=da2767a3f9bf4abb0436157972366202ad53a407
+Bug-Debian: https://bugs.debian.org/1034190
+
+Mosaic's validate_desc() doesn't write to the description string, so
+it has no need to make a copy of it.  And if it doesn't copy it, it
+can't leak the copy.
+---
+ mosaic.c | 13 +++++--------
+ 1 file changed, 5 insertions(+), 8 deletions(-)
+
+diff --git a/mosaic.c b/mosaic.c
+index 74a06d0f..39d368a0 100644
+--- a/mosaic.c
++++ b/mosaic.c
+@@ -832,21 +832,18 @@ static const char *validate_desc(const game_params *params,
+                                  const char *desc)
+ {
+     int size_dest = params->height * params->width;
+-    char *curr_desc = dupstr(desc);
+-    char *desc_base = curr_desc;
+     int length;
+     length = 0;
+ 
+-    while (*curr_desc != '\0') {
+-        if (*curr_desc >= 'a' && *curr_desc <= 'z') {
+-            length += *curr_desc - 'a';
+-        } else if (*curr_desc < '0' || *curr_desc > '9')
++    while (*desc != '\0') {
++        if (*desc >= 'a' && *desc <= 'z') {
++            length += *desc - 'a';
++        } else if (*desc < '0' || *desc > '9')
+             return "Invalid character in game description";
+         length++;
+-        curr_desc++;
++        desc++;
+     }
+ 
+-    sfree(desc_base);
+     if (length != size_dest) {
+         return "Desc size mismatch";
+     }
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0052-Avoid-division-by-zero-in-Cube-grid-size-checks.patch sgt-puzzles-20230122.806ae71/debian/patches/0052-Avoid-division-by-zero-in-Cube-grid-size-checks.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0052-Avoid-division-by-zero-in-Cube-grid-size-checks.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0052-Avoid-division-by-zero-in-Cube-grid-size-checks.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,34 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 00:14:22 +0000
+Subject: [PATCH 052/159] Avoid division by zero in Cube grid-size checks
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=df783b93e3271264a8d54f90876f41a80ef2247d
+Bug-Debian: https://bugs.debian.org/1034190
+
+On a triangular grid, Cube allows either d1 or d2 (but not both) to be
+zero, so it's important to check that each one is not zero before
+dividing by it.
+
+The crash could be triggered by, for instance "cube t0x2".
+---
+ cube.c | 8 +++++---
+ 1 file changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/cube.c b/cube.c
+index 1969bfb9..99f392aa 100644
+--- a/cube.c
++++ b/cube.c
+@@ -567,9 +567,11 @@ static const char *validate_params(const game_params *params, bool full)
+          * can safely multiply them and compare against the
+          * _remaining_ space.
+          */
+-        if ((params->d1 > INT_MAX / params->d1) ||
+-            (params->d2 > (INT_MAX - params->d1*params->d1) / params->d2) ||
+-            (params->d1*params->d2 > (INT_MAX - params->d1*params->d1 -
++        if ((params->d1 > 0 && params->d1 > INT_MAX / params->d1) ||
++            (params->d2 > 0 &&
++             params->d2 > (INT_MAX - params->d1*params->d1) / params->d2) ||
++            (params->d2 > 0 &&
++             params->d1*params->d2 > (INT_MAX - params->d1*params->d1 -
+                                       params->d2*params->d2) / params->d2))
+ 	    return "Grid area must not be unreasonably large";
+     }
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0055-Validate-that-save-file-values-are-ASCII-mostly.patch sgt-puzzles-20230122.806ae71/debian/patches/0055-Validate-that-save-file-values-are-ASCII-mostly.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0055-Validate-that-save-file-values-are-ASCII-mostly.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0055-Validate-that-save-file-values-are-ASCII-mostly.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,39 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sun, 12 Feb 2023 23:04:12 +0000
+Subject: [PATCH 055/159] Validate that save file values are ASCII (mostly)
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=c3a5a7842eb6c41fb75a8a110a3f2cbc1c8fc5d9
+Bug-Debian: https://bugs.debian.org/1034190
+
+Apart from "SEED" records, all values in save files generated by Puzzles
+should be printable ASCII.  This is enforced by assertion in the saving
+code.  However, if a save file with non-ASCII move strings (for
+instance) manages to get loaded then these non-ASCII values can cause an
+assertion failure on saving.  Instead, the loading code now checks
+values for ASCIIness.
+
+This will not only avoid problems when re-saving files, but will also
+defend the various internal parsers from at least some evil strings.  It
+shouldn't invalidate any save files actually generated by Puzzles, but
+it will sadly invalidate some of my fuzzing corpus.
+---
+ midend.c | 7 +++++++
+ 1 file changed, 7 insertions(+)
+
+diff --git a/midend.c b/midend.c
+index 17690959..e71c27e7 100644
+--- a/midend.c
++++ b/midend.c
+@@ -2329,6 +2329,13 @@ static const char *midend_deserialise_internal(
+             goto cleanup;
+         }
+         val[len] = '\0';
++        /* Validate that all values (apart from SEED) are printable ASCII. */
++        if (strcmp(key, "SEED"))
++            for (i = 0; val[i]; i++)
++                if (val[i] < 32 || val[i] >= 127) {
++                    ret = "Forbidden characters in saved game file";
++                    goto cleanup;
++                }
+ 
+         if (!started) {
+             if (strcmp(key, "SAVEFILE") || strcmp(val, SERIALISE_MAGIC)) {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0056-More-validation-of-solve-moves-in-Flood.patch sgt-puzzles-20230122.806ae71/debian/patches/0056-More-validation-of-solve-moves-in-Flood.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0056-More-validation-of-solve-moves-in-Flood.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0056-More-validation-of-solve-moves-in-Flood.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,31 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 00:00:30 +0000
+Subject: [PATCH 056/159] More validation of solve moves in Flood
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=e8668dc883e940f0852ff4520abc3d30cae90aef
+Bug-Debian: https://bugs.debian.org/1034190
+
+To avoid assertion failures while painting it, we need to ensure that
+the purported solution in a solve move doesn't include filling with the
+current top-left colour at any point.  That means checking the first
+entry against the current top-left colours, and each later one against
+its predecessor.
+---
+ flood.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+diff --git a/flood.c b/flood.c
+index f1ac5e1c..7a83e524 100644
+--- a/flood.c
++++ b/flood.c
+@@ -945,6 +945,11 @@ static game_state *execute_move(const game_state *state, const char *move)
+                 return NULL;
+             };
+             sol->moves[i] = atoi(p);
++            if (i == 0 ?
++                sol->moves[i] == state->grid[FILLY * state->w + FILLX] :
++                sol->moves[i] == sol->moves[i-1])
++                /* Solution contains a fill with the current colour. */
++                goto badsolve;
+             p += strspn(p, "0123456789");
+             if (*p) {
+                 if (*p != ',') goto badsolve;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0058-Make-sure-that-moves-in-Flood-use-only-valid-colours.patch sgt-puzzles-20230122.806ae71/debian/patches/0058-Make-sure-that-moves-in-Flood-use-only-valid-colours.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0058-Make-sure-that-moves-in-Flood-use-only-valid-colours.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0058-Make-sure-that-moves-in-Flood-use-only-valid-colours.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,56 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Tue, 14 Feb 2023 22:02:35 +0000
+Subject: [PATCH 058/159] Make sure that moves in Flood use only valid colours
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=7364ce8e266d947be146d635958a7b282752aac6
+Bug-Debian: https://bugs.debian.org/1034190
+
+If execute_move() receieves a move that uses a colour beyond the range
+for the current game, it now rejects it.  Without this a solve string
+containing an invalid colour would cause an assertion failure: "fill:
+Assertion `oldcolour != newcolour' failed."  While I was in the area I
+put a range check on colours for normal moves as well.  To demonstrate
+the problem, load this save file:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :5:Flood
+PARAMS  :7:6x6c6m5
+CPARAMS :7:6x6c6m3
+DESC    :39:432242034203340350204502505323231342,17
+NSTATES :1:2
+STATEPOS:1:2
+MOVE    :2:S6
+---
+ flood.c | 12 +++++++-----
+ 1 file changed, 7 insertions(+), 5 deletions(-)
+
+diff --git a/flood.c b/flood.c
+index 7a83e524..441119c6 100644
+--- a/flood.c
++++ b/flood.c
+@@ -886,7 +886,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+ 
+     if (move[0] == 'M' &&
+         sscanf(move+1, "%d", &c) == 1 &&
+-        c >= 0 &&
++        c >= 0 && c < state->colours &&
+         c != state->grid[FILLY * state->w + FILLX] &&
+         !state->complete) {
+         int *queue = snewn(state->w * state->h, int);
+@@ -945,10 +945,12 @@ static game_state *execute_move(const game_state *state, const char *move)
+                 return NULL;
+             };
+             sol->moves[i] = atoi(p);
+-            if (i == 0 ?
+-                sol->moves[i] == state->grid[FILLY * state->w + FILLX] :
+-                sol->moves[i] == sol->moves[i-1])
+-                /* Solution contains a fill with the current colour. */
++            if (sol->moves[i] < 0 || sol->moves[i] >= state->colours ||
++                (i == 0 ?
++                 sol->moves[i] == state->grid[FILLY * state->w + FILLX] :
++                 sol->moves[i] == sol->moves[i-1]))
++                /* Solution contains a fill with an invalid colour or
++                 * the current colour. */
+                 goto badsolve;
+             p += strspn(p, "0123456789");
+             if (*p) {
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0059-Tighten-grid-size-limit-in-Mines.patch sgt-puzzles-20230122.806ae71/debian/patches/0059-Tighten-grid-size-limit-in-Mines.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0059-Tighten-grid-size-limit-in-Mines.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0059-Tighten-grid-size-limit-in-Mines.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,48 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Wed, 15 Feb 2023 14:07:34 +0000
+Subject: [PATCH 059/159] Tighten grid-size limit in Mines
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=9394e9c74bdb48dc1c74693bcb41fd35f8fc743c
+Bug-Debian: https://bugs.debian.org/1034190
+
+Mines uses random_upto() to decide where to place mines, and
+random_upto() takes a maximum limit of 2^28-1, so limit the number of
+grid squares to that (or INT_MAX if someone's still trying to build on
+a 16-bit system).
+
+This avoids an assertion failure: "random_upto: Assertion `bits < 32'
+failed." which can be demonstrated by this save file:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :5:Mines
+PARAMS  :5:18090
+CPARAMS :5:18090
+DESC    :11:r9,u,MEdff6
+UI      :2:D0
+TIME    :1:0
+NSTATES :1:2
+STATEPOS:1:2
+MOVE    :4:O2,1
+---
+ mines.c | 7 +++++++
+ 1 file changed, 7 insertions(+)
+
+diff --git a/mines.c b/mines.c
+index 04364e5a..7ab4dbc3 100644
+--- a/mines.c
++++ b/mines.c
+@@ -265,7 +265,14 @@ static const char *validate_params(const game_params *params, bool full)
+ 	return "Width and height must both be at least one";
+     if (params->w > SHRT_MAX || params->h > SHRT_MAX)
+         return "Neither width nor height may be unreasonably large";
++    /*
++     * We use random_upto() to place mines, and its maximum limit is 2^28-1.
++     */
++#if (1<<28)-1 < INT_MAX
++    if (params->w > ((1<<28)-1) / params->h)
++#else
+     if (params->w > INT_MAX / params->h)
++#endif
+         return "Width times height must not be unreasonably large";
+     if (params->n < 0)
+ 	return "Mine count may not be negative";
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0061-Solo-cope-with-pencil-marks-when-tilesize-1.patch sgt-puzzles-20230122.806ae71/debian/patches/0061-Solo-cope-with-pencil-marks-when-tilesize-1.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0061-Solo-cope-with-pencil-marks-when-tilesize-1.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0061-Solo-cope-with-pencil-marks-when-tilesize-1.patch	2023-04-16 21:18:03.000000000 +0200
@@ -0,0 +1,32 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Thu, 16 Feb 2023 15:54:17 +0000
+Subject: [PATCH 061/159] Solo: cope with pencil marks when tilesize == 1
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=3cd51d001769c657ebb4184bd05343af4d7e12b1
+Bug-Debian: https://bugs.debian.org/905852
+
+Solo's layout calculations for pencil marks could fail with a tilesize
+of 1, generating an assertion failure: "draw_number: Assertion `pbest
+> 0' failed."  This was reported as Debian bug #905852.
+
+My solution is slightly silly, namely to change a ">" in the test for
+whether a new layout is the best so far to ">=".  This allows for
+finding a (terrible) layout even for tilesize == 1, and also has the
+side-effect of slightly preserring wide layouts over tall ones.
+Personally, I think that's an improvement.
+---
+ solo.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/solo.c b/solo.c
+index ff980730..732ca2e5 100644
+--- a/solo.c
++++ b/solo.c
+@@ -5133,7 +5133,7 @@ static void draw_number(drawing *dr, game_drawstate *ds,
+ 		fw = (pr - pl) / (float)pw;
+ 		fh = (pb - pt) / (float)ph;
+ 		fs = min(fw, fh);
+-		if (fs > bestsize) {
++		if (fs >= bestsize) {
+ 		    bestsize = fs;
+ 		    pbest = pw;
+ 		}
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0065-Tracks-set-drag_s-x-y-even-if-starting-off-grid.patch sgt-puzzles-20230122.806ae71/debian/patches/0065-Tracks-set-drag_s-x-y-even-if-starting-off-grid.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0065-Tracks-set-drag_s-x-y-even-if-starting-off-grid.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0065-Tracks-set-drag_s-x-y-even-if-starting-off-grid.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,30 @@
+From: Chris Boyle <chris@boyle.name>
+Date: Fri, 10 Feb 2023 15:23:33 +0000
+Subject: [PATCH 065/159] Tracks: set drag_s{x,y} even if starting off-grid
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=111db0743bd273c340dd683449076fd9ec5797f5
+Bug-Debian: https://bugs.debian.org/1034190
+
+Otherwise, if subsequent mouse/finger movement lines up with the previous
+drag attempt's start, then suddenly a drag is in progress from there, which
+is confusing.
+
+Fixes #588
+
+(cherry picked from Android port,
+commit 8ce1bbe460d70a915caf2dbeb30354d22dc8a8ef)
+---
+ tracks.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/tracks.c b/tracks.c
+index 6948b42c..0579d4bd 100644
+--- a/tracks.c
++++ b/tracks.c
+@@ -2267,6 +2267,7 @@ static char *interpret_move(const game_state *state, game_ui *ui,
+ 
+         if (!INGRID(state, gx, gy)) {
+             /* can't drag from off grid */
++            ui->drag_sx = ui->drag_sy = -1;
+             return NULL;
+         }
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0080-Undead-be-a-bit-more-careful-about-sprintf-buffer-si.patch sgt-puzzles-20230122.806ae71/debian/patches/0080-Undead-be-a-bit-more-careful-about-sprintf-buffer-si.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0080-Undead-be-a-bit-more-careful-about-sprintf-buffer-si.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0080-Undead-be-a-bit-more-careful-about-sprintf-buffer-si.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,33 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 18 Feb 2023 21:26:38 +0000
+Subject: [PATCH 080/159] Undead: be a bit more careful about sprintf buffer
+ sizes
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=448095ede815b1a63ddedc602c3ac768a0d52968
+Bug-Debian: https://bugs.debian.org/1034190
+
+---
+ undead.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/undead.c b/undead.c
+index 6feca640..78abf28e 100644
+--- a/undead.c
++++ b/undead.c
+@@ -2429,7 +2429,7 @@ static void draw_monster(drawing *dr, game_drawstate *ds, int x, int y,
+ static void draw_monster_count(drawing *dr, game_drawstate *ds,
+                                const game_state *state, int c, bool hflash) {
+     int dx,dy;
+-    char buf[8];
++    char buf[MAX_DIGITS(int) + 1];
+     char bufm[8];
+     
+     dy = TILESIZE/4;
+@@ -2474,7 +2474,7 @@ static void draw_path_hint(drawing *dr, game_drawstate *ds,
+                            const struct game_params *params,
+                            int hint_index, bool hflash, int hint) {
+     int x, y, color, dx, dy, text_dx, text_dy, text_size;
+-    char buf[4];
++    char buf[MAX_DIGITS(int) + 1];
+ 
+     if (ds->hint_errors[hint_index])
+         color = COL_ERROR;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0090-Fix-memory-leak-in-midend_game_id_int.patch sgt-puzzles-20230122.806ae71/debian/patches/0090-Fix-memory-leak-in-midend_game_id_int.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0090-Fix-memory-leak-in-midend_game_id_int.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0090-Fix-memory-leak-in-midend_game_id_int.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,34 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 20 Feb 2023 14:50:22 +0000
+Subject: [PATCH 090/159] Fix memory leak in midend_game_id_int()
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=4e09175fdaaffc0483fc9bf767311268794d956c
+Bug-Debian: https://bugs.debian.org/1034190
+
+The "par" string wasn't getting freed on some error paths.  Fixed by
+freeing it immediately after its last use, which is before any of the
+error paths.
+---
+ midend.c | 3 +--
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+diff --git a/midend.c b/midend.c
+index e71c27e7..f566251a 100644
+--- a/midend.c
++++ b/midend.c
+@@ -1801,6 +1801,7 @@ static const char *midend_game_id_int(midend *me, const char *id, int defmode)
+             newcurparams = me->ourgame->default_params();
+         }
+         me->ourgame->decode_params(newcurparams, par);
++        sfree(par);
+         error = me->ourgame->validate_params(newcurparams, desc == NULL);
+         if (error) {
+             me->ourgame->free_params(newcurparams);
+@@ -1876,8 +1877,6 @@ static const char *midend_game_id_int(midend *me, const char *id, int defmode)
+         me->genmode = GOT_SEED;
+     }
+ 
+-    sfree(par);
+-
+     me->newgame_can_store_undo = false;
+ 
+     return NULL;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0092-Flood-don-t-read-off-the-end-of-some-parameter-strin.patch sgt-puzzles-20230122.806ae71/debian/patches/0092-Flood-don-t-read-off-the-end-of-some-parameter-strin.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0092-Flood-don-t-read-off-the-end-of-some-parameter-strin.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0092-Flood-don-t-read-off-the-end-of-some-parameter-strin.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,37 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 20 Feb 2023 14:57:31 +0000
+Subject: [PATCH 092/159] Flood: don't read off the end of some parameter
+ strings
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=bbe866a3819c6a754a5b1d8c5bc5d0701796acfb
+Bug-Debian: https://bugs.debian.org/1034190
+
+This is essentially the same fix as 73c7bc090155ab8c was for Twiddle.
+The new code is less clever but more correct (and more obviously
+correct).  The bug could be demonstrated by using a parameter string
+of "c" or "m" with an AddressSanitizer build of Flood.
+---
+ flood.c | 8 ++++----
+ 1 file changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/flood.c b/flood.c
+index 441119c6..77eb48ae 100644
+--- a/flood.c
++++ b/flood.c
+@@ -141,13 +141,13 @@ static void decode_params(game_params *ret, char const *string)
+         if (*string == 'c') {
+             string++;
+ 	    ret->colours = atoi(string);
+-            while (string[1] && isdigit((unsigned char)string[1])) string++;
++            while (*string && isdigit((unsigned char)*string)) string++;
+ 	} else if (*string == 'm') {
+             string++;
+ 	    ret->leniency = atoi(string);
+-            while (string[1] && isdigit((unsigned char)string[1])) string++;
+-	}
+-	string++;
++            while (*string && isdigit((unsigned char)*string)) string++;
++	} else
++            string++;
+     }
+ }
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0101-Be-more-careful-with-type-of-left-operand-of.patch sgt-puzzles-20230122.806ae71/debian/patches/0101-Be-more-careful-with-type-of-left-operand-of.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0101-Be-more-careful-with-type-of-left-operand-of.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0101-Be-more-careful-with-type-of-left-operand-of.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,49 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sun, 26 Feb 2023 14:24:38 +0000
+Subject: [PATCH 101/159] Be more careful with type of left operand of <<
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=93be3f7ccaa63b0fd953bcfd88d685b47b76605e
+Bug-Debian: https://bugs.debian.org/1034190
+
+On a 32-bit system, evaluating 1<<31 causes undefined behaviour because
+1 is signed and so it produces signed overflow.  UBSan has spotted a
+couple of occasions where this happens in Puzzles, so in each case I've
+converted the left operand to the unsigned result type we actually want.
+---
+ cube.c   | 4 ++--
+ random.c | 4 ++--
+ 2 files changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/cube.c b/cube.c
+index 69a9d35e..3b4e9753 100644
+--- a/cube.c
++++ b/cube.c
+@@ -202,8 +202,8 @@ struct game_grid {
+ };
+ 
+ #define SET_SQUARE(state, i, val) \
+-    ((state)->bluemask[(i)/32] &= ~(1 << ((i)%32)), \
+-     (state)->bluemask[(i)/32] |= ((!!val) << ((i)%32)))
++    ((state)->bluemask[(i)/32] &= ~(1UL << ((i)%32)), \
++     (state)->bluemask[(i)/32] |= ((unsigned long)(!!val) << ((i)%32)))
+ #define GET_SQUARE(state, i) \
+     (((state)->bluemask[(i)/32] >> ((i)%32)) & 1)
+ 
+diff --git a/random.c b/random.c
+index 5527d6ff..bd11f166 100644
+--- a/random.c
++++ b/random.c
+@@ -254,12 +254,12 @@ unsigned long random_bits(random_state *state, int bits)
+     }
+ 
+     /*
+-     * `(1 << bits) - 1' is not good enough, since if bits==32 on a
++     * `(1UL << bits) - 1' is not good enough, since if bits==32 on a
+      * 32-bit machine, behaviour is undefined and Intel has a nasty
+      * habit of shifting left by zero instead. We'll shift by
+      * bits-1 and then separately shift by one.
+      */
+-    ret &= (1 << (bits-1)) * 2 - 1;
++    ret &= (1UL << (bits-1)) * 2 - 1;
+     return ret;
+ }
+ 
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0102-Map-reduce-maximum-size.patch sgt-puzzles-20230122.806ae71/debian/patches/0102-Map-reduce-maximum-size.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0102-Map-reduce-maximum-size.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0102-Map-reduce-maximum-size.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,25 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sun, 26 Feb 2023 14:51:09 +0000
+Subject: [PATCH 102/159] Map: reduce maximum size
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=e2d390aae872cee4cb16d746af3b2eeb7713cbf5
+Bug-Debian: https://bugs.debian.org/1034190
+
+validate_desc relies on being able to calculate 2*wh in an int, so the
+maximum grid size is at most INT_MAX/2.
+---
+ map.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/map.c b/map.c
+index 04783449..2d624b97 100644
+--- a/map.c
++++ b/map.c
+@@ -255,7 +255,7 @@ static const char *validate_params(const game_params *params, bool full)
+ {
+     if (params->w < 2 || params->h < 2)
+ 	return "Width and height must be at least two";
+-    if (params->w > INT_MAX / params->h)
++    if (params->w > INT_MAX / 2 / params->h)
+         return "Width times height must not be unreasonably large";
+     if (params->n < 5)
+ 	return "Must have at least five regions";
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0103-Correctly-handle-some-short-save-files.patch sgt-puzzles-20230122.806ae71/debian/patches/0103-Correctly-handle-some-short-save-files.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0103-Correctly-handle-some-short-save-files.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0103-Correctly-handle-some-short-save-files.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,42 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sun, 26 Feb 2023 21:48:10 +0000
+Subject: [PATCH 103/159] Correctly handle some short save files
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=6ee62a43abe7d7e77226415b21d1cbf16dbda85a
+Bug-Debian: https://bugs.debian.org/1034190
+
+A save file that ended in the middle of a value before the "SAVEFILE"
+field had been loaded would cause a read from uninitialised memory.
+While technically undefined behaviour this was practically pretty
+harmless.  Fixed by handling unexpected EOF here the same an
+unexpected EOF anywhere else.
+
+This bug could be demonstrated by loading a truncated save file like
+this in a build with MemorySanitizer enabled:
+
+SAVEFILE:41:Simo
+---
+ midend.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/midend.c b/midend.c
+index 23652583..90bcde27 100644
+--- a/midend.c
++++ b/midend.c
+@@ -2340,7 +2340,7 @@ static const char *midend_deserialise_internal(
+ 
+         val = snewn(len+1, char);
+         if (!read(rctx, val, len)) {
+-            if (started)
++            /* unexpected EOF */
+             goto cleanup;
+         }
+         val[len] = '\0';
+@@ -2747,7 +2747,7 @@ const char *identify_game(char **name,
+ 
+         val = snewn(len+1, char);
+         if (!read(rctx, val, len)) {
+-            if (started)
++            /* unexpected EOF */
+             goto cleanup;
+         }
+         val[len] = '\0';
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0104-Inertia-insist-that-solutions-must-be-non-empty.patch sgt-puzzles-20230122.806ae71/debian/patches/0104-Inertia-insist-that-solutions-must-be-non-empty.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0104-Inertia-insist-that-solutions-must-be-non-empty.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0104-Inertia-insist-that-solutions-must-be-non-empty.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,31 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sun, 26 Feb 2023 23:18:44 +0000
+Subject: [PATCH 104/159] Inertia: insist that solutions must be non-empty
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=5a491c5ad333ef34c1e7713f920f51cbb205af60
+Bug-Debian: https://bugs.debian.org/1034190
+
+Any solution actually generated by the solver will contain at least one
+move, because it refuses to solve games that are already solved.
+However, a save file might contain an empty "solve" move.  This causes
+an uninitialised read when execute_move() then tries to check if the
+next move is in accordance with the solution, because the check for
+running off the end of the solution happens after that.
+
+We now avoid this by treating a zero-length "solution" as an invalid
+move.
+---
+ inertia.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/inertia.c b/inertia.c
+index ed50c7a8..1a958af7 100644
+--- a/inertia.c
++++ b/inertia.c
+@@ -1697,6 +1697,7 @@ static game_state *execute_move(const game_state *state, const char *move)
+ 	 * This is a solve move, so we don't actually _change_ the
+ 	 * grid but merely set up a stored solution path.
+ 	 */
++        if (move[1] == '\0') return NULL; /* Solution must be non-empty. */
+ 	ret = dup_game(state);
+ 	install_new_solution(ret, move);
+ 	return ret;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0115-Galaxies-fix-recursion-depth-limit-in-solver.patch sgt-puzzles-20230122.806ae71/debian/patches/0115-Galaxies-fix-recursion-depth-limit-in-solver.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0115-Galaxies-fix-recursion-depth-limit-in-solver.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0115-Galaxies-fix-recursion-depth-limit-in-solver.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,91 @@
+From: Simon Tatham <anakin@pobox.com>
+Date: Sun, 12 Mar 2023 14:11:34 +0000
+Subject: [PATCH 115/159] Galaxies: fix recursion depth limit in solver.
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=f018ef97d34b9126744e63cc2112c302fcae4ab4
+Bug-Debian: https://bugs.debian.org/1034190
+
+The static variable 'solver_recurse_depth' is _mostly_ used by the
+standalone solver, to appropriately indent the solver diagnostics for
+the current recursion level. So most uses of it are guarded by an
+'#ifdef STANDALONE_SOLVER' statement, or some equivalent (such as
+being inside the solvep() macro).
+
+One exception is the check that limits the recursion depth to 5, to
+avoid getting hung up forever on a too-hard game. Unfortunately, this
+check depends on the variable actually incrementing when we recurse
+another level - and it wasn't, because the increment itself was under
+ifdef! So the generator in live Galaxies could recurse arbitrarily
+deep, and generate puzzles that the standalone solver found too hard
+_even_ at Unreasonable mode.
+
+Removed the ifdefs, so that solver_recurse_depth is now incremented
+and decremented. Also, make sure to initialise the depth to 0 at the
+start of a solver run, just in case it had a bogus value left over
+from a previous run.
+---
+ galaxies.c | 15 +++++++++------
+ 1 file changed, 9 insertions(+), 6 deletions(-)
+
+diff --git a/galaxies.c b/galaxies.c
+index 0db0d544..fc20b346 100644
+--- a/galaxies.c
++++ b/galaxies.c
+@@ -187,6 +187,7 @@ struct game_state {
+ };
+ 
+ static bool check_complete(const game_state *state, int *dsf, int *colours);
++static int solver_state_inner(game_state *state, int maxdiff);
+ static int solver_state(game_state *state, int maxdiff);
+ static int solver_obvious(game_state *state);
+ static int solver_obvious_dot(game_state *state, space *dot);
+@@ -2415,9 +2416,7 @@ static int solver_recurse(game_state *state, int maxdiff)
+            solver_recurse_depth*4, "",
+            rctx.best->x, rctx.best->y, rctx.bestn));
+ 
+-#ifdef STANDALONE_SOLVER
+     solver_recurse_depth++;
+-#endif
+ 
+     ingrid = snewn(gsz, space);
+     memcpy(ingrid, state->grid, gsz * sizeof(space));
+@@ -2432,7 +2431,7 @@ static int solver_recurse(game_state *state, int maxdiff)
+                          state->dots[n]->x, state->dots[n]->y,
+                          "Attempting for recursion");
+ 
+-        ret = solver_state(state, maxdiff);
++        ret = solver_state_inner(state, maxdiff);
+ 
+         if (diff == DIFF_IMPOSSIBLE && ret != DIFF_IMPOSSIBLE) {
+             /* we found our first solved grid; copy it away. */
+@@ -2464,9 +2463,7 @@ static int solver_recurse(game_state *state, int maxdiff)
+             break;
+     }
+ 
+-#ifdef STANDALONE_SOLVER
+     solver_recurse_depth--;
+-#endif
+ 
+     if (outgrid) {
+         /* we found (at least one) soln; copy it back to state */
+@@ -2477,7 +2474,7 @@ static int solver_recurse(game_state *state, int maxdiff)
+     return diff;
+ }
+ 
+-static int solver_state(game_state *state, int maxdiff)
++static int solver_state_inner(game_state *state, int maxdiff)
+ {
+     solver_ctx *sctx = new_solver(state);
+     int ret, diff = DIFF_NORMAL;
+@@ -2544,6 +2541,12 @@ static int solver_state(game_state *state, int maxdiff)
+     return diff;
+ }
+ 
++static int solver_state(game_state *state, int maxdiff)
++{
++    solver_recurse_depth = 0;
++    return solver_state_inner(state, maxdiff);
++}
++
+ #ifndef EDITOR
+ static char *solve_game(const game_state *state, const game_state *currstate,
+                         const char *aux, const char **error)
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0138-Correct-a-range-check-in-Magnets-layout-verification.patch sgt-puzzles-20230122.806ae71/debian/patches/0138-Correct-a-range-check-in-Magnets-layout-verification.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0138-Correct-a-range-check-in-Magnets-layout-verification.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0138-Correct-a-range-check-in-Magnets-layout-verification.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,26 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Fri, 31 Mar 2023 20:38:31 +0100
+Subject: [PATCH 138/159] Correct a range check in Magnets' layout verification
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=91735e5019be84d2fa693c5d40746c818ace28f8
+Bug-Debian: https://bugs.debian.org/1034190
+
+Squares in the grid are numbered from 0, so the upper limit check
+needs to use "<=" rather than "<".  Without this, invalid descriptions
+can cause a read overrun off the end of the board.
+---
+ magnets.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/magnets.c b/magnets.c
+index d190848a..d95d1464 100644
+--- a/magnets.c
++++ b/magnets.c
+@@ -520,7 +520,7 @@ static game_state *new_game_int(const game_params *params, const char *desc,
+      * (i.e. each end points to the other) */
+     for (idx = 0; idx < state->wh; idx++) {
+         if (state->common->dominoes[idx] < 0 ||
+-            state->common->dominoes[idx] > state->wh ||
++            state->common->dominoes[idx] >= state->wh ||
+             state->common->dominoes[state->common->dominoes[idx]] != idx) {
+             *prob = "Domino descriptions inconsistent";
+             goto done;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0139-Magnets-add-a-check-that-magnets-don-t-wrap-between-.patch sgt-puzzles-20230122.806ae71/debian/patches/0139-Magnets-add-a-check-that-magnets-don-t-wrap-between-.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0139-Magnets-add-a-check-that-magnets-don-t-wrap-between-.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0139-Magnets-add-a-check-that-magnets-don-t-wrap-between-.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,33 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Sat, 1 Apr 2023 18:54:29 +0100
+Subject: [PATCH 139/159] Magnets: add a check that magnets don't wrap between
+ lines
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=0bd1a8057841386754f9f4a8a268616c7ce80e80
+Bug-Debian: https://bugs.debian.org/1034190
+
+There was nothing in Magnet's description validation to prevent there
+being the left end of a magnet at the right end of a row and the right
+end of a magnet at the left end of the row below.  Indeed as far as I
+can such a game (e.g. 3x3:..2,2..,...,1.1,TLRB*LRLR) plays entirely
+correctly except that one magnet is discontinuous.
+
+While this worked, it was entirely an artefact of the particular memory
+layout that Magnets uses and shouldn't have been allowed, so I've added
+an additional validation rule to stop it.
+---
+ magnets.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/magnets.c b/magnets.c
+index d95d1464..06699784 100644
+--- a/magnets.c
++++ b/magnets.c
+@@ -521,6 +521,8 @@ static game_state *new_game_int(const game_params *params, const char *desc,
+     for (idx = 0; idx < state->wh; idx++) {
+         if (state->common->dominoes[idx] < 0 ||
+             state->common->dominoes[idx] >= state->wh ||
++            (state->common->dominoes[idx] % state->w != idx % state->w &&
++             state->common->dominoes[idx] / state->w != idx / state->w) ||
+             state->common->dominoes[state->common->dominoes[idx]] != idx) {
+             *prob = "Domino descriptions inconsistent";
+             goto done;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0155-Net-assert-that-cx-and-cy-are-in-range-in-compute_ac.patch sgt-puzzles-20230122.806ae71/debian/patches/0155-Net-assert-that-cx-and-cy-are-in-range-in-compute_ac.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0155-Net-assert-that-cx-and-cy-are-in-range-in-compute_ac.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0155-Net-assert-that-cx-and-cy-are-in-range-in-compute_ac.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,40 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Mon, 13 Feb 2023 22:14:26 +0000
+Subject: [PATCH 155/159] Net: assert that cx and cy are in range in
+ compute_active()
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=e411db788cfc0d0ed54b3c9b9deb15edba7d237a
+Bug-Debian: https://bugs.debian.org/1034190
+
+This avoids an out-of-range heap write shortly afterwards.  An assertion
+failure is better than a buffer overrun, but still not ideal.  Fixing
+the problem properly will require fairly wide-ranging changes, though.
+
+The bug can be demonstrated by loading this save file into a build with
+AddressSanitizer:
+
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :3:Net
+PARAMS  :4:5x5w
+CPARAMS :4:5x5w
+DESC    :25:9893e85285bb72e6de5182741
+UI      :9:O0,0;C6,6
+NSTATES :1:1
+STATEPOS:1:1
+---
+ net.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/net.c b/net.c
+index 6f7d1498..7f900472 100644
+--- a/net.c
++++ b/net.c
+@@ -1872,6 +1872,8 @@ static unsigned char *compute_active(const game_state *state, int cx, int cy)
+     active = snewn(state->width * state->height, unsigned char);
+     memset(active, 0, state->width * state->height);
+ 
++    assert(0 <= cx && cx < state->width);
++    assert(0 <= cy && cy < state->height);
+     /*
+      * We only store (x,y) pairs in todo, but it's easier to reuse
+      * xyd_cmp and just store direction 0 every time.
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/0159-Don-t-allow-zero-clues-in-Pattern.patch sgt-puzzles-20230122.806ae71/debian/patches/0159-Don-t-allow-zero-clues-in-Pattern.patch
--- sgt-puzzles-20230122.806ae71/debian/patches/0159-Don-t-allow-zero-clues-in-Pattern.patch	1970-01-01 01:00:00.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/0159-Don-t-allow-zero-clues-in-Pattern.patch	2023-04-16 21:15:13.000000000 +0200
@@ -0,0 +1,36 @@
+From: Ben Harris <bjh21@bjh21.me.uk>
+Date: Tue, 14 Feb 2023 13:16:53 +0000
+Subject: [PATCH 159/159] Don't allow zero clues in Pattern
+Origin: https://git.tartarus.org/?p=simon/puzzles.git;a=commitdiff;h=71cf891fdc3ab237ecf0e5d1aae39b6c9fe97a4d
+Bug-Debian: https://bugs.debian.org/1034190
+
+Some nonogram implementations allow zero clues so that a row or column
+with a single zero clue is equivalent to one with no clues, that is it
+has no black squares in it.  Pattern, however, doesn't interpret them
+like this and treats a puzzle with a zero clue as insoluble, so it's
+not helpful to permit them.
+
+Permitting zero clues also confuses Pattern's memory allocation so
+that it can suffer a buffer overrun.  As an example, before this
+commit a build with AddressSanitizer would report a buffer overrun
+with the description "1:0/0.0" because it tries to put two clues in a
+row that can have a maximum of one.
+---
+ pattern.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/pattern.c b/pattern.c
+index 4fd67530..46025856 100644
+--- a/pattern.c
++++ b/pattern.c
+@@ -917,8 +917,8 @@ static const char *validate_desc(const game_params *params, const char *desc)
+                 p = desc;
+                 while (*desc && isdigit((unsigned char)*desc)) desc++;
+                 n = atoi(p);
+-                if (n < 0)
+-                    return "at least one clue is negative";
++                if (n <= 0)
++                    return "all clues must be positive";
+                 if (n > INT_MAX - 1)
+                     return "at least one clue is grossly excessive";
+                 rowspace -= n+1;
diff -Nru sgt-puzzles-20230122.806ae71/debian/patches/series sgt-puzzles-20230122.806ae71/debian/patches/series
--- sgt-puzzles-20230122.806ae71/debian/patches/series	2023-01-22 23:21:38.000000000 +0100
+++ sgt-puzzles-20230122.806ae71/debian/patches/series	2023-04-16 21:19:11.000000000 +0200
@@ -6,3 +6,63 @@
 207_slant-shade-filled.diff
 303_show-debian-version-number.diff
 install-two-icon-sizes.patch
+0001-Black-Box-reject-negative-ball-counts-in-game_params.patch
+0002-Add-validate_params-bounds-checks-in-a-few-more-game.patch
+0006-Don-t-allow-Bridges-games-with-2-islands.patch
+0007-Forbid-moves-that-fill-with-the-current-colour-in-Fl.patch
+0008-Cleanly-reject-ill-formed-solve-moves-in-Flood.patch
+0009-Don-t-segfault-on-premature-solve-moves-in-Mines.patch
+0010-Limit-number-of-mines-in-Mines-game-description.patch
+0011-Validate-the-number-of-pegs-and-holes-in-a-Pegs-game.patch
+0017-Mines-forbid-moves-that-flag-or-unflag-an-exposed-sq.patch
+0018-Mines-Don-t-check-if-the-player-has-won-if-they-ve-a.patch
+0019-Avoid-invalid-moves-when-solving-Tracks.patch
+0020-Fix-move-validation-in-Netslide.patch
+0021-Tighten-validation-of-Tents-game-descriptions.patch
+0022-Dominosa-require-the-two-halves-of-a-domino-to-be-ad.patch
+0023-Forbid-lines-off-the-grid-in-Pearl.patch
+0024-Tolerate-incorrect-solutions-in-Inertia.patch
+0025-Palisade-replace-dfs_dsf-with-a-simple-iteration.patch
+0026-latin_solver_alloc-handle-clashing-numbers-in-input-.patch
+0027-Pearl-fix-assertion-failure-on-bad-puzzle.patch
+0028-Pearl-fix-bounds-check-in-previous-commit.patch
+0029-Unequal-Don-t-insist-that-solve-moves-must-actually-.patch
+0030-Range-Don-t-fail-an-assertion-on-an-all-black-board.patch
+0031-Limit-width-and-height-to-SHRT_MAX-in-Mines.patch
+0032-Mines-Add-assertions-to-range-check-conversions-to-s.patch
+0033-Unequal-fix-sense-error-in-latin_solver_alloc-fix.patch
+0034-Forbid-impossible-moves-in-Bridges.patch
+0035-Forbid-game-descriptions-with-joined-islands-in-Brid.patch
+0037-Check-state-is-valid-at-the-end-of-a-move-in-Pearl.patch
+0038-Cleanly-reject-more-ill-formed-solve-moves-in-Flood.patch
+0039-Don-t-allow-moves-that-change-the-constraints-in-Une.patch
+0041-Fix-memory-leaks-in-Keen-s-validate_desc.patch
+0042-Remember-to-free-the-actual_board-array-in-Mosaic.patch
+0043-Don-t-leak-grids-in-Loopy-s-validate_desc.patch
+0044-Remember-to-free-the-to_draw-member-from-Net-s-draws.patch
+0045-Undead-check-the-return-value-of-sscanf-in-execute_m.patch
+0046-Don-t-leak-duplicate-edges-in-Untangle.patch
+0047-Remember-to-free-the-numcolours-array-from-Pattern-s.patch
+0048-Free-new-game_state-properly-in-Mosaic-s-execute_mov.patch
+0049-Twiddle-don-t-read-off-the-end-of-parameter-strings-.patch
+0050-Loopy-free-the-grid-description-string-if-it-s-inval.patch
+0051-Mosaic-don-t-duplicate-the-description-being-validat.patch
+0052-Avoid-division-by-zero-in-Cube-grid-size-checks.patch
+0055-Validate-that-save-file-values-are-ASCII-mostly.patch
+0056-More-validation-of-solve-moves-in-Flood.patch
+0058-Make-sure-that-moves-in-Flood-use-only-valid-colours.patch
+0059-Tighten-grid-size-limit-in-Mines.patch
+0061-Solo-cope-with-pencil-marks-when-tilesize-1.patch
+0065-Tracks-set-drag_s-x-y-even-if-starting-off-grid.patch
+0080-Undead-be-a-bit-more-careful-about-sprintf-buffer-si.patch
+0090-Fix-memory-leak-in-midend_game_id_int.patch
+0092-Flood-don-t-read-off-the-end-of-some-parameter-strin.patch
+0101-Be-more-careful-with-type-of-left-operand-of.patch
+0102-Map-reduce-maximum-size.patch
+0103-Correctly-handle-some-short-save-files.patch
+0104-Inertia-insist-that-solutions-must-be-non-empty.patch
+0115-Galaxies-fix-recursion-depth-limit-in-solver.patch
+0138-Correct-a-range-check-in-Magnets-layout-verification.patch
+0139-Magnets-add-a-check-that-magnets-don-t-wrap-between-.patch
+0155-Net-assert-that-cx-and-cy-are-in-range-in-compute_ac.patch
+0159-Don-t-allow-zero-clues-in-Pattern.patch

Reply to: