TECHNICAL CASE NOTE 旧作ゲーム · 起動失敗 · Wine / mmap 解決済み・2026-06-13 実機確認

Wine更新後、一部の旧作が起動しない

file-backed mmap の EXEC 付与を二段階にする

§1

Wine更新後、特定の旧作だけ起動しなくなった

ゲーム本体と実行環境は変えず、内蔵する Wine のビルドだけを更新したところ、一部の旧作が起動直前で停止した。ファイルの読み込みには成功するが、読み込んだ領域へ実行許可を設定する段階で失敗していた。

旧ビルドでは何ごともなく起動する

比較条件: ゲーム本体と prefix を固定し、Wine のビルドだけを変更した。

新ビルドに切り替えた途端エラー画面

観測: 起動プロセスは生成されるが、ゲーム画面へ進む前に停止した。

ログに同一アドレスへの失敗が10万回オーダー

観測: 同一サイズのマッピング失敗がアドレスを変えながら反復していた。

実行コードを再配置するロード経路で発生

対象範囲: 通常の起動経路ではなく、実行コードを再配置する経路で発生した。
ログと実体の差: Wine は failed to set PROT_EXEC on file map, noexec filesystem? と出力していた。しかし対象はローカル APFS ボリュームで、マウント設定に noexec はなかった。この文言は原因の確定ではなく、失敗箇所から導いた推測として扱った。
§2

検証した仮説

ログ、マウント設定、メモリ領域、旧ビルドの実体を順に比較し、成立しない説明を除外した。最後に最小構成で mmap と mprotect の順序を比較した。

仮説 1

ログの言葉どおり、noexec なファイルシステム棄却

エラー文言がそのまま「noexec filesystem?」と疑問形で示唆していた。まずは疑わず、素直にそこを疑ってみた。

棄却した根拠: ゲームデータはローカル APFS ボリュームにあり、マウント設定に noexec はなかった。同じ場所から起動できる別タイトルも確認できた。
仮説 2

JIT 的な実行メモリの確保制限棄却

W^X のような仕組みにより、実行可能なメモリ領域の確保自体が制限されている可能性を調べた。

棄却した根拠: 起動中プロセスを観測すると、失敗している予約領域の最大許可(max protection)は rwx だった。実行許可の上限は確保されていた。
仮説 3

互換レイヤーの旧世代には、この壁を越える独自パッチがあるはず棄却

「動く旧ビルド」と「動かない新ビルド」がある以上、旧ビルドだけが持つ何らかの独自対応があるはずだ ― と見て、旧ビルドの該当バイナリを逆アセンブルして読んだ。

棄却した根拠: 比較した map_file_into_view の経路からは、起動可否を説明できる独自処理を確認できなかった。公開されている別実装の履歴を調べ、二段階で EXEC を付与する方法へ進んだ。
仮説 4・採用

対象条件では、mmap 時の PROT_EXEC 付与だけが失敗する採用

最小構成で、file-backed な領域を MAP_SHARED | MAP_FIXED | PROT_EXEC 付きで mmap すると EACCES になる条件を再現した。

確認した差: 同じ領域をまず読み取り可能として mmap し、その後 mprotectPROT_EXEC を追加すると成功した。最終的な保護属性ではなく、EXEC を付与する順序が成否を分けていた。
§3

EXEC付与の順序を比較する

修正前後で変えたのは、実行許可を付けるタイミングだけである。タブを切り替えると、mmap 時に要求する経路と、mprotect で後から追加する経路を比較できる。

mmapMAP_SHARED + MAP_FIXED + EXEC
マッピング結果EACCES
起動継続— 進めない —
0
観測したマッピング失敗回数
 
§4

調査手順

原因候補を増やす前に、ログが示す内容と実環境の差を確認した。その後、比較条件を固定し、観測範囲を OS の呼び出し単位まで縮小した。

ログの言葉を鵜呑みにしない

「noexec filesystem?」という疑問形の文言は、Wine 自身の推測にすぎない。マウント設定を実際に確認し、示唆と事実を切り分けた

Wine のビルドだけを変えた A/B を行う

同じゲームファイルと prefix を使い、Wine のビルドだけを入れ替えた。ゲーム側と環境側の変数を固定し、回帰の範囲を Wine 内へ絞った。

旧ビルドの実体を確認する

起動できる旧ビルドに独自処理がある可能性を、該当バイナリの比較で確認した。比較した関数の経路では説明できず、公開ソースと変更履歴の調査へ切り替えた。

メモリ観測ツールで「実行許可の上限」を実測する

失敗している予約領域の最大許可を確認した。rwx が許可されていたため、実行可能領域の確保自体が禁止されているという説明を除外した。

最小の再現コードで、通る手順だけを絞り込む

OS 標準の呼び出しだけを使い、mmap 時に EXEC を要求する手順と、読み取り可能として mmap した後に mprotect で EXEC を追加する手順を比較した。

§5

観測結果

観測 A ── 失敗ログの集計

map_file_into_view failed to set PROT_EXEC on file map,
noexec filesystem?

0x1560000 ── 62,658 回
0x1760000 ── 37,414 回
0x1460000 ── 2,900 回
その他      ── 数〜数百回
──────────────────────
合計       ── 約103,777 回(全て同一サイズ)

同一サイズの要求がアドレスを変えながら10万回オーダーで反復していた。単発のクラッシュではなく、マッピングの再試行が継続していた。

観測 B ── メモリ観測ツールの実測

失敗している予約領域:
  現在の許可   : ---  (none)
  最大許可     : rwx  (実行も許可済み)

実際に実行できている別領域:
  現在の許可   : rwx  (anon private)
→ 制限は「確保できるか」でなく
  「file を EXEC 付きで写せるか」だけ

最大許可は rwx だった。問題は実行許可の上限ではなく、file-backed な領域を EXEC 付きで mmap する手順に絞られた。

観測 C ── 通る手順の実証テスト

互換レイヤーが踏んでいた形(ログより):
mmap(MAP_SHARED | MAP_FIXED, PROT_EXEC)
  → EACCES

最小テストで実証した形:
mmap(MAP_SHARED | MAP_FIXED, PROT_READ)
  → OK
mprotect(同領域, +PROT_EXEC)
  → OK

同じ実行可能なマップ済み領域でも、EXEC を付与する順序で成否が変わった。この差に合わせて Wine の macOS 経路を修正した。

§6

再利用できる知見

ログの推測と環境の事実を分ける

「noexec filesystem?」は原因名ではなく、失敗地点から出力された推測だった。マウント設定を別に確認する必要がある。

A/B で変数を1つずつ潰す

同一ファイル・同一環境で「互換レイヤーのビルドだけ」を入れ替え、差をそこだけに絞り込んだ。

旧ビルドの差を実体で確認する

起動できる旧世代に独自パッチがあるという仮説も、該当バイナリを比較してから判断する。

読めることと、動かせることは別の許可

OS はファイルを読むことと、そこに書かれたコードとして実行することを、別々の入口で管理している。両方が通って初めて動く。

公開された対応策の「正典」に当たる

同じ壁を越えた実績のある、ライセンスの明らかな公開ソースへ最終的にたどり着き、概念だけを正確に汲み取った。

§7

現在の状態と適用範囲

確認できていること

  • macOS 向けの map_file_into_view では、mmap 時に PROT_EXEC を外し、成功後の mprotect で追加する実装を現在も使用している。
  • 2026-06-13 の実機検証では、対象ログの失敗回数が 103,777 回から 0 回になり、影響を受けていた2タイトルの起動を確認した。
  • 変更は __APPLE__ 条件内にあり、他 OS のコード経路には適用されない。

この結果から断定しないこと

  • すべての旧作や、すべての mmap 失敗が同じ原因とは限らない。
  • noexec filesystem? というログだけで本件と同一視せず、マウント設定と mmap のフラグを確認する。
  • 起動確認の範囲は当時の2タイトルであり、現在の全タイトルに対する回帰試験を意味しない。