📰 newsreader

twostopbits score 0.02 好み 0.00 en

Infidelが暴走

原題: Infidel goes wild (zarfhome.com)

infocominfidelinteractive fictionz-machinezilmemory corruptionwild pointercompiler bug
原文 ↗

日本語訳

# タイトル

Infidelが暴走する (zarfhome.com)

# 本文

Infidelが暴走する

2026年10月2日(金)

コメント: 6(ライブ含む)(最新の4時間後)

タグ: if, interactive fiction, infidel, bugs, z-machine, infocom, zil, visible zorker, patreon

昨日『Planetfall』が一般公開されましたが、もちろんここ数週間は『Infidel』に集中していました。数日前、私はInfocomのゲームでこれまで見たこともないような、非常にひどいバグに遭遇しました。

このバグは、ゲームをプレイするだけでは検出するのが極めて困難です。私がこれに気づいた最初の人物であるに違いないのは、メモリレベルのデバッガ(に相当するもの)を使ってゲームをプレイした最初の人間だからです。

では、なぜこれを「ひどいバグ」と呼ぶのでしょうか? ほとんどの人は、ゲームプレイに影響を与えることがほとんどないため、これを「些細な」バグと呼ぶでしょう。しかし、見てください。これは、意図しない方法でメモリをめちゃくちゃに書き換えてしまうワイルドポインタのバグなのです。Cプログラマとして、メモリ汚染をあらゆる罪の中で最悪のものと見なすことは、もはや義務と言ってもいいでしょう。その上、これはコンパイラ・バグなのです!

(警告: ZILコードとZ-machineの実装の詳細に踏み込みます。解析対象は、Macintosh版のアップデートであるInfidel release 22 serial 840522です。オリジナル版のserial 830916にも、メモリ番地がわずかに異なるだけで同じバグが存在します。)

状況を説明しましょう。『Infidel』の舞台はエジプトの辺境の砂漠です。プレイヤーはキャンプからスタートします。西にはナイル川があります。東には3x3のグリッド状に9つの砂漠の場所があります(そのうちの1つに、探索対象である埋められたピラミッドがあります)。

もしその領域の外へ踏み出したり、キャンプから他の方向に離れたりすると、公式に「砂漠で迷子」になります。どれだけ遠くまで移動しても、ただ砂漠が広がるだけです(日射病に襲われるまでは)。これは『Enchanter』で初めて見られた有名なトリックで、「ENDLESS-DESERT」という名前の単一の部屋が、マップされていないすべての砂漠の場所を表しています。移動した際、もしキャンプやマップされたエリアに偶然戻っていないのであれば、プレイヤーは単にENDLESS-DESERTへとループします。ゲームはプレイヤーの緯度と経度を記録しています。

これを説得力のあるものにするために、ゲームはオブジェクトをやりくりする必要があります。もしENDLESS-DESERTに物を落として移動すると、それらのアイテムは「オフステージ(画面外)」に退避されます。それらの緯度・経度座標は「DESERT-TABLE」という配列に記録されます。後でその座標に戻ると、ゲームはDESERT-TABLEからそれらのオブジェクトをENDLESS-DESERTへと戻すことができ、プレイヤーはそれらが待っているのを見つけることができます。

(おそらく。ENDLESS-DESERTでアイテムを落とした場合、3分の1の確率で即座に砂に埋もれて永遠に失われます。これは今回の件には直接関係ありませんが、後述するように、バグを発見しにくくさせている要因です。)

さて、ここまでは計画は完璧に見えます。実際に動かしてみましょう。

砂漠 あなたは砂漠にいます。砂と熱に包まれた広大な荒野です。

> AXE, SHOVEL, PICK AXEを落とす: 落とされた。shovel: 落とされた。砂丘から短くも強い突風が吹き上げ、砂が顔に舞い上がり、シャベルを見失うほど一時的に視界を遮った。

> EAST(東へ) 砂漠 あなたは砂漠にいます。砂と熱に包まれた広大な荒野です。

> WEST(西へ) 砂漠 あなたは砂漠にいます。砂と熱に包まれた広大な荒野です。ここにはpick axeがあります。

どちらの場所も同じENDLESS-DESERTですが、pickaxeがあることで別々の部屋であるかのような錯覚が維持されています。

(背を向けた隙に、シャベルが砂に埋もれて静かに消えてしまった方が、よりリアルではないでしょうか? おそらく、プレイテストの結果、そうはならなかったのでしょう。)

ともかく、それはうまく機能しているようです。バグはどこにあるのでしょうか?

これを実現しているDESERT-TO-TABLEルーチンを見てみましょう。

```zil

<ROUTINE DESERT-TO-TABLE (

SLOC

"AUX" (TBL ,DESERT-TABLE)

(CNT 0)

(F <FIRST? ,ENDLESS-DESERT>)

N)

<REPEAT ()

<COND (.F <SET N <NEXT? .F>>)

(ELSE <RETURN>)>

<COND (<EQUAL? .F ,WINNER>)

(<FSET? .F ,TAKEBIT>

<REPEAT ()

<COND (<==? <GET .TBL .CNT> 0>

<PUT .TBL .CNT .SLOC>

<PUT .TBL <+ .CNT 1> .F>

<SET CNT <+ .CNT 2>>

<REMOVE .F>

<RETURN>)

(ELSE

<SET CNT <+ .CNT 2>>)>>)>

<SET F .N>>>

```

アイテムを戻すための、似たようなTABLE-TO-DESERTルーチンもありますが、ここではこちらに焦点を当てます。

このルーチンには、必須の引数が1つあります。緯度・経度の座標であるSLOCです(これは単一の数値としてエンコードされていますが、細かいことは気にしないでください)。次に、"AUX"が4つのオプション引数をマークしており、これらはローカル変数としても機能します(Z-machineはこれらを区別しません)。TBLは、書き込み先となる配列のアドレスです。デフォルトではDESERT-TABLEになります。Fは、ENDLESS-DESERTのコンテンツリストの最初のオブジェクトに初期化されます。CNTとNはゼロに初期化されます。

実際、これが呼び出されるとき、ゲームはSLOCという引数1つだけを渡します。TBL=DESERT-TABLEというデフォルト値に依存しているのです。(もしゲーム内に2つの「終わりのない砂漠」があれば、2つの別々のテーブルを使ってこのルーチンを呼び出す必要があるでしょうが、そんなものはありません。)DESERT-TABLEは、アドレス11129から始まる100個の値(200バイト)の配列です。

関数本体は、ENDLESS-DESERT内のすべてのオブジェクト(Fから開始)をループします。持ち運び可能なオブジェクト(背景オブジェクトやプレイヤーは除く)はすべて取り除かれ、TBL配列に2つの値、つまり座標SLOCとオブジェクトIDが追加されます。テーブルにはすでにエントリがある場合があるため(複数の場所に物を落とすことができるため)、空いているスロットを見つけて埋めるように注意深く処理されます。リストの終わりに達すると終了です。ロジックが少し複雑に見えるかもしれませんが、これは連結リストを扱っていることを忘れないでください。

ここまでは順調そうでしょうか? 私にはそう見えました。「そうだ、DESERT-TABLEの内容をStateタブに表示させよう! そうすれば、オブジェクトがどのように出し入れされているか観察できるぞ」と私は思いました。

それで設定してみたのですが……うまくいきませんでした。DESERT-TABLE配列は頑なに空のままだったのです。オブジェクトが消えたり現れたりするのは確認できました(上のpickaxeを見てください!)。しかし、それらは一体どこへ行っているのでしょうか?

固定長配列を見ているCに詳しい人間なら、「

原文(英語)を表示

Infidel goes wild

Friday, October 2, 2026

Comments: 6 (plus live) (latest 4 hours later)

Tagged: if, interactive fiction, infidel, bugs, z-machine, infocom, zil, visible zorker, patreon

Planetfall was yesterday's public release, but of course I've spent the past few weeks concentrating on Infidel. A couple of days I came across the most egregious bug I've yet seen in an Infocom game.

This bug is extremely difficult to detect by playing the game. I must the first person to notice it, just because I'm the first person to play the game with (the equivalent of) a memory-level debugger.

So why do I call it an egregious bug? Most people would call it a "trivial" bug, since it almost never impacts gameplay. But look you: it's a wild pointer bug which scribbles over memory in an unintended way. As a C programmer, I am legally required to regard memory corruption as the worst of all possible sins. On top of that, it's a compiler bug!

(Warning: This will get into ZIL code and Z-machine implementation details. Note that I will be analyzing Infidel release 22 serial 840522, the update for Macintosh. The original release, serial 830916, has the same bug with slightly different memory addresses.)

Let's set the scene. Infidel takes place in the remote Egyptian desert. You start out in your camp. The Nile is to the west. To the east are nine desert locations, a 3x3 grid. (One of these holds the buried pyramid you're searching for.)

If you venture outside that region, or leave your camp in any other direction, you are officially lost in the desert. You can travel as far as you want; you'll just find more desert. (Until sunstroke finds you.) This is a known trick, first seen in Enchanter; a single room called ENDLESS-DESERT

represents every unmapped desert location. When you move, if you haven't blundered back to your camp or the mapped area, you just loop back to ENDLESS-DESERT

. The game keeps track of your latitude and longitude.

To make this convincing, the game has to juggle objects. If you drop stuff in ENDLESS-DESERT

and then move, those items are shifted offstage. Their lat/long coordinates are recorded in an array called DESERT-TABLE

. If you return to those coordinates later, the game can shuffle those objects from DESERT-TABLE

back to ENDLESS-DESERT

, and you will find them waiting for you.

(Probably. When you drop an item in ENDLESS-DESERT

, there's a one-in-three chance that it is immediately covered up by sand and lost forever. That's not relevant here, except that it makes the bug harder to uncover, as we'll see.)

Okay, so far the plan looks solid. Let's see it in action:

Desert You are in the desert, a vast wasteland of sand and heat.

>DROP AXE, SHOVEL pick axe: Dropped. shovel: Dropped. A brief but strong gust of wind comes up off a dune, whipping sand in your face, blinding you for long enough to lose track of the shovel.

>EAST Desert You are in the desert, a vast wasteland of sand and heat.

>WEST Desert You are in the desert, a vast wasteland of sand and heat. There is a pick axe here.

Both locations are the same ENDLESS-DESERT

, but the pickaxe maintains the illusion of distinct rooms.

(Wouldn't it be more realistic if the shovel disappeared silently, buried in sand, while your back was turned? Maybe that playtested badly.)

Anyhow, that seems to work fine. Where's the bug?

Let's take a look at the DESERT-TO-TABLE

routine that makes this work:

<ROUTINE DESERT-TO-TABLE (

SLOC

"AUX" (TBL ,DESERT-TABLE)

(CNT 0)

(F <FIRST? ,ENDLESS-DESERT>)

N)

<REPEAT ()

<COND (.F <SET N <NEXT? .F>>)

(ELSE <RETURN>)>

<COND (<EQUAL? .F ,WINNER>)

(<FSET? .F ,TAKEBIT>

<REPEAT ()

<COND (<==? <GET .TBL .CNT> 0>

<PUT .TBL .CNT .SLOC>

<PUT .TBL <+ .CNT 1> .F>

<SET CNT <+ .CNT 2>>

<REMOVE .F>

<RETURN>)

(ELSE

<SET CNT <+ .CNT 2>>)>>)>

<SET F .N>>>

There's a similar TABLE-TO-DESERT

routine to bring stuff back. We'll focus on this one.

The routine has one required argument: SLOC

, the lat/long coordinate. (This is encoded as a single number, but never mind that.) Then "AUX"

marks the four optional arguments, which also serve as local variables; the Z-machine doesn't distinguish. TBL

is the array address we'll be writing to. It defaults to DESERT-TABLE

. F

is initialized to the first object in the ENDLESS-DESERT

contents list. CNT

and N

are initialized to zero.

As it happens, when this is called, the game only passes one argument, SLOC

. It relies on the default value of TBL=DESERT-TABLE

. (If there were two endless deserts in the game, it would want to call this routine with two separate tables. But there ain't.) DESERT-TABLE

is an array of 100 values (200 bytes) starting at address 11129.

The function body loops through all the objects in ENDLESS-DESERT

(starting with F

). Every portable object (ignoring scenery and the player) is removed, and we add two values to the TBL

array: the coordinate SLOC

and the object ID. The table might already have entries (you can drop stuff in multiple places), so we're careful to find empty slots to fill in. When we reach the end of the list we're done. If the logic looks a bit convoluted, remember that we're dealing with a linked list.

All sound good so far? It sounded good to me. "Hey," I said to myself, "I should display the DESERT-TABLE

contents in the State tab! That way people can watch the objects being shuffled in and out."

So I set that up... and it didn't work. The DESERT-TABLE

array remained resolutely empty. I could see the objects disappearing and appearing -- just look at that pickaxe above! -- but where were they going?

Any C nerd looking at a fixed-size array will ask "What about array overflow?" But DESERT-TABLE

has a comment:

;"length should be 2*number of takeable objects"

Indeed, 100 values at two values per entry is enough space for 50 objects. Piling up every portable object in the game would get you (I think) 45. Anyhow, nothing is being stored anywhere in the array.

Time to disassemble the DESERT-TO-TABLE

function from the game file and take a look. (I warned you we'd go deep...)

Routine 109bc, 5 locals (0000, 001e, 0000, 0000, 0000)

109c7: GET_CHILD ENDLESS-DESERT -> .F [TRUE] 109cb

109cb: JZ .F [TRUE] RTRUE

109ce: GET_SIBLING .F -> .N [TRUE] 109d2

109d2: JE .F, G70 [FALSE] 109d9

109d6: JUMP 10a02

109d9: TEST_ATTR .F, #0f [FALSE] 10a02

109dd: LOADW .TBL, .CNT -> -(SP)

109e1: JZ (SP)+ [FALSE] 109fb

109e4: STOREW .TBL, .CNT, .SLOC

109e9: ADD .CNT, #01 -> -(SP)

109ed: STOREW .TBL, (SP)+, .F

109f2: ADD .CNT, #02 -> .CNT

109f6: REMOVE_OBJ .F

109f8: JUMP 10a02

109fb: ADD .CNT, #02 -> .CNT

109ff: JUMP 109dd

10a02: STORE .F, .N

10a05: JUMP 109cb

(If you're looking at the 1983 release, this routine is at address 1051a

but is otherwise identical.)

I'm using the txd

disassembly tool from ztools, but I've edited the output to show variable names. Note that the opcode names do not match ZIL terminology. txd

dates from the early 1990s. We had no ZIL manuals so we had to make up our own opcode names.

The first line sets the initial value of F

to the first object in the room (<FIRST? ,ENDLESS-DESERT>

). The second line tests whether F

is zero, which is the condition of the function's REPEAT

loop. The loop continues from there.

Notice anything missing? Where do we initialize TBL

to its default value of DESERT-TABLE

(11129)? That seems like an obvious hole.

Aha, says ZIL, we've got you covered. A Z-machine function has slots to initialize its local variables (or optional arguments) to constants. That's shown in the header lines:

Routine 109bc, 5 locals (0000, 001e, 0000, 0000, 0000)

CNT

is initialized to zero, as is N

, implicitly. F

is not initialized from a slot because it's calculated, not a constant. ZIL generates that FIRST?

(GET_CHILD

) line at the top of the function. DESERT-TABLE

is a constant, so...

...Whoops. No it isn't. DESERT-TABLE

is a global variable. Its value will never vary -- it will be 11129 through the whole game -- but ZIL doesn't know that! TBL

is initialized to the constant value 30 (hex 1e

), which is the index number of DESERT-TABLE

.

(Global variables are numbered from 16 to 255 for boring reasons. DESERT-TABLE

is the 15th global, so it's number 30.)

It seems clear that nobody thought about this case (an argument defaulting to a global variable value). If they had, ZIL would either display a compiler error ("Default value is not a constant!") or generate a STORE

instruction at the top of the function (same as how F

is initialized). 30 is simply wrong.

The upshot is that when DESERT-TO-TABLE

start writing object numbers to memory, it doesn't write to address 11129. It overwrites memory addresses 30 and up. Oh dear.

The TABLE-TO-DESERT

routine uses a default global argument in exactly the same way. So it pulls the object list back from address 30, producing apparently correct results. But memory has been stomped.

So what's at that address?

The first 64 bytes of memory are header data, but not all of the header is used. By sheer luck, only addresses 0-29 are meaningful in Z-machine version 3. If DESERT-TABLE

had been the third global, index 18, this bug would have corrupted the game's serial number! Which would be a lot more obvious.

So addresses 30-63 are unused, but address 64 is the beginning of the abbreviations data. This is part of the Z-machine text compression scheme: a list of 96 words which occur frequently and can therefore be pulled out and replaced by a few bits.

Prediction: if we drop nine objects in the desert (and then walk away), the last one will overwrite the start of the abbreviations table.

This is a bit of a nuisance to arrange. Remember that dropped objects have a one-in-three chance of simply disappearing. I had to drag a lot of stuff out there and try it a few times. But I succeeded, and the next text the game printed looked like this:

Desert You are in the desert, a vast wasteland of sand and heat. A small lizard pokes its head up from You sands and asks You shortest route to Times Square. You scratch your head for a moment and when you remember You proper subway line to recommend to it, you notice it's disappeared.

This is of course a heat-driven hallucination, but notice that the word "the" (the first abbreviation) has been replaced by "You" (the second). This indicates that the early string data has been overwritten by junk which happens to have no terminator, so the print routine continues to the next valid string.

In another test I managed to crash the interpreter. I suspect that it overwrote the first abbreviation with the bits representing the first abbreviation, so the print routine went into an infinite loop. You know. As one does.

Okay! Bug demonstrated! What have we learned?

This bug shipped and absolutely nobody noticed. When I played Infidel as a kid and explored the desert, my game was corrupting memory. Same with every other player, including Infocom's playtesters.

It's very likely that nobody tried dropping more than eight objects out in the Endless Desert. Why would you? There's nothing out there! It's not on the way to anywhere. The entire mechanism exists only to occupy players who have not yet discovered the pyramid. The attention to detail is typical of Infocom, but that area can't have been playtested very hard.

(I would love to peruse an Infocom testing log to see if any trace of the bug appeared. The Infocom Cabinet doesn't seem to have anything relevant. Let know if you come across such a thing.)

The 1984 revision of the game did nothing to address the bug. Infocom did not have anything like the Visible Zorker. They might have had debugging tools to inspect memory, but nobody thought to apply them to these routines and their storage.

I have not surveyed later games to see if a similar bug exists -- or is correctly handled by the compiler. I'll keep an eye out.

How would one fix the bug? Simple: remove the argument default for TBL

, and instead start the routine with <SET TBL ,DESERT-TABLE>

. Or else pass in DESERT-TABLE

as a second argument to the routine. Either would work.

There's another pair of routines PROP-TO-TBL

, TBL-TO-PROP

which have a similar bug with the PROP-TBL

table (address 11329, global number 22). I am honestly a bit baffled by these routines. They look like they're doing a similar shuffle-job with the holes you can dig in the sand. (These are stored in desert rooms as the CAPACITY

property.) But the game doesn't let you dig in ENDLESS-DESERT

; it says "The ground is too hard here." So the routines appear to be useless. They may be a remnant of an earlier feature which was removed during development. Perhaps because it corrupted the serial number!

If I were updating Infidel (as opposed to riffing it), I would definitely restore the ability to dig useless holes out in the desert. That kind of pointless detail is one of the great joys of IF.

Comments from Bluesky

Comments from Mastodon

Comments from Mastodon (live)

Please wait...

← 一覧に戻る