<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Boa</title><link>https://blog.ast.moe/tags/boa/</link><description>Recent posts from 五里霧中</description><generator>Hugo</generator><item><title>BoaでPromiseを保持し忘れてmallocが壊れた</title><link>https://blog.ast.moe/blog/2026-09-10-4/</link><pubDate>Thu, 10 Sep 2026 20:37:07 +0900</pubDate><guid>https://blog.ast.moe/blog/2026-09-10-4/</guid><description>&lt;p&gt;BoaのTest262を回していたら、たまにテストが終了コード134で落ちました。&lt;/p&gt;
&lt;p&gt;ログには、こんなエラーが出ています。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;malloc(): unsorted double linked list corrupted
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Issueは&lt;a href="https://github.com/ieee0824/omoikane/issues/638"&gt;通常Test262のrelease実行がallocator整合性エラーで中断する&lt;/a&gt;です。&lt;/p&gt;
&lt;p&gt;最初は、最近入った変更のどこかでメモリを壊しているのだろうと思いました。ただ、Test262は並列に大量のテストを実行するので、ログだけではどのテストが原因なのか分かりません。&lt;/p&gt;
&lt;h2 id="まずは変更前後を比べる"&gt;まずは変更前後を比べる&lt;/h2&gt;
&lt;p&gt;最初にやったのは、変更前と変更後の比較です。&lt;/p&gt;
&lt;p&gt;Boaの変更前revisionと、問題が見つかったrevisionをそれぞれreleaseビルドして、同じ条件でTest262を実行しました。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Ubuntu x86_64
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;release build
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2 MiB worker stack
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;4 workers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;この条件では、変更前後ともに50,595ケースを最後まで実行できました。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;47,606 passed
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2,056 ignored
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;933 failed
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;0 panic
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;allocatorの異常終了も再現しませんでした。&lt;/p&gt;
&lt;p&gt;実行ファイルのSHA256も一致していたので、少なくともこの実行結果からは、別の変更によって壊れたとは言えません。&lt;/p&gt;
&lt;p&gt;ただ、問題のCIでは2回続けて終了134になっています。再実行が通ったからといって、問題が解決したとは扱えません。&lt;/p&gt;
&lt;h2 id="小さいケースに絞る"&gt;小さいケースに絞る&lt;/h2&gt;
&lt;p&gt;ログを見ていると、落ちる直前には次のようなsuiteが実行されていました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GeneratorFunction/prototype&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;classのasync private method&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;どちらもPromiseやasync処理に関係しています。&lt;/p&gt;
&lt;p&gt;そこで、全Test262を何度も回すのではなく、Promiseの解決処理でGCが動く条件を小さいテストにしました。&lt;/p&gt;
&lt;p&gt;問題になったのは、Promiseの解決中に&lt;code&gt;resolve&lt;/code&gt;や&lt;code&gt;reject&lt;/code&gt;を取り出し、そのあとで&lt;code&gt;then&lt;/code&gt; getterを呼ぶような処理です。getterの実行中には、ユーザーコードが動く可能性があります。そこではGCも発生します。&lt;/p&gt;
&lt;h2 id="promiseへの参照が消えていた"&gt;Promiseへの参照が消えていた&lt;/h2&gt;
&lt;p&gt;Promiseの処理では、内部の共有captureから&lt;code&gt;resolve&lt;/code&gt;や&lt;code&gt;reject&lt;/code&gt;を取り出します。&lt;/p&gt;
&lt;p&gt;このとき、Promiseを追跡可能な参照として保持している場所まで一緒に失われていました。&lt;/p&gt;
&lt;p&gt;通常はすぐに問題が起きません。処理が短く、GCも動かなければ、たまたまメモリ上に残った状態で処理が進むからです。&lt;/p&gt;
&lt;p&gt;しかし、そのあとに&lt;code&gt;then&lt;/code&gt; getterやhost hookを呼び出すと、ユーザーコードが実行されます。その間にGCが走ると、Promiseが不要なオブジェクトだと判断され、回収される可能性があります。&lt;/p&gt;
&lt;p&gt;その状態で後続処理がPromiseを使おうとすると、解放された領域を参照することになります。&lt;/p&gt;
&lt;p&gt;最終的に見えていたのが、allocatorの内部データ構造が壊れたというエラーでした。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;malloc(): unsorted double linked list corrupted
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;エラーがmallocから出ているので、最初はヒープのどこかを直接壊しているように見えます。実際には、GCによる回収と参照の保持が正しく対応していないことが入口でした。&lt;/p&gt;
&lt;h2 id="回帰テストで再現する"&gt;回帰テストで再現する&lt;/h2&gt;
&lt;p&gt;修正前のrevisionに回帰テストだけを追加して実行すると、ARM64 LinuxでSIGSEGVを再現できました。&lt;/p&gt;
&lt;p&gt;このテストは、次のような条件を含めています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;then&lt;/code&gt; getterがcallableを返す&lt;/li&gt;
&lt;li&gt;&lt;code&gt;then&lt;/code&gt; getterがcallableではない値を返す&lt;/li&gt;
&lt;li&gt;&lt;code&gt;then&lt;/code&gt; getterが例外を投げる&lt;/li&gt;
&lt;li&gt;Promise解決中に再入して&lt;code&gt;resolve&lt;/code&gt;や&lt;code&gt;reject&lt;/code&gt;を呼ぶ&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;単純にPromiseが解決するだけなら問題が出ないので、getterの呼び出しとGCが入り得る箇所を組み合わせる必要がありました。&lt;/p&gt;</description></item><item><title>BoaがASANのfake stackでスタック使用量を誤判定した</title><link>https://blog.ast.moe/blog/2026-09-10-3/</link><pubDate>Thu, 10 Sep 2026 20:14:12 +0900</pubDate><guid>https://blog.ast.moe/blog/2026-09-10-3/</guid><description>&lt;p&gt;&lt;a href="https://blog.ast.moe/blog/2026-09-10-2/"&gt;前回の記事&lt;/a&gt;では、Boaのパーサーが2 MiBのスタックを使い切る問題を修正しました。&lt;/p&gt;
&lt;p&gt;再帰中のフレームを小さくし、深すぎる入力にはエラーを返すようにしました。通常のビルドでは、元のTest262も2 MiBで通るようになっています。&lt;/p&gt;
&lt;p&gt;その状態でAddressSanitizer（ASAN）を使った診断を進めると、今度は浅い入力まで拒否するようになりました。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;parser recursion limit exceeded
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;スタックを使い切らないように追加した保護が、使い切っていないところで止めていました。&lt;a href="https://github.com/ieee0824/omoikane/issues/640"&gt;Issue #640&lt;/a&gt;の話です。&lt;/p&gt;
&lt;h2 id="同じ実行ファイルなのに設定を変えると通る"&gt;同じ実行ファイルなのに設定を変えると通る&lt;/h2&gt;
&lt;p&gt;最初に確認したのは、Test262の&lt;code&gt;test/built-ins/Function/prototype/constructor&lt;/code&gt;です。&lt;/p&gt;
&lt;p&gt;x86_64 Linux、Rust 1.98.1、ASANを有効にしたreleaseビルドで、workerのスタックは8 MiBありました。&lt;/p&gt;
&lt;p&gt;同じ実行ファイルとテストを使い、ASANの設定だけを切り替えたところ、結果が変わりました。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;detect_stack_use_after_return&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;結果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0/1。&lt;code&gt;assert.js&lt;/code&gt;の読み込み時に再帰制限へ到達&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1/1。normalとstrictの両方で成功&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;比較ログは&lt;a href="https://github.com/ieee0824/boa/actions/runs/34409886173"&gt;当時のCI run&lt;/a&gt;のartifactにある&lt;code&gt;calibration-1.log&lt;/code&gt;と&lt;code&gt;calibration-0.log&lt;/code&gt;に残しています。&lt;/p&gt;
&lt;p&gt;テスト本体の複雑な処理どころか、テスト用の&lt;code&gt;assert.js&lt;/code&gt;を読み込む段階で止まっています。前回の深い入れ子とは少し様子が違いました。&lt;/p&gt;
&lt;h2 id="ローカル変数のアドレスで測っていた"&gt;ローカル変数のアドレスで測っていた&lt;/h2&gt;
&lt;p&gt;前回追加したスタック使用量の検査では、解析の入口で小さなローカル変数を作り、そのアドレスを記録していました。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; marker &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0_&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;u8&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cursor.enter_parser(std::ptr::from_ref(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;marker) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;, interner)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一番外側の解析で記録したアドレスと、現在の解析で得たアドレスの差を、スタック使用量として扱う方式です。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;外側の解析にあるmarkerのアドレス
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓ アドレスの差
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;再帰先の解析にあるmarkerのアドレス
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ここには、両方の&lt;code&gt;marker&lt;/code&gt;が同じnative stack上に置かれているという前提があります。&lt;/p&gt;
&lt;p&gt;ASANのfake stackを有効にすると、この前提が崩れました。&lt;/p&gt;
&lt;h2 id="fake-stackでは変数が別の場所に置かれる"&gt;fake stackでは変数が別の場所に置かれる&lt;/h2&gt;
&lt;p&gt;ASANには、関数から戻ったあとで、その関数のローカル変数を参照してしまう不具合を検出する機能があります。stack-use-after-returnの検査です。&lt;/p&gt;
&lt;p&gt;通常のスタック領域は、関数から戻ると次の呼び出しで再利用されます。そこでASANは検査対象のローカル変数をfake stackという別の領域へ配置し、関数が終わったあとにその領域へのアクセスを検出できるようにします。&lt;a href="https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn"&gt;ASANの設計文書&lt;/a&gt;に仕組みが説明されています。&lt;/p&gt;
&lt;p&gt;この配置では、ローカル変数のアドレス同士が遠く離れていても、その分だけnative stackを消費したとは限りません。&lt;/p&gt;
&lt;p&gt;今回の実装は、その距離をそのまま使用量として扱っていました。浅い解析でも1.5 MiBを超えたと判定され、保護処理がエラーを返していたわけです。&lt;/p&gt;
&lt;p&gt;ASANが不正アクセスを報告して止めたのではなく、ASANによって変わった変数配置をパーサー側が読み違えていました。&lt;/p&gt;
&lt;h2 id="スタックポインタを直接読む"&gt;スタックポインタを直接読む&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/ieee0824/boa/pull/89"&gt;Boa PR #89&lt;/a&gt;では、x86_64とARM64について、CPUのスタックポインタを直接読む方式に変更しました。&lt;/p&gt;
&lt;p&gt;x86_64では&lt;code&gt;rsp&lt;/code&gt;を読みます。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; address: &lt;span style="color:#66d9ef"&gt;usize&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;unsafe&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; core::arch::&lt;span style="color:#a6e22e"&gt;asm!&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;mov {}, rsp&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; out(reg) address,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options(nomem, nostack, preserves_flags)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ARM64では&lt;code&gt;sp&lt;/code&gt;を読みます。&lt;/p&gt;</description></item><item><title>Boaのパーサーが2 MiBのスタックを使い切った</title><link>https://blog.ast.moe/blog/2026-09-10-2/</link><pubDate>Thu, 10 Sep 2026 19:58:13 +0900</pubDate><guid>https://blog.ast.moe/blog/2026-09-10-2/</guid><description>&lt;p&gt;OmoikaneへBoaを取り込む前にTest262を動かしていたところ、&lt;a href="https://github.com/ieee0824/omoikane/issues/633"&gt;未最適化のパーサーがstack overflowでabortするケース&lt;/a&gt;が見つかりました。&lt;/p&gt;
&lt;p&gt;失敗したのはTest262の&lt;a href="https://github.com/tc39/test262/blob/a073f479f80b336256b7fc4e04700c827293e2fe/test/staging/sm/regress/regress-672893.js"&gt;&lt;code&gt;regress-672893.js&lt;/code&gt;&lt;/a&gt;です。&lt;/p&gt;
&lt;p&gt;無効なJavaScriptを何万段も入れ子にしたわけではありません。関数式を15段ほど入れ子にした有効なJavaScriptでした。&lt;/p&gt;
&lt;p&gt;構造を省略すると、次のようなコードです。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;function f() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; return function () {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; return function () {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...同じ形で15段まで続く...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; return function (a) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; var v = a;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; return function () { return v; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f()()() ... ()(42)();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このコードをdev profileでビルドしたBoaへ渡し、スレッドスタックを2 MiBにすると、パーサーがstack overflowを起こしてexit 134で終了しました。&lt;/p&gt;
&lt;p&gt;同じコードでも8 MiBなら通ります。また、JITやJavaScriptの実行中ではなく、ソースコードをASTへ変換する途中で落ちていました。&lt;/p&gt;
&lt;h2 id="入れ子1段で約125-kb使っていた"&gt;入れ子1段で約125 KB使っていた&lt;/h2&gt;
&lt;p&gt;デバッガで関数式の解析フレーム間を測ると、修正前の未最適化aarch64 Linuxでは、入れ子1段あたり125,360 bytes使っていました。&lt;/p&gt;
&lt;p&gt;15段なら単純計算で約1.88 MBです。これにパーサー以外の呼び出し、lexer、エラー処理なども加わるため、2 MiBのスタックには収まりません。&lt;/p&gt;
&lt;p&gt;原因は、再帰的な式解析が後半の処理で使うASTの一時値を保持したまま、次の関数本体を解析していたことでした。&lt;/p&gt;
&lt;p&gt;例えば二項演算子を処理するパーサーでは、まず左辺を解析し、そのあとに続く演算子と右辺を処理します。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;左辺を解析する
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;演算子が続いているか調べる
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;右辺を解析してASTを組み立てる
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;関数全体が一つになっていると、後半で使う変数やASTのための領域まで含んだ大きいフレームを作り、その状態で左辺の解析へ入ります。&lt;/p&gt;</description></item></channel></rss>