Debugging NetHack with rr

rr is a gdb extender that records a program, and then allows for replay and rewinding. I've started NetHack under rr and turned on the fuzzer. After a while, it crashed, with error message "monster (chieftain) taking or picking up attached ball (leather armor)?"

I then replayed the recording, and tried to figure out what happened to cause the crash.

The debugging session is below, with comments.


I've just started 'rr replay', now I 'continue' to the end of it, to see the crash ...

(rr) continue
Continuing.

Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50	../sysdeps/unix/sysv/linux/raise.c: No such file or directory.

Here it crashed, with the error message
monster (chieftain) taking or picking up attached ball (leather armor)?
Let's see the backtrace

(rr) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007f160d24d535 in __GI_abort () at abort.c:79
#2  0x00005611f787c3d5 in NH_abort () at end.c:189
#3  0x00005611f787e66e in panic (str=str@entry=0x5611f7a576a6 "%s") at end.c:624
#4  0x00005611f7931c5a in impossible (s=s@entry=0x5611f7a58560 "monster (%s) taking or picking up attached %s (%s)?") at pline.c:500
#5  0x00005611f79805d8 in mpickobj (mtmp=mtmp@entry=0x5611f7d14750, otmp=otmp@entry=0x5611f7d22a00) at steal.c:494
#6  0x00005611f78b2732 in mongets (mtmp=mtmp@entry=0x5611f7d14750, otyp=<optimized out>) at makemon.c:1968
#7  0x00005611f78b4c2f in m_initweap (mtmp=0x5611f7d14750) at makemon.c:291
#8  makemon (ptr=ptr@entry=0x5611f7adff48 <mons+26568>, x=<optimized out>, y=<optimized out>, mmflags=mmflags@entry=0) at makemon.c:1387
#9  0x00005611f7975565 in create_monster (croom=0x0, m=<synthetic pointer>) at sp_lev.c:1838
#10 lspo_monster (L=0x5611f7d844f8) at sp_lev.c:3163
#11 0x00005611f79d4727 in luaD_precall.part.3 ()
#12 0x00005611f79df7a5 in luaV_execute ()
#13 0x00005611f79d4988 in luaD_call ()
#14 0x00005611f79d49b1 in luaD_callnoyield ()
#15 0x00005611f79d3dff in luaD_rawrunprotected ()
#16 0x00005611f79d4cbb in luaD_pcall ()
#17 0x00005611f79d22a6 in lua_pcallk ()
#18 0x00005611f79076c6 in nhl_loadlua (L=L@entry=0x5611f7d844f8, fname=<optimized out>, fname@entry=0x7fffefbc5210 "Bar-strt.lua") at nhlua.c:1098
#19 0x00005611f79077fe in load_lua (name=name@entry=0x7fffefbc5210 "Bar-strt.lua") at nhlua.c:1157
#20 0x00005611f797bc9c in load_special (name=name@entry=0x7fffefbc5210 "Bar-strt.lua") at sp_lev.c:6309
#21 0x00005611f78d8f24 in makemaz (s=s@entry=0x5611f7d14f9a "Bar-strt") at mkmaze.c:989
#22 0x00005611f78d448c in makelevel () at mklev.c:685
#23 0x00005611f78d55af in mklev () at mklev.c:1020
#24 0x00005611f782eba5 in wiz_makemap () at cmd.c:886
#25 0x00005611f78324a2 in rhack (cmd=<optimized out>, cmd@entry=0x0) at cmd.c:3296
#26 0x00005611f780b0eb in moveloop (resuming=<optimized out>) at allmain.c:457
#27 0x00005611f79cfa9a in main (argc=<optimized out>, argv=0x7fffefbc55b8) at ../sys/unix/unixmain.c:351

So, #wizmakemap extended command, making the level, loaded "Bar-strt.lua", creating a monster, and giving the monster some item? Weird.
Looking at the code, the error indicated the ball was a leather armor instead.
Seems like a stale pointer problem.
How does the 'uball' look like?

(rr) print uball
$1 = (struct obj *) 0x5611f7d22a00

(rr) print *uball
$2 = {nobj = 0x0, v = {v_nexthere = 0x0, v_ocontainer = 0x0, v_ocarry = 0x0}, cobj = 0x0, o_id = 2921945, ox = 0 '\000', oy = 0 '\000', otyp = 113, owt = 150, quan = 1, spe = 0 '\000', oclass = 3 '\003', 
  invlet = 0 '\000', oartifact = 0 '\000', where = 0 '\000', timed = 0 '\000', cursed = 0, blessed = 0, unpaid = 0, no_charge = 0, known = 0, dknown = 1, bknown = 0, rknown = 0, oeroded = 0, oeroded2 = 0, 
  oerodeproof = 0, olocked = 0, obroken = 0, otrapped = 0, recharged = 0, lamplit = 0, globby = 0, greased = 0, nomerge = 0, was_thrown = 0, in_use = 0, bypass = 0, cknown = 0, lknown = 0, corpsenm = -1, 
  usecount = 0, oeaten = 0, age = 4181589, owornmask = 0, lua_ref_cnt = 0, oextra = 0x0}

'otyp' is 113? Grepping include/onames.h, that's a leather armor, not the expected iron ball!
Let's see where it changes ...

(rr) watch uball
Hardware watchpoint 1: uball

(rr) watch uball->otyp
Hardware watchpoint 2: uball->otyp

Everything above is just normal gdb, but with rr we can rewind the program execution:

(rr) reverse-continue 1
Not stopped at any breakpoint; argument ignored.
Continuing.

Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50	in ../sysdeps/unix/sysv/linux/raise.c

(rr) reverse-continue 1
Not stopped at any breakpoint; argument ignored.
Continuing.

Hardware watchpoint 2: uball->otyp

Old value = 113
New value = 0
mksobj (otyp=otyp@entry=113, init=init@entry=1 '\001', artif=artif@entry=0 '\000') at mkobj.c:794
794	    otmp->otyp = otyp;

Interesting. 'uball->otyp' went from 0 to 113. Neither of those is an iron ball.
Now where are we?

(rr) bt
#0  mksobj (otyp=otyp@entry=113, init=init@entry=1 '\001', artif=artif@entry=0 '\000') at mkobj.c:794
#1  0x00005611f78b2626 in mongets (mtmp=mtmp@entry=0x5611f7d14750, otyp=113) at makemon.c:1931
#2  0x00005611f78b4c2f in m_initweap (mtmp=0x5611f7d14750) at makemon.c:291
#3  makemon (ptr=ptr@entry=0x5611f7adff48 <mons+26568>, x=<optimized out>, y=<optimized out>, mmflags=mmflags@entry=0) at makemon.c:1387
#4  0x00005611f7975565 in create_monster (croom=0x0, m=<synthetic pointer>) at sp_lev.c:1838
#5  lspo_monster (L=0x5611f7d844f8) at sp_lev.c:3163
#6  0x00005611f79d4727 in luaD_precall.part.3 ()
#7  0x00005611f79df7a5 in luaV_execute ()
#8  0x00005611f79d4988 in luaD_call ()
#9  0x00005611f79d49b1 in luaD_callnoyield ()
#10 0x00005611f79d3dff in luaD_rawrunprotected ()
#11 0x00005611f79d4cbb in luaD_pcall ()
#12 0x00005611f79d22a6 in lua_pcallk ()
#13 0x00005611f79076c6 in nhl_loadlua (L=L@entry=0x5611f7d844f8, fname=<optimized out>, fname@entry=0x7fffefbc5210 "Bar-strt.lua") at nhlua.c:1098
#14 0x00005611f79077fe in load_lua (name=name@entry=0x7fffefbc5210 "Bar-strt.lua") at nhlua.c:1157
#15 0x00005611f797bc9c in load_special (name=name@entry=0x7fffefbc5210 "Bar-strt.lua") at sp_lev.c:6309
#16 0x00005611f78d8f24 in makemaz (s=s@entry=0x5611f7d14f9a "Bar-strt") at mkmaze.c:989
#17 0x00005611f78d448c in makelevel () at mklev.c:685
#18 0x00005611f78d55af in mklev () at mklev.c:1020
#19 0x00005611f782eba5 in wiz_makemap () at cmd.c:886
#20 0x00005611f78324a2 in rhack (cmd=<optimized out>, cmd@entry=0x0) at cmd.c:3296
#21 0x00005611f780b0eb in moveloop (resuming=<optimized out>) at allmain.c:457
#22 0x00005611f79cfa9a in main (argc=<optimized out>, argv=0x7fffefbc55b8) at ../sys/unix/unixmain.c:351

Still in the monster creation.

(rr) print *uball
$4 = {nobj = 0x0, v = {v_nexthere = 0x0, v_ocontainer = 0x0, v_ocarry = 0x0}, cobj = 0x0, o_id = 2921945, ox = 0 '\000', oy = 0 '\000', otyp = 0, owt = 0, quan = 1, spe = 0 '\000', oclass = 3 '\003', 
  invlet = 0 '\000', oartifact = 0 '\000', where = 0 '\000', timed = 0 '\000', cursed = 0, blessed = 0, unpaid = 0, no_charge = 0, known = 0, dknown = 0, bknown = 0, rknown = 0, oeroded = 0, oeroded2 = 0, 
  oerodeproof = 0, olocked = 0, obroken = 0, otrapped = 0, recharged = 0, lamplit = 0, globby = 0, greased = 0, nomerge = 0, was_thrown = 0, in_use = 0, bypass = 0, cknown = 0, lknown = 0, corpsenm = 0, 
  usecount = 0, oeaten = 0, age = 4181589, owornmask = 0, lua_ref_cnt = 0, oextra = 0x0}

Yup, that still looks wrong.
Let's continue backwards.

(rr) reverse-continue
Continuing.

Hardware watchpoint 2: uball->otyp

Old value = 0
New value = 450
0x00005611f78de367 in mksobj (otyp=otyp@entry=113, init=init@entry=1 '\001', artif=artif@entry=0 '\000') at mkobj.c:787
787	    *otmp = cg.zeroobj;

Okay, 450 is an iron ball, so here it still was good.

(rr) bt
#0  0x00005611f78de367 in mksobj (otyp=otyp@entry=113, init=init@entry=1 '\001', artif=artif@entry=0 '\000') at mkobj.c:787
#1  0x00005611f78b2626 in mongets (mtmp=mtmp@entry=0x5611f7d14750, otyp=113) at makemon.c:1931
#2  0x00005611f78b4c2f in m_initweap (mtmp=0x5611f7d14750) at makemon.c:291
#3  makemon (ptr=ptr@entry=0x5611f7adff48 <mons+26568>, x=<optimized out>, y=<optimized out>, mmflags=mmflags@entry=0) at makemon.c:1387
#4  0x00005611f7975565 in create_monster (croom=0x0, m=<synthetic pointer>) at sp_lev.c:1838
#5  lspo_monster (L=0x5611f7d844f8) at sp_lev.c:3163
#6  0x00005611f79d4727 in luaD_precall.part.3 ()
#7  0x00005611f79df7a5 in luaV_execute ()
#8  0x00005611f79d4988 in luaD_call ()
#9  0x00005611f79d49b1 in luaD_callnoyield ()
#10 0x00005611f79d3dff in luaD_rawrunprotected ()
#11 0x00005611f79d4cbb in luaD_pcall ()
#12 0x00005611f79d22a6 in lua_pcallk ()
#13 0x00005611f79076c6 in nhl_loadlua (L=L@entry=0x5611f7d844f8, fname=<optimized out>, fname@entry=0x7fffefbc5210 "Bar-strt.lua") at nhlua.c:1098
#14 0x00005611f79077fe in load_lua (name=name@entry=0x7fffefbc5210 "Bar-strt.lua") at nhlua.c:1157
#15 0x00005611f797bc9c in load_special (name=name@entry=0x7fffefbc5210 "Bar-strt.lua") at sp_lev.c:6309
#16 0x00005611f78d8f24 in makemaz (s=s@entry=0x5611f7d14f9a "Bar-strt") at mkmaze.c:989
#17 0x00005611f78d448c in makelevel () at mklev.c:685
#18 0x00005611f78d55af in mklev () at mklev.c:1020
#19 0x00005611f782eba5 in wiz_makemap () at cmd.c:886
#20 0x00005611f78324a2 in rhack (cmd=<optimized out>, cmd@entry=0x0) at cmd.c:3296
#21 0x00005611f780b0eb in moveloop (resuming=<optimized out>) at allmain.c:457
#22 0x00005611f79cfa9a in main (argc=<optimized out>, argv=0x7fffefbc55b8) at ../sys/unix/unixmain.c:351

... but we're still in the monster creation?
Back up some more.

(rr) reverse-continue
Continuing.

Hardware watchpoint 1: uball

Old value = (struct obj *) 0x5611f7d22a00
New value = (struct obj *) 0x0

Hardware watchpoint 2: uball->otyp

Old value = 450
New value = <unreadable>
0x00005611f79bd318 in setworn (obj=0x5611f7d22a00, mask=mask@entry=2097152) at worn.c:86
86	                *(wp->w_obj) = obj;

Hm, where are we now?

(rr) bt
#0  0x00005611f79bd318 in setworn (obj=0x5611f7d22a00, mask=mask@entry=2097152) at worn.c:86
#1  0x00005611f794bdf5 in punish (sobj=sobj@entry=0x0) at read.c:2183
#2  0x00005611f780a246 in moveloop (resuming=<optimized out>) at allmain.c:99
#3  0x00005611f79cfa9a in main (argc=<optimized out>, argv=0x7fffefbc55b8) at ../sys/unix/unixmain.c:351

Ah. In moveloop. This place is where I added couple lines of code to automatically punish the hero every turn.
Otherwise the fuzzer wouldn't stay punished for long.
What about 'uball'?

(rr) print uball
$7 = (struct obj *) 0x0

Okay, so this is where the ball came from.

(rr) print obj
$8 = (struct obj *) 0x5611f7d22a00

(rr) print *obj
$9 = {nobj = 0x0, v = {v_nexthere = 0x0, v_ocontainer = 0x0, v_ocarry = 0x0}, cobj = 0x0, o_id = 2911482, ox = 0 '\000', oy = 0 '\000', otyp = 450, owt = 480, quan = 1, spe = 0 '\000', oclass = 15 '\017', 
  invlet = 0 '\000', oartifact = 0 '\000', where = 0 '\000', timed = 0 '\000', cursed = 0, blessed = 0, unpaid = 0, no_charge = 0, known = 1, dknown = 1, bknown = 0, rknown = 0, oeroded = 0, oeroded2 = 0, 
  oerodeproof = 0, olocked = 0, obroken = 0, otrapped = 0, recharged = 0, lamplit = 0, globby = 0, greased = 0, nomerge = 0, was_thrown = 0, in_use = 0, bypass = 0, cknown = 0, lknown = 0, corpsenm = -1, 
  usecount = 0, oeaten = 0, age = 4172668, owornmask = 0, lua_ref_cnt = 0, oextra = 0x0}

And it looks to be correct.
Here I was just stepping forwards, waiting for my brain to catch up.

(rr) next

Hardware watchpoint 1: uball

Old value = (struct obj *) 0x0
New value = (struct obj *) 0x5611f7d22a00

Hardware watchpoint 2: uball->otyp

Old value = <unreadable>
New value = 450
setworn (obj=0x5611f7d22a00, mask=mask@entry=2097152) at worn.c:87
87	                if (obj) {

So, 'uball' was set now, and is correct.

(rr) finish
Run till exit from #0  setworn (obj=0x5611f7d22a00, mask=mask@entry=2097152) at worn.c:87
0x00005611f794bdf5 in punish (sobj=sobj@entry=0x0) at read.c:2183
2183	        setworn(mkobj(BALL_CLASS, TRUE), W_BALL);

(rr) print *uball
$10 = {nobj = 0x0, v = {v_nexthere = 0x0, v_ocontainer = 0x0, v_ocarry = 0x0}, cobj = 0x0, o_id = 2911482, ox = 0 '\000', oy = 0 '\000', otyp = 450, owt = 480, quan = 1, spe = 0 '\000', oclass = 15 '\017', 
  invlet = 0 '\000', oartifact = 0 '\000', where = 0 '\000', timed = 0 '\000', cursed = 0, blessed = 0, unpaid = 0, no_charge = 0, known = 1, dknown = 1, bknown = 0, rknown = 0, oeroded = 0, oeroded2 = 0, 
  oerodeproof = 0, olocked = 0, obroken = 0, otrapped = 0, recharged = 0, lamplit = 0, globby = 0, greased = 0, nomerge = 0, was_thrown = 0, in_use = 0, bypass = 0, cknown = 0, lknown = 0, corpsenm = -1, 
  usecount = 0, oeaten = 0, age = 4172668, owornmask = 2097152, lua_ref_cnt = 0, oextra = 0x0}

(rr) printf "%x\n", uball->owornmask
200000

And 'uball' worn mask is W_BALL, as it should be...
Oh. Maybe the iron ball was freed somewhere? Let's set up a breakpoint for it, and continue forwards.

(rr) break dealloc_obj if obj == uball
Breakpoint 3 at 0x5611f78df871: file mkobj.c, line 2170.

(rr) cont
Continuing.

Breakpoint 3, dealloc_obj (obj=obj@entry=0x5611f7d22a00) at mkobj.c:2170
2170	{

Ah-hah!

(rr) bt
#0  dealloc_obj (obj=obj@entry=0x5611f7d22a00) at mkobj.c:2170
#1  0x00005611f7959be3 in saveobjchn (nhfp=nhfp@entry=0x7fffefbc5380, otmp=0x5611f7d22a00) at save.c:811
#2  0x00005611f7959f8b in savelev (nhfp=nhfp@entry=0x7fffefbc5380, lev=<optimized out>) at save.c:572
#3  0x00005611f782e9ec in makemap_prepost (pre=pre@entry=1 '\001', wiztower=wiztower@entry=0 '\000') at cmd.c:844
#4  0x00005611f782eba0 in wiz_makemap () at cmd.c:882
#5  0x00005611f78324a2 in rhack (cmd=<optimized out>, cmd@entry=0x0) at cmd.c:3296
#6  0x00005611f780b0eb in moveloop (resuming=<optimized out>) at allmain.c:457
#7  0x00005611f79cfa9a in main (argc=<optimized out>, argv=0x7fffefbc55b8) at ../sys/unix/unixmain.c:351

So, again we're in #wizmakemap, but now it's in savelev(), discarding the old level ...
... but it certainly shouldn't touch the attached ball!
The ball shouldn't be on the map.

(rr) print uball
$11 = (struct obj *) 0x5611f7d22a00

(rr) print *uball
$12 = {nobj = 0x0, v = {v_nexthere = 0x0, v_ocontainer = 0x0, v_ocarry = 0x0}, cobj = 0x0, o_id = 2911482, ox = 40 '(', oy = 12 '\f', otyp = 450, owt = 480, quan = 1, spe = 1 '\001', oclass = 15 '\017', 
  invlet = 100 'd', oartifact = 0 '\000', where = 0 '\000', timed = 0 '\000', cursed = 0, blessed = 0, unpaid = 0, no_charge = 0, known = 1, dknown = 1, bknown = 1, rknown = 1, oeroded = 2, oeroded2 = 0, 
  oerodeproof = 0, olocked = 0, obroken = 0, otrapped = 0, recharged = 0, lamplit = 0, globby = 0, greased = 0, nomerge = 0, was_thrown = 0, in_use = 0, bypass = 0, cknown = 0, lknown = 0, corpsenm = -1, 
  usecount = 0, oeaten = 0, age = 4172668, owornmask = 2097152, lua_ref_cnt = 0, oextra = 0x0}

The ball is still fine, and is the same object, as o_id is the same.
Anything interesting in the top lines?

(rr) print g.toplines 
$13 = "You don't have anything to use or apply.", '\000' <repeats 259 times>

No. Hmmm.. Where does the ball get removed from the map before this?

(rr) break unplacebc
Breakpoint 4 at 0x5611f781ec0c: file ball.c, line 211.

(rr) reverse-continue 
Continuing.

Breakpoint 4, unplacebc () at ball.c:211
211	{

(rr) bt
#0  unplacebc () at ball.c:211
#1  0x00005611f782e939 in makemap_prepost (pre=pre@entry=1 '\001', wiztower=wiztower@entry=0 '\000') at cmd.c:819
#2  0x00005611f782eba0 in wiz_makemap () at cmd.c:882
#3  0x00005611f78324a2 in rhack (cmd=<optimized out>, cmd@entry=0x0) at cmd.c:3296
#4  0x00005611f780b0eb in moveloop (resuming=<optimized out>) at allmain.c:457
#5  0x00005611f79cfa9a in main (argc=<optimized out>, argv=0x7fffefbc55b8) at ../sys/unix/unixmain.c:351

(rr) next
212	    if (bcrestriction) {

(rr) print bcrestriction 
$14 = 0

(rr) next
216	    unplacebc_core();

(rr) step
unplacebc_core () at ball.c:146
146	{

Looking at the unplacebc_core code, I see it checks for u.uswallow ...

(rr) print u.uswallow 
$15 = 1

Oh, we're swallowed?

(rr) print uball
$16 = (struct obj *) 0x5611f7d22a00

(rr) print *uball
$17 = {nobj = 0x5611f7d2e0b0, v = {v_nexthere = 0x0, v_ocontainer = 0x0, v_ocarry = 0x0}, cobj = 0x0, o_id = 2911482, ox = 40 '(', oy = 12 '\f', otyp = 450, owt = 480, quan = 1, spe = 1 '\001', 
  oclass = 15 '\017', invlet = 100 'd', oartifact = 0 '\000', where = 1 '\001', timed = 0 '\000', cursed = 0, blessed = 0, unpaid = 0, no_charge = 0, known = 1, dknown = 1, bknown = 1, rknown = 1, oeroded = 2, 
  oeroded2 = 0, oerodeproof = 0, olocked = 0, obroken = 0, otrapped = 0, recharged = 0, lamplit = 0, globby = 0, greased = 0, nomerge = 0, was_thrown = 0, in_use = 0, bypass = 0, cknown = 0, lknown = 0, 
  corpsenm = -1, usecount = 0, oeaten = 0, age = 4172668, owornmask = 2097152, lua_ref_cnt = 0, oextra = 0x0}

The ball is just fine ... except it's on the floor! (where = 1)
If we're swallowed, it should NOT be there.

(rr) nh_printmon u.ustuck
$18 = {mname = 0x5611f7a07583 "air elemental", mlet = 31 '\037', mlevel = 8 '\b', mmove = 36 '$', ac = 2 '\002', mr = 30 '\036', maligntyp = 0 '\000', geno = 17, mattk = {{aatyp = 11 '\v', adtyp = 0 '\000', 
      damn = 1 '\001', damd = 10 '\n'}, {aatyp = 0 '\000', adtyp = 0 '\000', damn = 0 '\000', damd = 0 '\000'}, {aatyp = 0 '\000', adtyp = 0 '\000', damn = 0 '\000', damd = 0 '\000'}, {aatyp = 0 '\000', 
      adtyp = 0 '\000', damn = 0 '\000', damd = 0 '\000'}, {aatyp = 0 '\000', adtyp = 0 '\000', damn = 0 '\000', damd = 0 '\000'}, {aatyp = 0 '\000', adtyp = 0 '\000', damn = 0 '\000', damd = 0 '\000'}}, 
  cwt = 0, cnutrit = 0, msound = 0 '\000', msize = 4 '\004', mresists = 160 '\240', mconveys = 0 '\000', mflags1 = 1176577, mflags2 = 67371008, mflags3 = 0, difficulty = 10 '\n', mcolor = 6 '\006'}
$19 = {nmon = 0x5611f7d8e450, data = 0x5611f7adc1f8 <mons+10872>, m_id = 2921924, mnum = 151, cham = -1, movement = 36, m_lev = 9 '\t', malign = 0 '\000', mx = 40 '(', my = 12 '\f', mux = 40 '(', muy = 12 '\f', 
  mtrack = {{x = 42 '*', y = 12 '\f'}, {x = 43 '+', y = 13 '\r'}, {x = 44 ',', y = 14 '\016'}, {x = 45 '-', y = 14 '\016'}}, mhp = 40, mhpmax = 40, mappearance = 0, m_ap_type = 0 '\000', mtame = 0 '\000', 
  mextrinsics = 0, mspec_used = 0, female = 0, minvis = 0, invis_blkd = 0, perminvis = 0, mcan = 0, mburied = 0, mundetected = 0, mcansee = 1, mspeed = 0, permspeed = 0, mrevived = 0, mcloned = 0, mavenge = 0, 
  mflee = 0, mfleetim = 0, msleeping = 0, mblinded = 0, mstun = 0, mfrozen = 0, mcanmove = 1, mconf = 0, mpeaceful = 0, mtrapped = 0, mleashed = 0, isshk = 0, isminion = 0, isgd = 0, ispriest = 0, iswiz = 0, 
  wormno = 0, mtemplit = 0, mstrategy = 0, mtrapseen = 0, mlstmv = 0, mstate = 0, migflags = 0, mspare1 = 0, minvent = 0x0, mw = 0x0, misc_worn_check = 0, weapon_check = 0 '\000', meating = 0, mextra = 0x0}

We're stuck inside an air elemental.
Where does the ball get placed on the map before this?

(rr) watch uball->where
Hardware watchpoint 5: uball->where

(rr) reverse-continue 
Continuing.

Breakpoint 4, unplacebc () at ball.c:211
211	{

Whoops, hit another breakpoint. Rewind more ...

(rr) reverse-continue 
Continuing.

Hardware watchpoint 5: uball->where

Old value = 1 '\001'
New value = 0 '\000'
place_object (otmp=0x5611f7d22a00, x=40, y=12) at mkobj.c:1790
1790	    otmp->where = OBJ_FLOOR;

Ah. Here we are.

(rr) bt
#0  place_object (otmp=0x5611f7d22a00, x=40, y=12) at mkobj.c:1790
#1  0x00005611f781e6e4 in placebc_core () at ball.c:132
#2  0x00005611f781eb96 in placebc () at ball.c:206
#3  0x00005611f799a786 in dotrap (trap=trap@entry=0x5611f7d2e120, trflags=trflags@entry=16) at trap.c:1293
#4  0x00005611f784b6d9 in dodown () at do.c:1018
#5  0x00005611f78324a2 in rhack (cmd=<optimized out>, cmd@entry=0x0) at cmd.c:3296
#6  0x00005611f780b0eb in moveloop (resuming=<optimized out>) at allmain.c:457
#7  0x00005611f79cfa9a in main (argc=<optimized out>, argv=0x7fffefbc55b8) at ../sys/unix/unixmain.c:351

Hero tried to go down? On a trap?

(rr) print u.uswallow 
$20 = 1

While swallowed.

(rr) print *(struct trap *)0x5611f7d2e120
$22 = {ntrap = 0x0, tx = 40 '(', ty = 12 '\f', dst = {dnum = -1 '\377', dlevel = -1 '\377'}, launch = {x = -1 '\377', y = -1 '\377'}, ttyp = 12, tseen = 1, once = 0, madeby_u = 0, vl = {v_launch_otyp = 0, 
    v_launch2 = {x = 0 '\000', y = 0 '\000'}, v_conjoined = 0 '\000', v_tnote = 0}}

Trap ttyp = 12 is a pit.

Testing this on a clean NetHack build in wizmode:
  1. Get punished
  2. Go on a pit
  3. Get swallowed by an air elemental
  4. Try to go down
  5. #wizmakemap or Save the game
... results in the iron ball being freed. Fixed in this commit.