2 ポイント 投稿者 GN⁺ 1 일 전 | 1件のコメント | WhatsAppで共有
  • GhostLock(CVE-2026-43499) は Linux 2.6.39 で導入され、7.1 で修正されたカーネル脆弱性で、非特権のローカル攻撃者が一般的なスレッド関連システムコールだけでスタック UAF を引き起こし、root 権限の取得とコンテナ脱出に悪用できる
  • Requeue-PI プロキシ経路の remove_waiter() が実際の待機タスクではなく currentpi_blocked_on を消去するため、ユーザー空間に復帰したタスクに 解放済みスタックフレームを指すポインタ が残る
  • 3つの futex と 3つのスレッドで PI 依存関係の循環を作り、-EDEADLK のロールバックを誘発し、PR_SET_MM_MAP の制御可能なスタックバッファに偽の rt_mutex_waiter を構築して 制約付きポインタ書き込み を確保する
  • エクスプロイトは prefetch で KASLR・physmap の基準アドレスを見つけ、CPU entry area(CEA)に偽構造体と ROP スタックを配置したうえで、inet6_protos[IPPROTO_UDP] を上書きし、IPv6 UDP ループバックパケットで制御フローを乗っ取る
  • 研究チームは 97% の安定性を持つ権限昇格・コンテナ脱出 エクスプロイトにより Google kernelCTF で 92,337 ドルを受け取り、未パッチのすべての Linux ディストリビューションは最新 LTS へアップグレードすべきだとしている

影響範囲と脆弱性の概要

  • GhostLock は VEGA が発見した Linux カーネル脆弱性で、追加権限やユーザーネームスペースなしにローカルの非特権ユーザーがトリガーできる
  • 8161239a8bcc の rtmutex 再実装によって導入され、影響範囲は v2.6.39-rc1 から v7.1-rc1 まで
  • 2026年4月に 3bfdc63936dd で修正され、必要なカーネル設定は CONFIG_FUTEX_PI=y のみ
  • 攻撃者は次の手順で権限を昇格できる
    • 一般的なスレッド関連システムコールだけで、カーネルスタックメモリを指す ダングリングカーネルポインタ を取得する
    • ほぼ任意のアドレスにポインタまたは 8 バイトの 0 を書き込める制約付きプリミティブを作る
    • 関数テーブルを乗っ取って制御フローを掌握し、root 権限を取得する
  • 未パッチのすべての Linux ディストリビューションが影響を受けるため、最新の LTS バージョンへアップグレードする必要がある

remove_waiter() が誤ったタスクをクリーンアップする原因

  • kernel/locking/rtmutex.cremove_waiter() は、もともとブロックされたスレッドが自分の待機状態を直接クリーンアップする経路向けに書かれている
  • 通常のスローパスでは、実行中の current が waiter の所有タスクであるため、current->pi_blocked_on を消去する動作は正しい
  • Requeue-PI プロキシ経路 では、rt_mutex_start_proxy_lock() が別のスリープ中タスクの代わりに rt_mutex_waiter をキューに入れ、エラーが発生するとそれをロールバックする
    • このとき currentFUTEX_CMP_REQUEUE_PI を呼び出した requeuer
    • 実際の waiter は FUTEX_WAIT_REQUEUE_PI でスリープしている別タスク
  • __rt_mutex_start_proxy_lock()-EDEADLK を返すと、remove_waiter() は waiter を lock から外す一方で current->pi_blocked_on だけを NULL にする
  • 実際の waiter の pi_blocked_on は、自身のカーネルスタック上にある rt_mutex_waiter を引き続き指し、waiter がユーザー空間に復帰すると、そのスタックフレームは解放済みと見なされる
  • 以後、PI チェーン探索がそのタスクを通過した瞬間に 解放済みスタックオブジェクトを逆参照 する
  • lockdep はどの pi_lock が保持されたかだけを確認し、それが誰の lock かは検査しないため、この不具合を検出できない

-EDEADLK ロールバックを作る 3 つの futex 循環

  • エラーパスに到達するには、3つの futex と 3つのスレッドで PI 依存関係の循環を構成する
    • f_pi_chain: waiter が先にロックする PI futex
    • f_pi_target: owner が先にロックし、requeue の対象になる PI futex
    • f_wait: waiter が FUTEX_WAIT_REQUEUE_PI で待機する通常の futex
  • トリガー順序は次の通り
    1. waiter が f_pi_chain をロックした後、FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target) でブロックされ、rt_mutex_waiter が自身のカーネルスタック上に置かれる
    2. owner が f_pi_target をロックした後、waiter が保持する f_pi_chain でブロックされる
    3. main スレッドが FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target) を呼び出す
  • プロキシ requeue が waiter を f_pi_target に接続しようとすると、waiter → f_pi_target → owner → f_pi_chain → waiter の循環が閉じる
  • PI チェーン探索が -EDEADLK を返して誤ったロールバックを実行し、waiter はダングリングな pi_blocked_on を持ったまま起床する
  • 重要な条件は、waiter がまだスタックオブジェクトを保持している間に requeuer がロールバックすることであり、循環が完成した後は処理が自走する
  • waiter がユーザー空間に戻れば、もはや時間的制約はなく、後からいつでも sched_setattr() でチェーン探索を発生させられる
  • 構成には 3 スレッドを使うが、UAF の競合自体は 1つの CPU コアでもトリガー可能

スタック UAF が提供する初期プリミティブ

  • ダングリングポインタは、以前の FUTEX_WAIT_REQUEUE_PI フレームにあった rt_mutex_waiter を指している
  • 同じタスクの同じスタック深度に制御可能なバイト列を再配置すれば、カーネルにそれを偽の rt_mutex_waiter として逆参照させられる
  • 偽構造体をどう配置するかによって、1回のアクセスで 2 種類の主要なプリミティブを得られる
    • 制約付きで、ほぼ任意のアドレスに ポインタを書き込める
    • 制約付きで、ほぼ任意のアドレスに 8 バイトの 0 を書き込める
  • 書き込み前に複数のポインタ逆参照と整合性チェックが行われるが、条件を満たせば書き込み後もカーネルはクラッシュせず正常に復帰する
  • エクスプロイトを完成させるには、スタックフレームの再利用、偽 waiter の構造チェック通過、書き込み制約を満たす対象の選定がすべて必要になる

PR_SET_MM_MAP による解放済みスタックフレームの再利用

  • waiter は futex システムコールから戻るとすぐに prctl(PR_SET_MM, PR_SET_MM_MAP, ...) を呼び出す
  • prctl_set_mm_map() はユーザー提供の auxv を固定サイズのスタックバッファ unsigned long user_auxv[AT_VECTOR_SIZE] にコピーする
  • このバッファは解放済み waiter と近いスタック深度に配置されるため、大きく整列した制御可能な qword ブロックが以前の rt_mutex_waiter の上に重なる
  • auxv の重ね合わせ領域は次のように構成する
    • tree: 削除時に選ばれた子ポインタ W0_BASE をツリールートへ昇格させる rb ノードにする
    • task: チェーン探索の逆参照を安全に通過できるよう &init_task に設定する
    • lock: 書き込み先を合わせるため &inet6_protos[IPPROTO_UDP] - 8 に指定する
    • wake_state: 0 に設定する
  • auxv を memfd に置き、コピーがページ境界をまたぐように配置したうえで、兄弟スレッドが prctl 実行中に後ろ側のページへ fallocate(PUNCH_HOLE) の競合を仕掛け、copy_from_user の時間を引き延ばす
  • 別 CPU の consumer スレッドは、偽 waiter がスタック上に残っている間に waiter に対して sched_setattr() を呼び出し、PI チェーンを探索する
  • clonesetsockoptpselectkeyctl のように、制御可能な大きいスタックローカル変数を使う他のシステムコールでも同じ役割を果たせる
  • prctl が選ばれたのは、バッファが大きく整列しており、名前空間を必要としないためであり、追加候補は 公開 PoC コード に含まれている

rb-tree 削除で制約付きポインタ書き込みを作る

  • 偽 waiter を制御できても直ちに完全な任意書き込みになるわけではなく、チェーン探索は次の経路を実行する
    • task->pi_blocked_on から偽 waiter を見つける
    • fake waiter->lock から偽 rt_mutex_base を見つける
    • rt_mutex_dequeue(lock, waiter)lock->waiters で rb-tree の削除を行う
  • 子が1つしかないルートノードを削除すると、その子がルートスロットに書き込まれる性質を利用する
  • locktarget - 8 に指定すると、周辺データは次の rt_mutex_base フィールドとして解釈される
    • target - 8: ロックされていない状態として読まれる必要がある wait_lock
    • target: 上書きされる waiters.rb_root.rb_node
    • target + 8: waiters.rb_leftmost
    • target + 16: owner
  • 結果として実行される単一の書き込みは *(uint64_t *)target = W0_BASE である
  • 対象アドレスはおおむね次の条件を満たす必要がある
    • target - 0x08 の下位 32 ビットが 0 であること
    • target + 0x08 の 64 ビット値が 0 であること
    • target + 0x10 の owner ポインタから下位フラグを除いた値が 0 であること
  • 手前の qword がロック済み spinlock のように見えると trylock が失敗し、何も書き込まずに終了する
  • 後ろ側の値が制御されていない top waiter や owner を指す、あるいは未マップ値である場合、カーネルパニックが発生する可能性がある
  • W0_BASE は比較、再キューイング、優先度更新、owner なし wakeup が終わるまで有効である必要があるため、CEA の direct-map エイリアスを使う

prefetch リークと CPU entry area

  • KASLR・physmap ベースアドレスの特定

    • 特定アドレスに対する prefetch の実行時間は、現在のページテーブルでそのアドレスがマップされているかどうかによって変わる
    • 非特権プロセスがカーネルアドレス範囲の実行時間を測定すると、マップ位置を推定でき、詳細な原理は prefetch 論文 にまとめられている
    • 基本 Linux カーネルイメージのベースアドレスのエントロピーは約 9 ビットであるため、反復測定によって KASLR ベースアドレスをほぼ 100% の信頼度で復元できる
    • 理論上は prefetch があり適切な KPTI がない CPU が影響を受けるが、実際には KPTI が無効な x86 で主に使われる手法である
    • kernelCTF イメージでは KPTI が無効であり、KPTI が有効でも prefetchEntryBleed を組み合わせれば trampoline を通じてカーネルイメージのベースアドレスを復元できる
  • CEA アドレスランダム化の回避

    • CPU entry area(CEA) は、エントリ処理・例外処理用のスタックとレジスタコンテキストを保持する x86 の CPU ごとの構造体である
    • 非特権プログラムがソフトウェア例外を発生させると、自身のレジスタコンテキストを CEA 例外スタックの pt_regs に記録でき、約 120 バイトの連続した制御可能メモリを作れる
    • Linux 6.2 より前では CEA 仮想アドレスは完全に固定されており、偽構造体、ポインタ逆参照の副作用吸収、ROP スタック構築にそのまま利用できた
    • Project Zero の Bringing back the stack attack 公開後、Linux 6.2 から CEA 仮想アドレスは強くランダム化されるようになった
    • 各 CPU の CEA 仮想アドレスはそれぞれ異なるようにランダム化されるが、物理アドレスは固定されているため、physmap ベースアドレスが分かれば direct-map エイリアスを計算できる
    • prefetch、候補境界の正規化、想定される CEA ページの検査を組み合わせることで、周辺エイリアスを除外し、cea_direct = physmap_base + CPU1_CEA_BASE を求める
    • kernelCTF LTS 6.12.80 の 3.5GB ブート環境では、関連オフセットは 0x11c517000(+0x1f58) である

CEA を偽 waiter と後続オブジェクトとして再利用

  • 最初の書き込みの前に、CEA の W0 に自己整合性を持つ偽 waiter と lock を配置する
    • task&init_task に設定する
    • prio には有効な値を入れる
    • lock の wait_lock はロックされていない状態に見えるようにする
    • owner は dequeue、再キューイング、優先度更新、wakeup を安全に通過できるよう構成する
  • rb-tree 書き込みが終わると W0 はもう waiter である必要がないため、上書きした対象が要求する構造体として CEA を再度埋め直せる
  • CEA は約 120 バイトと小さいが、計算可能な固定カーネルアドレスにデータを配置できるため効率的である
  • NPerm や kernelsnitch などは、より広い領域で同じ役割を果たせる
  • エクスプロイトは 1 つの CEA 領域を、偽 rt_mutex_waiter、偽 lock、inet6_protocol、JOP・スタックピボット用スロット、最終的な ROP スタックとして順次または同時に活用する

inet6_protos[IPPROTO_UDP] による制御フロー奪取

  • 一般的な x86_64 Linux では、KASLR のベースアドレスを確保した後、条件に合う関数テーブルやそれを含むオブジェクトを上書きする短い経路を選べる
  • 書き込み可能なデータ領域の inet6_protos[IPPROTO_UDP] 周辺は、必要な制約を自然に満たしている
    • inet6_protos[16] == NULL は偽の wait_lock の未ロック状態になる
    • inet6_protos[17] == &udpv6_protocol は実際に上書きする対象である
    • inet6_protos[18] == NULL は偽の rb_leftmost になる
    • inet6_protos[19] == NULL は偽の owner になる
  • 書き込みが終わると、inet6_protos[IPPROTO_UDP] は CEA ページ内の偽の inet6_protocol を指す
  • CEA を再度スプレーして構造体を次のように構成する
    • handler: 最初のピボットガジェットとして指定する
    • err_handler: 使用しない
    • flags: INET6_PROTO_NOPOLICY | INET6_PROTO_FINAL に設定する
  • ::1connect した後、データを書き込む IPv6 UDP ループバックパケット を送ると、カーネルが偽の handler を呼び出し、プログラムカウンタを制御できる

短いピボットと DirtyMode による権限昇格

  • Google kernelCTF の lts-6.12.80 対象では、適切な単一スタックピボットガジェットが見つからなかったため、追加の load/call で CEA アドレスを rbp に入れた後、mov rsp, rbp; pop rbp; ret でピボットする
  • ret2usr/proc/%P/fd/x の全体上書きは、約 10 個のガジェット qword が必要で、限られた CEA 空間に比べて大きすぎる
  • 最終段階では、一度の書き込みで権限ビットを変え、残りの処理をユーザー空間で行う DirtyMode を使う
  • 書き込み対象はカーネルデータの coredump_sysctls[1].mode、つまり core_pattern sysctl のアクセスモードである
  • カーネルイメージと同じ KASLR スライドを共有するためアドレスを計算でき、書き込みビットである下から 2 番目のビットが設定された値であれば十分である
  • 短い pop reg; mov [reg], reg; ret チェーンで mode 値を変更し、msleep で奪取したスレッドを安全に停止させる
  • /proc/sys/kernel/core_pattern を全ユーザーが書き込めるようになると、非特権プロセスが |/proc/%P/fd/666 %P を書き込み、helper をクラッシュさせることで、カーネルに攻撃者のバイナリを root 権限で実行させられる
  • 初期の rb-tree 書き込みは配置制約のため coredump_sysctls[1].mode に直接到達できず、mode 変更は短い ROP 段階で行う

全体のエクスプロイトの流れと結果

  • 攻撃は次の順序で進む
    1. prefetch でカーネルイメージのスライドと physmap のベースアドレスを漏えいさせる
    2. GhostLock で waiter の pi_blocked_on にダングリング rt_mutex_waiter を残す
    3. PR_SET_MM_MAP で同じカーネルスタックフレームを再利用し、偽の waiter を作る
    4. rtmutex rb-tree の削除を利用して inet6_protos[IPPROTO_UDP] に CEA ポインタを書き込む
    5. CEA に偽の inet6_protocol、ピボットスロット、ROP スタックを配置する
    6. IPv6 UDP ループバックパケットで上書きした handler を呼び出す
    7. DirtyMode で core_pattern の mode ビットを変更し、ユーザー空間で権限昇格を完了する
  • kernelCTF のリモート環境では、CEA と DirtyMode を組み合わせた経路で約 5 秒でフラグ を取得した
  • 全体のエクスプロイトは CyberMeowfia プロジェクト で公開されている
  • Android では、スタックフレーム再利用と ASLR・CFI 回避の方法が異なり、別の続編記事で扱う予定である

代替経路と緩和策

  • より大きな ROP 空間

    • NPerm ベースのメモリは、制御フローを奪取した後に大きな偽スタックとして使える
    • Lukas Maar の heap-KASLR 漏えい のように、より重い経路も可能だが、段階が増えるため実行時間が長くなる
    • kernelCTF では最も短く信頼性の高いチェーンが有利なため、CEA と DirtyMode の組み合わせを使う
  • カーネルパッチ

    • 最終パッチでは current の代わりに waiter->task を基準に pi_lock を取得し、pi_blocked_on を消去する
    • remove_waiter()waiter_task = waiter->task を保存した後、次の順序で処理する
      1. waiter_task->pi_lock をロックする
      2. waiter を rtmutex キューから削除する
      3. waiter_task->pi_blocked_on = NULL に設定する
      4. 後続の rt_mutex_adjust_prio_chain() にも current ではなく waiter_task を渡す
    • 研究者が v1 以前に送った別の修正案では、呼び出し側が所有タスクを明示的に渡す構成になっていた
      • 自分自身がブロックされる経路では current を渡す
      • プロキシロールバックではプロキシ対象の task を渡す
      • pi_blocked_on が依然としてその waiter を指しているときだけ消去し、タスクの pi_lock で保護する
  • RANDOMIZE_KSTACK_OFFSET

    • エクスプロイトは、解放された waiter フレームと後続の user_auxv フレームが決定的に重なることに依存している
    • RANDOMIZE_KSTACK_OFFSET を有効にすると、スタックオフセットが変わり、この段階は約 1/32 の確率の 5 ビット推測 に変わる
    • 提出された 2 つの一般対象ではこの設定はデフォルトで無効で、緩和対象では有効だったため、このエクスプロイト経路は使わない
  • STATIC_USERMODE_HELPER

    • STATIC_USERMODE_HELPER はこの特定の DirtyMode 経路を遮断する
    • ただし、アクセス権限を ctl_table::mode で制御し、テーブルが予測可能な書き込み可能カーネルデータにある他の /proc/sys 設定にも、同じ方式を一般化できる

公開日程

  • 2026年4月18日: 脆弱性と草案パッチを security@kernel.org に送付
  • 2026年4月20日: 別のパッチで脆弱性が修正される
  • 2026年5月4日: 修正案 v1 がバックポートされる
  • 2026年6月30日: Google が kernelCTF 提出を確認
  • 2026年7月7日: 技術分析を公開
  • VEGA が発見した脆弱性には標準の 90+30日公開ポリシー が適用される

1件のコメント

 
GN⁺ 1 일 전
Hacker News の意見
  • Android 9・13・16 と、それぞれ異なる Firefox 150 未満のバージョンを使う端末 3 台でテストしたところ、2 台はブートループに陥ってリカバリモードに入る必要があり、残り 1 台は電源が落ちた。デモは対応する Pixel 端末の壁紙を変更するもので、テストページは IonStack で確認できる
    個人端末でブログや任意のサイトを見るときは、メインブラウザとは別に Chromite のような Chromium 系ブラウザを入れ、フラグで JavaScript と、攻撃されがちなハードウェアアクセラレーション動画デコーダーを無効にし、壊れるサイトにはリーダーモードを使うほうが安全。あるいは専用タブレットを用意する方法もある

    • 現在は Pixel 10 でのみテストしているが、ほかの端末をサポートしようとする PR がいくつか上がっており、https://github.com/NebuSec/CyberMeowfia で確認できる
    • カーネルエクスプロイトを別の端末へ移植してみると、コンパイラがカーネルビルドごとに スタックフレームを配置する方法に非常に敏感だった。特定のビルドに合うスタンプ方式とオフセットを見つけてしまえば、かなり安定して動作する
    • リスクを承知で Samsung S26 Ultra で実行しており、adb をインストールして確認したうえで全結果を明らかにする予定
      テストページにアクセスすると Firefox タブに出力が表示され、概念実証コードが実行されたように見えたが、その後スマートフォンが固まり、すべての入力を受け付けなくなった。再起動だけは動作したが、カーネルが停止しているように見える状況で、どうして再起動イベントには応答できるのか気になる。画面は実行結果の一部を表示したまま点灯し、その後スクリーンセーバーが作動した
    • まだ root 化できない Android 端末を root 化するために活用できるならすごいが、可能な方法が気になる
    • Firefox の脆弱性は IonMonkey JIT コンパイラの型の取り違えである CVE-2026-10702 のように見える: https://www.sentinelone.com/vulnerability-database/cve-2026-10702/
  • エクスプロイトを発見しただけでなく、copyfail と違って誰でもすぐ使える ゼロデイのローカル権限昇格スクリプトを公開しなかったセキュリティ研究者たちに大きな賛辞を送りたい
    Rocky Linux 9 で数時間にわたりローカル権限昇格(LPE)を試したが、幸い成功しなかった。時間に余裕があるか、非常に腕が立つのでなければ、企業向けディストリビューションで実際の攻撃に利用するのは難しそうだ

  • 一般にブートローダーを解除できないスマートフォンでも、この脆弱性で ブートローダーのロック解除が可能なのか気になる。可能なら Android エコシステムで起きた最高の出来事の一つになり得る

  • タイトルにローカル権限昇格を意味する LPE を入れておけば、ほとんどの人が安心して週末に戻れたのではないかと思う

    • それほど安心できる話ではない。通常、ローカル権限エクスプロイトは一般ユーザー権限から root 権限へ昇格する攻撃を指し、一般権限のアプリでもすでに大きな被害を与えられるため、たいていはそこまで大きく心配しない
      しかし今回の攻撃は、Firefox の隔離されたブラウザプロセスのような 強力にサンドボックス化されたプロセス内からでも発動できる。攻撃者は JavaScript の脆弱性で隔離サンドボックス内にローカルコードを実行し、その後この脆弱性でカーネルモードまで昇格する二段階攻撃をつなげればよいので、Firefox と Linux カーネルの両方を更新する必要がある
    • 上位コメントの攻撃は JavaScript から直接 root 権限を得るように見えるが、実際には 別々のエクスプロイト 2 つを連鎖させている
    • コンテナ脱出が可能なら、依然として多くの人に影響し得るのではないかと思う
    • Firefox/IonMonkey の型の取り違え脆弱性もあわせて発見されているため、任意の Web サイトにアクセスするだけでも非常に素早く 端末が乗っ取られる可能性がある
    • 最近はこういう状況に使うために蓄えられたゼロデイが数百個はありそうだ。SSH から Node.js まで数週間ごとに新しい問題が出てくるので、すべての通信を WireGuard の背後に置くくらいでなければ、事実上すべてをリモート脆弱性のように扱うべき状況だ
  • 「Google が kernelCTF の報奨金として 92,337 ドルを支払った」というくだりから俄然興味を引かれた

    • 影響範囲を考えると少ない金額に見える。企業はリモートエクスプロイトにだけ大金を払うのだろうか
  • Android アプリが NDK でネイティブコードを実行して root 権限を得られるという意味なのか、SELinux が防御に役立つのか気になる

    • 非フラッグシップのスマートフォンはカーネルを含むアップデートを受けることがまれなので、実際に可能である確率は高そうだ
      古いカーネルにもパッチをバックポートすることはできるが、スマートフォンのアップデート変更履歴で CVE を明記することはまれなため、脆弱性検査ツールが事実上唯一の確認手段になる。Play Store や外部から入手したアプリが侵害されていれば即座に root 権限を得られるので、インストール時に信頼と監査を確認する原則は依然として重要だ
      今後、すべての Google Play Integrity レベルにこの検査が追加され、パッチ未適用のスマートフォンには多くのアプリをインストールできなくなる可能性がある。任意のサイトや広告を避けにくいブラウザでは、サンドボックス脱出がアプリ分離まで迂回するため、より深刻であり、iOS の JailbreakMe に似ている
    • カーネル自体が侵害されると SELinux は防御できない。Android サンドボックスや Docker のようなコンテナ技術もこのエクスプロイトを防げず、現実的な隔離手段は完全仮想化だけだ。KVM を使うなら、先週公開された CVE-2026-53359 のパッチがあらゆる場所に配布済みであることが前提になる
      過去 15 年に出た Linux でネイティブコードを実行できるアプリなら、端末にカーネルアップデートが届くまで root 権限を得られる
  • GhostLock が Linux 2.6.39 に入り、Linux 7.1 でようやく修正されたという事実は衝撃的だ

  • コメントを前日にすでに読んだ気がするのに、投稿時刻はすべて 10 時間以内と表示されていて、HN の時刻表示がおかしいのか気になる

    • HN の 再掲載(re-up)システムによる現象である可能性が高い。この投稿を再び上げる際に、既存コメントのタイムスタンプが相対時刻として再計算された。関連する内容は https://hn.algolia.com/?dateRange=all&page=0&prefix=true&query=by%3Adang%20timestamps%20re-up&sort=byDate&type=comment で見られる
      毎日確認している「underwater」リスト、つまり多くの賛成票を得たものの何らかの理由でトップページに載らなかった記事の一覧で、この投稿が最上位だったので再掲載した。奇妙に見えるが、まだこれより混乱の少ない代替案は出ていない
    • 似た記事を一つにまとめる際に、コメントまで統合することがある