Skip to content

Commit 37a21d2

Browse files
committed
Add worn-slot sanity check to catch stale equipment-slot pointers.
A were-creature's lycanthropy bite can panic the game with "extract_nobj: object lost": set_ulycn() calls retouch_equipment() to force-drop gear the new were-form can no longer touch, and the forced drop reaches freeinv() with a wielded object that is no longer in invent. The wielded slot (uwep) was left pointing at an object that had already departed the invent chain on an earlier turn, so the drop fails its inventory lookup and panics. The per-turn obj_sanity_check() never caught this because sanity_check_worn() only validates the invent->slot direction: for each worn object found in invent, it confirms the matching ufoo points back to it. A slot pointing the other way, at an object that has left invent, is never visited and rides undetected until the downstream freeinv(). Add worn_slot_sanity(), the missing slot->invent check, to report the stale slot at (or one turn after) the producing action with turn context, instead of crashing later. * mkobj.c: - new worn_slot_sanity() walks the 14 invent-resident worn-slot globals (uwep..ublindf; ball/chain and the steed saddle are excluded) and reports any slot whose object is absent from the invent chain. It compares pointers only, never dereferencing the possibly-dangling suspect, and dedups per slot so a persistent stale slot reports once rather than every turn (mirrors dmonsfree's orphan-warning pattern). Called from obj_sanity_check() alongside the existing list checks. * artifact.c: - call worn_slot_sanity() at the top of retouch_equipment(), under the same sanity/fuzzer gate, so a slot corrupted earlier in the same turn is reported immediately before the forced drop turns it into a panic. * extern.h: - declare worn_slot_sanity().
1 parent 94b0d12 commit 37a21d2

4 files changed

Lines changed: 61 additions & 0 deletions

File tree

‎doc/evilhack-changelog.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5502,4 +5502,5 @@ The following changes to date are:
55025502
- Fix: off-hand artifact extrinsics lost after a returning thrown weapon
55035503
- Grant skeletal dragons immunity to their own breath element
55045504
- Fix: covetous monster freezes when hero stands at a map edge
5505+
- Add worn-slot sanity check to catch stale equipment-slot pointers
55055506

‎include/extern.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,7 @@ E long FDECL(peek_at_iced_corpse_age, (struct obj *));
15461546
E long FDECL(blood_freshness, (struct obj *));
15471547
E int FDECL(hornoplenty, (struct obj *, BOOLEAN_P, struct obj *));
15481548
E void NDECL(obj_sanity_check);
1549+
E void FDECL(worn_slot_sanity, (const char *));
15491550
E struct obj *FDECL(obj_nexto, (struct obj *));
15501551
E struct obj *FDECL(obj_nexto_xy, (struct obj *, int, int, BOOLEAN_P));
15511552
E struct obj *FDECL(obj_absorb, (struct obj **, struct obj **));

‎src/artifact.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4735,6 +4735,11 @@ int dropflag; /* 0==don't drop, 1==drop all, 2==drop weapon */
47354735
if (!nesting++)
47364736
clear_bypasses(); /* init upon initial entry */
47374737

4738+
/* catch a worn slot already pointing outside invent on entry, before
4739+
a forced drop turns it into a freeinv() "object lost" panic */
4740+
if (iflags.sanity_check || iflags.debug_fuzzer)
4741+
worn_slot_sanity("retouch_equipment entry");
4742+
47384743
dropit = (dropflag > 0); /* drop all or drop weapon */
47394744
/* check secondary weapon first, before possibly unwielding primary */
47404745
if (u.twoweap) {

‎src/mkobj.c‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2885,6 +2885,7 @@ obj_sanity_check()
28852885
}
28862886

28872887
objlist_sanity(invent, OBJ_INVENT, "invent sanity");
2888+
worn_slot_sanity("worn-slot sanity");
28882889
objlist_sanity(migrating_objs, OBJ_MIGRATING, "migrating sanity");
28892890
objlist_sanity(mchest, OBJ_MCHEST, "magic chest sanity");
28902891
objlist_sanity(level.buriedobjlist, OBJ_BURIED, "buried sanity");
@@ -2914,6 +2915,59 @@ obj_sanity_check()
29142915
(struct monst *) 0);
29152916
}
29162917

2918+
/* verify every set worn-equipment slot still points into invent.
2919+
sanity_check_worn() only checks the invent->slot direction (for each
2920+
worn object in invent, the matching ufoo points back to it); a slot
2921+
left pointing at an object that has departed invent is invisible to
2922+
that check and otherwise surfaces only later, as a freeinv() ->
2923+
extract_nobj("object lost") panic. ball/chain are excluded (a chained
2924+
ball can legitimately rest on the floor) and the steed saddle is not a
2925+
hero slot. the suspect pointer is never dereferenced -- it may be
2926+
dangling -- so detection is pure pointer comparison and the report
2927+
prints only its address, the slot name, and the turn */
2928+
void
2929+
worn_slot_sanity(mesg)
2930+
const char *mesg;
2931+
{
2932+
static const struct {
2933+
struct obj **slotp;
2934+
const char *nm;
2935+
} slots[] = {
2936+
{ &uwep, "uwep" }, { &uswapwep, "uswapwep" },
2937+
{ &uquiver, "uquiver" },
2938+
{ &uarm, "uarm" }, { &uarmc, "uarmc" }, { &uarmh, "uarmh" },
2939+
{ &uarms, "uarms" }, { &uarmg, "uarmg" }, { &uarmf, "uarmf" },
2940+
{ &uarmu, "uarmu" }, { &uleft, "uleft" }, { &uright, "uright" },
2941+
{ &uamul, "uamul" }, { &ublindf, "ublindf" }
2942+
};
2943+
/* per-slot dedup so a persistent stale slot reports once, not every
2944+
turn (mirrors dmonsfree's orphan_warned_id) */
2945+
static struct obj *warned[SIZE(slots)];
2946+
struct obj *o, *otmp;
2947+
int i;
2948+
2949+
for (i = 0; i < (int) SIZE(slots); i++) {
2950+
o = *slots[i].slotp;
2951+
if (!o) {
2952+
warned[i] = 0;
2953+
continue;
2954+
}
2955+
for (otmp = invent; otmp; otmp = otmp->nobj)
2956+
if (otmp == o)
2957+
break;
2958+
if (otmp) {
2959+
warned[i] = 0; /* slot is healthy */
2960+
continue;
2961+
}
2962+
/* slot is set but its object is no longer in invent */
2963+
if (o != warned[i]) {
2964+
warned[i] = o;
2965+
impossible("%s: worn slot %s -> %s not in invent (turn %ld)",
2966+
mesg, slots[i].nm, fmt_ptr((genericptr_t) o), moves);
2967+
}
2968+
}
2969+
}
2970+
29172971
/* sanity check for objects on specified list (fobj, &c) */
29182972
STATIC_OVL void
29192973
objlist_sanity(objlist, wheretype, mesg)

0 commit comments

Comments
 (0)