kubo39's blog

ただの雑記です。

外部からの信頼できない入力がある場合ctRegexを使うのはやめよう

俺も気がついてなかった。。

github.com

D言語はコンパイル時に正規表現オブジェクトを構築する ctRegex という関数があるのだが、これはバックトラック法の実装しか選択できない。そのため、マッチングの際に入力される文字数に対して線形時間である計算量保証が成り立たない。

普段実行時の regex を使う場合は後方参照(\1みたいな表現)が入力文字列の中にあるかどうかでThompshon NFAを使うかバックトラック法を使うかを自動判別して適用されるが、コンパイル時生成の場合は上で述べたようにルールが変わっている。

バックトラック法だとReDoSのような攻撃手法が成り立ってしまうので、タイトルにあるように外部からの信頼できない入力がある場合は使うのを避けるべきである。

GPUで描画するモダンなフォントレンダラを自作したはなし

kubo39/toy-font-renderer

まえおき

ちょっと前の話なんですが、fadisさんの低レイヤーからはじめるGUIという発表をみてからぼんやりGPUでフォント描画するやつやりてえなーと思っていました。かっこいいので。

でまあちょいちょい調べてたりしてたんですが、けっこういろんな手法があるんですよね。

  • SDF: グリフアウトラインからの距離をテクスチャにのせてfragment shaderで計算する。
  • MSDF: SDFの改良的な。RGB3チャンネル(Mutli-channel)を使ってSDFを符号化。
  • ジオメトリメッシュ: 上の発表にあった、事前に三角形分割しておいて頂点データとしてGPUに送る。
  • Loop-Blinn: fragment shaderでベジエ曲線の内外判定を行う。
  • Slug: ベジエ曲線をバンド構造(グリフを水平・垂直の両方向にバンド分割し、各バンドにそのバンドを横断するベジエ曲線の参照リストを持たせる)で整理し、GPUフラグメントシェーダが各ピクセルで曲線とのワインディングを直接計算してサブピクセル精度のカバレッジを求める。
  • Vello: 手法の名前ではなく実装。Compute Shaderでベジェ曲線のラスタライズをやる。

まあ他にもあるんだと思います。

余談ですが、こういうアルゴリズムがいろいろある中で(SDFとかは2007年の発表)、もちろんゲーム業界なんかでは使われているんでしょうが、あまり一般のソフトウェアで使われていなかったりする印象があります。 GhosttyがGPU使っててすごいみたいな発表もみましたが、ラスタライズそのものはCPUベース(FreeTypeかな)で、複数のグリフをShelf packingしてひとつのテクスチャにしたものをGPUに転送してそれをdrawしているだけ、みたいな感じでした。 使っているコンポーネントの違いはあれどwezTermとか、もっというとSkia(Chrome)やWebRender(Firefox)なんかも同じ仕組みですね。

Slugアルゴリズム

今回採用したのはSlugアルゴリズムです。

というか実装の直接のきっかけになったのはHarfBuzzの作者が自作レンダラをSlugに置き換えたという話を知ったからなんですね。 それで芋づる式にSlugがほんのつい最近Public Domainになったなどを知って、タイミングいいやん!となったかんじです。 SlugはHLSLの参照実装もあるので機械的なGLSLポートも容易だった、というのもあります。まあ置き換えはAIにやらせましたが。。

Slugいいなと思っているのは(ほかのGPUベースのフォント描画アルゴリズムにもいえることかもしれませんが)ピクセルとベジェ曲線のワインディング・内外判定を行う際に実質的にアンチエイリアスのような処理が入っていることですね。

リファレンス実装とちょっと変えている点としては、テクスチャにのっけるのではなくVulkanのStorage Bufferを利用している点です。このほうが実装シンプルになるのでは?と思いつき、実際にちゃんと動いているっぽいのでまあたぶん問題ないでしょう。

HarfBuzz

当初はhairetsuという純D言語製のフォントパース・テキストシェーピング用ライブラリを使うつもりだったんですが、まだまだ成熟しておらずたとえばCFFアウトラインを取得できないことやそもそも最新のコンパイラでビルドが通らない(これについてはおそらくコンパイラのバグ)といった諸問題があり、最終的には定番のHarfBuzzを利用することにしました。

HarfBuzzはかなり使いやすいのでいいですね。テキストシェーピングというとグリフの配置だったりカーニングだったりまあいろいろなことをやる必要があるのですが、単一のAPI呼び出しの裏にすべて隠蔽されていて細かいところを意識する必要がなかったです。 また、これはあくまでミニマムなtoy実装という面もあるんですが、FreeTypeに依存する必要がなかったのもよかったです。

おわりに

あくまでtoy実装ですが学ぶことは多かったですね。 めんどうなコーディングはClaudeクンにやってもらう部分も多かったですが、こういう実装になるには細かい指示を出し続ける必要があったなというのも学びではありました。

あとぜんぜん関係ないですが、今フォント関連はRust化の波がアツいんですよね。 GoogleがフォントのコンポネントのRust化を推しまくっていたり(てかChromeはフォントパースはもうRustにしてますね)、HarfBuzzのRustポートが段階的に進んでいたり(C++を置き換えるところまでいくのは相当かかりそうですが)。

気が向いたらこのへんもまとめてみたいと思います。それでは。

RLP encode 実装メモ

d-eth/dethのrlpEncodeの実装がだいぶ厳しい。

理由としては主に

  • 引数にbytes(ubyteのtype alias)しかとれない
    • 主要な型をrlpのbytes表現にシリアライズしてからrlpEncodeしないといけない
  • パフォーマンスに気を使っていない実装になっている
    • .dupなどコピーしまくり
    • 数値のコンパクト表現を実装する際も毎回メモリを確保

なのでこのあたりを置き換えるようなものを作ろうとしている。まだencodeしか実装していないがいちおうdecodeも作る予定。

kubo39/rlpでは

  • 型ごとにencodeとencodeLengthを実装
  • 内部的な実装ではバッファを外から渡す方式にしてそこにencodeしたものを追加していく
  • 数値のコンパクト表現の実装の細かい最適化
    • llvm_ctlz intrinsicの利用/ヒープ領域の割り当てを避けるなど

といったあたりを意識しながら作っている。

go-ethereumのrlp実装などもみたが、主に参考にしているのはalloy-rs/rlpAPIなども似せている。

バイト列の扱い

ubyteを渡す際にRLP表現においてこれがたんなるバイト列なのかubyte型数値のリストなのかを区別する必要がある。 どういう方式でencodeしたいかはユーザの意思しだいであるが、ubyte型のみでは区別ができないので多少工夫がいる。

kubo39/rlpではubyteをencodeする場合のみテンプレート引数としてasListを渡す方式にした。

void encode(bool asList = false, T)(T[] value, ref ubyte[] buffer) nothrow pure @safe
    if (is(Unqual!T == ubyte))
{
    static if (asList)
    {
        rlpListHeader(value).encodeHeader(buffer);
        foreach (elem; value)
            elem.encode(buffer);
    }
    else
    {
        if (value.length != 1 || value[0] >= rlp.EMPTY_STRING_CODE)
        {
            Header h = { isList: false, payloadLength: value.length };
            h.encodeHeader(buffer);
        }
        buffer ~= value;
    }
}

Alpine Linuxで小さなD言語の静的バイナリを作りたい

こちらはD言語 Advent Calendar 2025 シリーズ2の16日目の記事として書いてます。

qiita.com

ちょうど https://github.com/mattn/small-build-test というプロジェクトをみかけて。

docker imageで生成物をデプロイ・配布することは今ではもう当たり前のことになってきており、とりわけ本番環境にimageを頻繁にデプロイするような使い方をしているとストレージ・通信のコストの面からimageのサイズは極力小さくしておきたいという要求がある。

D言語はバイナリを生成するコンパイラを提供している言語でありPythonのようにランタイムを同梱して配布する必要がないので、こういったコスト観点では有利かもしれない。せっかくなのでどのくらいまでいけるかみてみよう。

レギュレーション用?のコードはなんの変哲もないHello, World!。 ただし標準ライブラリstd.stdioを使っていることに意味がある。

import std.stdio;

void main()
{
    writeln("Hello, World!");
}

最終的にはこのようなDockerfileになった。

# syntax=docker/dockerfile:1
FROM alpine:latest AS builder
RUN apk add --no-cache ldc gcc musl-dev
WORKDIR /app
COPY main.d .
RUN ldc2 -Oz --release --boundscheck=off --platformlib= --flto=full --static -L-Wl,--strip-all -of=/app/hello main.d

FROM scratch
COPY --from=builder /app/hello /hello
CMD [ "/hello" ]

まずはちゃんと動くことを確認する。

$ docker build -t hello-d . -q
sha256:b563982aa847a4888c56220722093228b1bb706de92218a9ef111f548a50d46c
$ docker run --rm hello-d:latest
Hello, World!

imageの大きさをみてみると、ただのHello, World!にしてはそこそこ大きい(もちろんPythonなんかに比べると格段に小さくできるのだが)

$ docker image ls --format=table hello-d
REPOSITORY   TAG       IMAGE ID       CREATED      SIZE
hello-d      latest    b563982aa847   2 days ago   4.75MB

内訳をみたい。

docker create --name tmp hello-d
docker cp tmp:/hello .
docker rm tmp

静的なバイナリにはなっている。

$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, BuildID[sha1]=eb5b088c42d206c904c7729645709e99d03b2d15, stripped
$ ldd hello
        statically linked

size -Aでみた結果。

$ size -A hello
hello  :
section                 size      addr
.note.gnu.build-id        36       680
.dynsym               328104       720
.gnu.hash             101132    328824
.dynstr              1181543    429956
.rela.dyn             224736   1611504
.gcc_except_table       8352   1836240
.rodata               460604   1844608
.eh_frame_hdr          89188   2305212
.eh_frame             405044   2394400
.text                1778941   2803584
.init                      3   4582525
.fini                      3   4582528
.tdata                  2800   4586640
.tbss                    888   4589440
.fini_array               24   4589440
.init_array               64   4589464
.data.rel.ro           58728   4589536
.dynamic                 304   4648264
.got                     144   4648568
.got.plt                  24   4648712
.relro_padding           224   4648736
.tm_clone_table            0   4652832
.data                 108824   4652832
__minfo                 1224   4761656
.bss                    7824   4762880
.comment                  95         0
Total                4758853

.dynstrとかめっちゃでかい。libdruntimeとかlibphobos2をpieでビルドするとシンボル名の情報がここにのっかってくるためだ。stripだとここは消せない。

以下の出力をみると雰囲気が掴めるだろうか。

$ readelf -p .dynstr hello | grep _D3std | head -10 | ddemangle
  [    41]  @safe void std.stdio.writeln!(immutable(char)[]).writeln(immutable(char)[])
  [   27c]  @safe noreturn std.exception.bailOut!(std.exception.ErrnoException).bailOut(immutable(char)[], ulong, scope const(char)[])
  [   2c3]  @safe void std.stdio.File.LockingTextWriter.put!(immutable(char)[]).put(scope immutable(char)[])
  [   2ff]  @safe void std.stdio.File.LockingTextWriter.put!(char).put(scope char)
  [   338]  nothrow @nogc @trusted ulong std.stdio.trustedFwrite!(char).trustedFwrite(shared(core.stdc.stdio._IO_FILE)*, const(char[]))
  [   381]  @safe int std.exception.enforce!(std.exception.ErrnoException).enforce!(int).enforce(int, lazy const(char)[], immutable(char)[], ulong)
  [   3d1]  @safe void std.stdio.File.LockingTextWriter.put!(immutable(char)).put(scope immutable(char))
  [   40c]  pure @safe uint std.utf.stride!(char[]).stride(char[])
  [   430]  pure @safe dchar std.utf.decodeFront!(0, char[]).decodeFront(ref char[])
  [   4a6]  pure @safe ulong std.utf.encode!(0).encode(out dchar[1], dchar)

.dynsym も然り。

$ readelf --demangle=dlang --dyn-sym -W hello | head -10                                                                                                                     Symbol table '.dynsym' contains 13671 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000002ebc90    97 FUNC    WEAK   DEFAULT   10 std.encoding.EncoderInstance!(dchar).__mixin13.encode(dchar, ref dchar[])
     2: 0000000000365320    24 FUNC    WEAK   DEFAULT   10 std.range.SortedRange!(immutable(char)[][], "a < b", 0).SortedRange.__xtoHash(ref const(std.range.SortedRange!(immutable(char)[][], "a < b", 0).SortedRange))
     3: 000000000043b150    18 FUNC    WEAK   DEFAULT   10 rt.util.typeinfo.TypeInfoArrayGeneric!(long, ulong).TypeInfoArrayGeneric.toString() const
     4: 0000000000472b30    24 OBJECT  WEAK   DEFAULT   23 initializer for TypeInfo_APyS3std8datetime8timezone13PosixTimeZone6TTInfo
     5: 000000000037f760    39 FUNC    WEAK   DEFAULT   10 std.typecons.SafeRefCounted!(std.file.DirIteratorImpl, 0).SafeRefCounted.RefCountedStore.deallocateStore()
     6: 000000000038da10   159 FUNC    WEAK   DEFAULT   10 std.uni.PackedArrayViewImpl!(std.uni.BitPacked!(uint, 13uL).BitPacked, 16uL).PackedArrayViewImpl.__xopEquals(ref const(std.uni.PackedArrayViewImpl!(std.uni.BitPacked!(uint, 13uL).BitPacked, 16uL).PackedArrayViewImpl)) const

そのほかで言うと

  • eh_frame: druntime/phobosが例外を使ってる部分で切り離せない
  • .text,.rodata: テンプレート展開でコードサイズが大きくなっている/TypeInfoがけっこう大きい/モジュールコンストラクタ(static this()/static shared this())があるモジュールは消せないのでコード上使ってなくてもbundleされている

といったあたりで、apkで提供されているldcのstatic libraryを使っているとこれ以上小さくするのはなかなか難しいということがわかった。

前日のD言語Advent Calenderの記事にしたldc-build-runtimeを使って自前で標準ライブラリをビルドするアプローチをここでもあわせて紹介しておくが、まあ実用上ここまでやる必要はほとんどないだろう。

(追記: 2025-12-18)

relocationとかもうちょっとなんとなからんかなーと調べてみたところ、relative relocationを圧縮するRELRフォーマットというものがあり、実際にこれを試してみると小さくなった。

diff --git a/d/Dockerfile b/d/Dockerfile
index a01e6e7..4a8c551 100644
--- a/d/Dockerfile
+++ b/d/Dockerfile
@@ -3,7 +3,7 @@ FROM alpine:latest AS builder
 RUN apk add --no-cache ldc gcc musl-dev
 WORKDIR /app
 COPY main.d .
-RUN ldc2 -Oz --release --boundscheck=off --platformlib= --flto=full --static -L-Wl,--strip-all -of=/app/hello main.d
+RUN ldc2 -Oz --release --boundscheck=off --platformlib= --flto=full --static -L-Wl,-z,pack-relative-relocs -L-Wl,--strip-all -of=/app/hello main.d

 FROM scratch
 COPY --from=builder /app/hello /hello

上の結果と比較すると rela.dyn セクションが relr.dyn セクションに名前が変わっており、(バイナリ全体からみると微々たるものだが)セクションのサイズも大幅に削減されている。

$ size -A hello
section                 size      addr
.note.gnu.build-id        36       680
.dynsym               328104       720
.gnu.hash             101132    328824
.dynstr              1181543    429956
.relr.dyn               2672   1611504
.gcc_except_table       8352   1614176
.rodata               460604   1622528
.eh_frame_hdr          89188   2083132
.eh_frame             405044   2172320
.text                1778941   2581504
.init                      3   4360445
.fini                      3   4360448
.tdata                  2800   4364560
.tbss                    888   4367360
.fini_array               24   4367360
.init_array               64   4367384
.data.rel.ro           58728   4367456
.dynamic                 288   4426184
.got                     144   4426472
.got.plt                  24   4426616
.relro_padding          1136   4426640
.tm_clone_table            0   4430736
.data                 108824   4430752
__minfo                 1224   4539576
.bss                    7824   4540800
.comment                  95         0
Total                4537685

D言語製VulkanバインディングのEruptedを利用していてハマった話

この記事はグラフィックス全般 Advent Calendar 2025の3日目の記事です。

Vulkan向けで以下のようなVK_KHR_dynamic_rendering拡張を使ってdynamic renderingを行うコードを書いていたところ

        auto imageView = swapchain.getCurrentView();
        auto extent = swapchain.getExtent();

        // 補足: EruptedはsTypeをコード生成時によしなに設定してくれる
        VkRenderingAttachmentInfoKHR colorAttachment = {
            imageView: imageView,
            imageLayout: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
            loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR,
            storeOp: VK_ATTACHMENT_STORE_OP_STORE,
            clearValue: VkClearValue(
                VkClearColorValue([0.6f, 0.2f, 0.3f, 1.0f])
            ),
        };
        VkRenderingInfoKHR renderingInfo = {
            renderArea: VkRect2D(VkOffset2D(0, 0), extent),
            layerCount: 1,
            colorAttachmentCount: 1,
            pColorAttachments: &colorAttachment
        };

        vkCmdBeginRenderingKHR(commandBuffer.get(), &renderingInfo);
        vkCmdEndRenderingKHR(commandBuffer.get());

とある環境で実行してみるとcode -1073741819(Windows: Access Violation)のエラーになった。

$ dub run -q
Error Program exited with code -1073741819

vkCmdBeginRenderingKHRは0x0になっているために当然このような落ち方をするのだが、動的ロードを行っているとはいえなぜこのようになってしまうのかなかなか思い当たらない。 vulkaninfoでも拡張をサポートしているという情報は出ているし、loadDeviceLevelFunctionsの呼び忘れやVK_KHR_DYNAMIC_RENDERING_EXTENSION_NAMEの指定忘れをしたわけでもない。

調べてみたところ、原因は利用しているVulkanバインディングの実装の問題であることがわかった。

D言語製のVulkanバインディングであるEruptedはVulkan1.3でコアに追加された拡張に対してコアの関数へのaliasにしてしまっている

// VK_KHR_dynamic_rendering
alias CmdBeginRenderingKHR                                         = CmdBeginRendering;
alias CmdEndRenderingKHR                                           = CmdEndRendering;

型定義などはaliasにしても(ほぼほぼ)問題ないのだが、Vulkan1.3機能に対応していないドライバではVulkan1.3の機能を使わないようにして拡張を利用しようとしても暗黙にVulkan1.3の関数を参照しようとしてAccess Violationが起きてしまっていたわけだ。

原因がわかれば解決策はそれほど難しい話ではない。 自前で関数アドレスをロードしてしまえばよいだけである。

pfnCmdBeginRenderingKHR = cast(PFN_vkCmdBeginRenderingKHR)
    vkGetDeviceProcAddr(m_vkDevice, "vkCmdBeginRenderingKHR");

ひとまずの応急処置としてはこれでよいのだが、これはupstream側で対処すべき問題のようにみえる。

Erupted側で対応する場合を考えると、Eruptedがvk.ymlを読み込んで自動生成する処理の関数生成のエイリアスの分岐内での処理あたりを変更する必要がありそうだ。 ちなみにErputedが依存・利用しているPythonパッケージはVulkan-Headersで提供されているもので、コード生成の元となるvk.ymlもこのレポジトリに含まれる(そもそもVulkan-Headersはこれを元にCのヘッダファイルを生成したものを提供するためのもの)。 ここで提供されているgenerator.py(vk.ymlからソースを生成するためのgeneratorコード)を継承・利用する形でD言語のVulkanバインディングが生成されている。

Erupted自体がそれほどメンテナンスされていないこともあり結局ここまで踏み込んで対応すべきかどうかは今の時点でも決め切れていないが、ひとまずはこういった問題があることは認識できた。

swap/swapRangesのはなし

ふと、swapみたいな単純なコードでも標準ライブラリの実装を呼び出した方がいいよ、という例として

import core.int128 : Cent;
import std.algorithm.mutation : swap;

struct Huge
{
    Cent[128] data;
    alias this = data;
}

void f(ref Huge x, ref Huge y)
{
    swap(x, y);
}

みたいなコードを使おうと思ったんだけど、ぜんぜんそんなことはなかった。

$ ldc2 -O -c swap.d
$ objdump -dr --demangle=dlang -Mintel swap.o
(...)
0000000000000000 <swap.f(ref swap.Huge, ref swap.Huge)>:
   0:   41 57                   push   r15
   2:   41 56                   push   r14
   4:   53                      push   rbx
   5:   48 81 ec 00 08 00 00    sub    rsp,0x800
   c:   48 89 f3                mov    rbx,rsi
   f:   49 89 fe                mov    r14,rdi
  12:   49 89 e7                mov    r15,rsp
  15:   ba 00 08 00 00          mov    edx,0x800
  1a:   4c 89 ff                mov    rdi,r15
  1d:   4c 89 f6                mov    rsi,r14
  20:   e8 00 00 00 00          call   25 <swap.f(ref swap.Huge, ref swap.Huge)+0x25>
            21: R_X86_64_PLT32  memcpy-0x4
  25:   ba 00 08 00 00          mov    edx,0x800
  2a:   4c 89 f7                mov    rdi,r14
  2d:   48 89 de                mov    rsi,rbx
  30:   e8 00 00 00 00          call   35 <swap.f(ref swap.Huge, ref swap.Huge)+0x35>
            31: R_X86_64_PLT32  memcpy-0x4
  35:   ba 00 08 00 00          mov    edx,0x800
  3a:   48 89 df                mov    rdi,rbx
  3d:   4c 89 fe                mov    rsi,r15
  40:   e8 00 00 00 00          call   45 <swap.f(ref swap.Huge, ref swap.Huge)+0x45>
            41: R_X86_64_PLT32  memcpy-0x4
  45:   48 81 c4 00 08 00 00    add    rsp,0x800
  4c:   5b                      pop    rbx
  4d:   41 5e                   pop    r14
  4f:   41 5f                   pop    r15
  51:   c3                      ret    
(...)
0000000000000000 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)>:
   0:   41 57                   push   r15
   2:   41 56                   push   r14
   4:   53                      push   rbx
   5:   48 81 ec 00 08 00 00    sub    rsp,0x800
   c:   48 89 f3                mov    rbx,rsi
   f:   49 89 fe                mov    r14,rdi
  12:   49 89 e7                mov    r15,rsp
  15:   ba 00 08 00 00          mov    edx,0x800
  1a:   4c 89 ff                mov    rdi,r15
  1d:   4c 89 f6                mov    rsi,r14
  20:   e8 00 00 00 00          call   25 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x25>
            21: R_X86_64_PLT32  memcpy-0x4
  25:   ba 00 08 00 00          mov    edx,0x800
  2a:   4c 89 f7                mov    rdi,r14
  2d:   48 89 de                mov    rsi,rbx
  30:   e8 00 00 00 00          call   35 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x35>
            31: R_X86_64_PLT32  memcpy-0x4
  35:   ba 00 08 00 00          mov    edx,0x800
  3a:   48 89 df                mov    rdi,rbx
  3d:   4c 89 fe                mov    rsi,r15
  40:   e8 00 00 00 00          call   45 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x45>
            41: R_X86_64_PLT32  memcpy-0x4
  45:   48 81 c4 00 08 00 00    add    rsp,0x800
  4c:   5b                      pop    rbx
  4d:   41 5e                   pop    r14
  4f:   41 5f                   pop    r15
  51:   c3                      ret    

実装みてもそんな感じしないけど

void swap(T)(ref T lhs, ref T rhs)
if (is(typeof(lhs.proxySwap(rhs))))
{
    lhs.proxySwap(rhs);
}

/// ditto
void swap(T)(ref T lhs, ref T rhs) @trusted pure nothrow @nogc
if (isBlitAssignable!T && !is(typeof(lhs.proxySwap(rhs))))
{
    import std.traits : hasAliasing, hasElaborateAssign, isAssignable,
                        isStaticArray;
    static if (hasAliasing!T) if (!__ctfe)
    {
        import std.exception : doesPointTo;
        assert(!doesPointTo(lhs, lhs), "Swap: lhs internal pointer.");
        assert(!doesPointTo(rhs, rhs), "Swap: rhs internal pointer.");
        assert(!doesPointTo(lhs, rhs), "Swap: lhs points to rhs.");
        assert(!doesPointTo(rhs, lhs), "Swap: rhs points to lhs.");
    }

    static if (hasElaborateAssign!T || !isAssignable!T)
    {
        if (&lhs != &rhs)
        {
            static if (__traits(compiles, lhs.tupleof = rhs.tupleof))
            {
                if (__ctfe)
                {
                    // can't reinterpret cast
                    foreach (i, ref e; lhs.tupleof)
                        swap(e, rhs.tupleof[i]);
                    return;
                }
            }

            // For structs with non-trivial assignment, move memory directly
            ubyte[T.sizeof] t = void;
            auto a = (cast(ubyte*) &lhs)[0 .. T.sizeof];
            auto b = (cast(ubyte*) &rhs)[0 .. T.sizeof];
            t[] = a[];
            a[] = b[];
            b[] = t[];
        }
    }
    else
    {
        //Avoid assigning overlapping arrays. Dynamic arrays are fine, because
        //it's their ptr and length properties which get assigned rather
        //than their elements when assigning them, but static arrays are value
        //types and therefore all of their elements get copied as part of
        //assigning them, which would be assigning overlapping arrays if lhs
        //and rhs were the same array.
        static if (isStaticArray!T)
        {
            if (lhs.ptr == rhs.ptr)
                return;
        }

        // For non-elaborate-assign types, suffice to do the classic swap
        static if (__traits(hasCopyConstructor, T))
        {
            // don't invoke any elaborate constructors either
            T tmp = void;
            tmp = lhs;
        }
        else
            auto tmp = lhs;
        lhs = rhs;
        rhs = tmp;
    }
}

なんかうまく最適化してくれそうだからここにきてほしいんだよな。

            // For structs with non-trivial assignment, move memory directly
            ubyte[T.sizeof] t = void;
            auto a = (cast(ubyte*) &lhs)[0 .. T.sizeof];
            auto b = (cast(ubyte*) &rhs)[0 .. T.sizeof];
            t[] = a[];
            a[] = b[];
            b[] = t[];

hasElaborateAssign!T || !isAssignable!Tを満たしてないのか。

import core.int128 : Cent;
import std.algorithm.mutation : swap;
import std.traits : isAssignable, hasElaborateAssign;

struct Huge
{
    Cent[128] data;
    alias this = data;
}

void f(ref Huge x, ref Huge y)
{
    pragma(msg, hasElaborateAssign!Huge);
    pragma(msg, !isAssignable!Huge);
    swap(x, y);
}

だめだった。

$ ldc2 -O -c swap.d
false
false

disable this(this)を足そう。

import core.int128 : Cent;
import std.algorithm.mutation : swap;
import std.traits : isAssignable, hasElaborateAssign;

struct Huge
{
    @disable this(this);
    Cent[128] data;
    alias this = data;
}

void f(ref Huge x, ref Huge y)
{
    pragma(msg, hasElaborateAssign!Huge);
    pragma(msg, !isAssignable!Huge);
    swap(x, y);
}
$ ldc2 -O -c swap.d
true
true

これでいけるかな。

$ objdump -dr --demangle=dlang -Mintel swap.o
(...)
0000000000000000 <swap.f(ref swap.Huge, ref swap.Huge)>:
   0:   48 39 f7                cmp    rdi,rsi
   3:   74 5c                   je     61 <swap.f(ref swap.Huge, ref swap.Huge)+0x61>
   5:   41 57                   push   r15
   7:   41 56                   push   r14
   9:   53                      push   rbx
   a:   48 81 ec 00 08 00 00    sub    rsp,0x800
  11:   48 89 f3                mov    rbx,rsi
  14:   49 89 fe                mov    r14,rdi
  17:   49 89 e7                mov    r15,rsp
  1a:   ba 00 08 00 00          mov    edx,0x800
  1f:   4c 89 ff                mov    rdi,r15
  22:   4c 89 f6                mov    rsi,r14
  25:   e8 00 00 00 00          call   2a <swap.f(ref swap.Huge, ref swap.Huge)+0x2a>
            26: R_X86_64_PLT32  memcpy-0x4
  2a:   be 00 08 00 00          mov    esi,0x800
  2f:   b9 00 08 00 00          mov    ecx,0x800
  34:   41 b8 01 00 00 00       mov    r8d,0x1
  3a:   4c 89 f7                mov    rdi,r14
  3d:   48 89 da                mov    rdx,rbx
  40:   e8 00 00 00 00          call   45 <swap.f(ref swap.Huge, ref swap.Huge)+0x45>
            41: R_X86_64_PLT32  _d_array_slice_copy-0x4
  45:   ba 00 08 00 00          mov    edx,0x800
  4a:   48 89 df                mov    rdi,rbx
  4d:   4c 89 fe                mov    rsi,r15
  50:   e8 00 00 00 00          call   55 <swap.f(ref swap.Huge, ref swap.Huge)+0x55>
            51: R_X86_64_PLT32  memcpy-0x4
  55:   48 81 c4 00 08 00 00    add    rsp,0x800
  5c:   5b                      pop    rbx
  5d:   41 5e                   pop    r14
  5f:   41 5f                   pop    r15
  61:   c3                      ret    
(...)
0000000000000000 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)>:
   0:   48 39 f7                cmp    rdi,rsi
   3:   74 5c                   je     61 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x61>
   5:   41 57                   push   r15
   7:   41 56                   push   r14
   9:   53                      push   rbx
   a:   48 81 ec 00 08 00 00    sub    rsp,0x800
  11:   48 89 f3                mov    rbx,rsi
  14:   49 89 fe                mov    r14,rdi
  17:   49 89 e7                mov    r15,rsp
  1a:   ba 00 08 00 00          mov    edx,0x800
  1f:   4c 89 ff                mov    rdi,r15
  22:   4c 89 f6                mov    rsi,r14
  25:   e8 00 00 00 00          call   2a <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x2a>
            26: R_X86_64_PLT32  memcpy-0x4
  2a:   be 00 08 00 00          mov    esi,0x800
  2f:   b9 00 08 00 00          mov    ecx,0x800
  34:   41 b8 01 00 00 00       mov    r8d,0x1
  3a:   4c 89 f7                mov    rdi,r14
  3d:   48 89 da                mov    rdx,rbx
  40:   e8 00 00 00 00          call   45 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x45>
            41: R_X86_64_PLT32  _d_array_slice_copy-0x4
  45:   ba 00 08 00 00          mov    edx,0x800
  4a:   48 89 df                mov    rdi,rbx
  4d:   4c 89 fe                mov    rsi,r15
  50:   e8 00 00 00 00          call   55 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x55>
            51: R_X86_64_PLT32  memcpy-0x4
  55:   48 81 c4 00 08 00 00    add    rsp,0x800
  5c:   5b                      pop    rbx
  5d:   41 5e                   pop    r14
  5f:   41 5f                   pop    r15
  61:   c3                      ret    
(...)
0000000000000000 <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])>:
   0:   48 39 f7                cmp    rdi,rsi
   3:   74 51                   je     56 <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])+0x56>
   5:   41 57                   push   r15
   7:   41 56                   push   r14
   9:   53                      push   rbx
   a:   48 81 ec 00 08 00 00    sub    rsp,0x800
  11:   48 89 f3                mov    rbx,rsi
  14:   49 89 fe                mov    r14,rdi
  17:   49 89 e7                mov    r15,rsp
  1a:   ba 00 08 00 00          mov    edx,0x800
  1f:   4c 89 ff                mov    rdi,r15
  22:   4c 89 f6                mov    rsi,r14
  25:   e8 00 00 00 00          call   2a <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])+0x2a>
            26: R_X86_64_PLT32  memcpy-0x4
  2a:   ba 00 08 00 00          mov    edx,0x800
  2f:   4c 89 f7                mov    rdi,r14
  32:   48 89 de                mov    rsi,rbx
  35:   e8 00 00 00 00          call   3a <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])+0x3a>
            36: R_X86_64_PLT32  memcpy-0x4
  3a:   ba 00 08 00 00          mov    edx,0x800
  3f:   48 89 df                mov    rdi,rbx
  42:   4c 89 fe                mov    rsi,r15
  45:   e8 00 00 00 00          call   4a <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])+0x4a>
            46: R_X86_64_PLT32  memcpy-0x4
  4a:   48 81 c4 00 08 00 00    add    rsp,0x800
  51:   5b                      pop    rbx
  52:   41 5e                   pop    r14
  54:   41 5f                   pop    r15
  56:   c3                      ret    

ぜんぜんだめ。 (というか_d_array_slice_copyはmemcpyにloweringされてほしいのだがエイリアス情報がないのでだめか。@restrictをつけるとできそうだが。)

sliceのコピーがそもそもだめなのか。

export void myswap(ref Huge lhs, ref Huge rhs)
{
    ubyte[Huge.sizeof] t = void;
    auto a = (cast(ubyte*) &lhs)[0 .. Huge.sizeof];
    auto b = (cast(ubyte*) &rhs)[0 .. Huge.sizeof];
    t[] = a[];
    a[] = b[];
    b[] = t[];  
}

そうらしい。

$ ldc2 -O -c swap.d
$ objdump -dr --demangle=dlang -Mintel swap.o
(...)
0000000000000000 <swap.myswap(ref swap.Huge, ref swap.Huge)>:
   0:   41 57                   push   r15
   2:   41 56                   push   r14
   4:   53                      push   rbx
   5:   48 81 ec 00 08 00 00    sub    rsp,0x800
   c:   48 89 f3                mov    rbx,rsi
   f:   49 89 fe                mov    r14,rdi
  12:   49 89 e7                mov    r15,rsp
  15:   ba 00 08 00 00          mov    edx,0x800
  1a:   4c 89 ff                mov    rdi,r15
  1d:   4c 89 f6                mov    rsi,r14
  20:   e8 00 00 00 00          call   25 <swap.myswap(ref swap.Huge, ref swap.Huge)+0x25>
            21: R_X86_64_PLT32  memcpy-0x4
  25:   be 00 08 00 00          mov    esi,0x800
  2a:   b9 00 08 00 00          mov    ecx,0x800
  2f:   41 b8 01 00 00 00       mov    r8d,0x1
  35:   4c 89 f7                mov    rdi,r14
  38:   48 89 da                mov    rdx,rbx
  3b:   e8 00 00 00 00          call   40 <swap.myswap(ref swap.Huge, ref swap.Huge)+0x40>
            3c: R_X86_64_PLT32  _d_array_slice_copy-0x4
  40:   ba 00 08 00 00          mov    edx,0x800
  45:   48 89 df                mov    rdi,rbx
  48:   4c 89 fe                mov    rsi,r15
  4b:   e8 00 00 00 00          call   50 <swap.myswap(ref swap.Huge, ref swap.Huge)+0x50>
            4c: R_X86_64_PLT32  memcpy-0x4
  50:   48 81 c4 00 08 00 00    add    rsp,0x800
  57:   5b                      pop    rbx
  58:   41 5e                   pop    r14
  5a:   41 5f                   pop    r15
  5c:   c3                      ret    

と思ってみてみたらswapRangesってのがあるのか。

import core.int128 : Cent;
import std.algorithm.mutation : swapRanges;

struct Huge
{
    Cent[128] data;
    alias this = data;
}

void f(ref Huge x, ref Huge y)
{
    swapRanges(x[], y[]);
}

あ、これでいいですね。

0000000000000000 <swap.f(ref swap.Huge, ref swap.Huge)>:
swap.f(ref swap.Huge, ref swap.Huge):
   0:   31 c0                   xor    eax,eax
   2:   66 66 66 66 66 2e 0f    data16 data16 data16 data16 cs nop WORD PTR [rax+rax*1+0x0]
   9:   1f 84 00 00 00 00 00 
  10:   0f 10 04 07             movups xmm0,XMMWORD PTR [rdi+rax*1]
  14:   0f 29 44 24 e8          movaps XMMWORD PTR [rsp-0x18],xmm0
  19:   0f 10 04 06             movups xmm0,XMMWORD PTR [rsi+rax*1]
  1d:   0f 11 04 07             movups XMMWORD PTR [rdi+rax*1],xmm0
  21:   0f 28 44 24 e8          movaps xmm0,XMMWORD PTR [rsp-0x18]
  26:   0f 11 04 06             movups XMMWORD PTR [rsi+rax*1],xmm0
  2a:   0f 10 44 07 10          movups xmm0,XMMWORD PTR [rdi+rax*1+0x10]
  2f:   0f 29 44 24 e8          movaps XMMWORD PTR [rsp-0x18],xmm0
  34:   0f 10 44 06 10          movups xmm0,XMMWORD PTR [rsi+rax*1+0x10]
  39:   0f 11 44 07 10          movups XMMWORD PTR [rdi+rax*1+0x10],xmm0
  3e:   0f 28 44 24 e8          movaps xmm0,XMMWORD PTR [rsp-0x18]
  43:   0f 11 44 06 10          movups XMMWORD PTR [rsi+rax*1+0x10],xmm0
  48:   48 83 c0 20             add    rax,0x20
  4c:   48 3d 00 08 00 00       cmp    rax,0x800
  52:   75 bc                   jne    10 <swap.f(ref swap.Huge, ref swap.Huge)+0x10>
  54:   c3                      ret    

Disassembly of section .text._D3std9algorithm8mutation__T10swapRangesTAS4core6int1284CentTQuZQBkFNaNbNiNfQBjQBmZSQDe8typecons__T5TupleTQCnTQCrZQp:

0000000000000000 <std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[])>:
std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[]):
   0:   48 89 f8                mov    rax,rdi
   3:   48 85 f6                test   rsi,rsi
   6:   40 0f 94 c7             sete   dil
   a:   48 85 c9                test   rcx,rcx
   d:   41 0f 94 c1             sete   r9b
  11:   41 08 f9                or     r9b,dil
  14:   75 4d                   jne    63 <std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[])+0x63>
  16:   31 ff                   xor    edi,edi
  18:   0f 1f 84 00 00 00 00    nop    DWORD PTR [rax+rax*1+0x0]
  1f:   00 
  20:   0f 10 02                movups xmm0,XMMWORD PTR [rdx]
  23:   0f 29 44 24 e8          movaps XMMWORD PTR [rsp-0x18],xmm0
  28:   41 0f 10 00             movups xmm0,XMMWORD PTR [r8]
  2c:   0f 11 02                movups XMMWORD PTR [rdx],xmm0
  2f:   0f 28 44 24 e8          movaps xmm0,XMMWORD PTR [rsp-0x18]
  34:   41 0f 11 00             movups XMMWORD PTR [r8],xmm0
  38:   48 83 c2 10             add    rdx,0x10
  3c:   49 83 c0 10             add    r8,0x10
  40:   4c 8d 14 3e             lea    r10,[rsi+rdi*1]
  44:   4c 8d 4f ff             lea    r9,[rdi-0x1]
  48:   49 83 fa 01             cmp    r10,0x1
  4c:   74 0f                   je     5d <std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[])+0x5d>
  4e:   4c 8d 14 0f             lea    r10,[rdi+rcx*1]
  52:   49 ff ca                dec    r10
  55:   4c 89 cf                mov    rdi,r9
  58:   4d 85 d2                test   r10,r10
  5b:   75 c3                   jne    20 <std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[])+0x20>
  5d:   4c 01 c9                add    rcx,r9
  60:   4c 01 ce                add    rsi,r9
  63:   48 89 30                mov    QWORD PTR [rax],rsi
  66:   48 89 50 08             mov    QWORD PTR [rax+0x8],rdx
  6a:   48 89 48 10             mov    QWORD PTR [rax+0x10],rcx
  6e:   4c 89 40 18             mov    QWORD PTR [rax+0x18],r8
  72:   c3                      ret

どうもLLVM1バイトずつswapする処理をうまく最適化できるらしい。 swapRangesもこのような実装になっている。

Tuple!(InputRange1, InputRange2)
swapRanges(InputRange1, InputRange2)(InputRange1 r1, InputRange2 r2)
if (hasSwappableElements!InputRange1 && hasSwappableElements!InputRange2
    && is(ElementType!InputRange1 == ElementType!InputRange2))
{
    for (; !r1.empty && !r2.empty; r1.popFront(), r2.popFront())
    {
        swap(r1.front, r2.front);
    }
    return tuple(r1, r2);
}

いままでのswapのはなしはなんだったの?なんだったんですかね。。

Conclusion

  • 複数のelementがあるデータのswapがしたいときはswapRangesを使おう

Linuxサンドボックス環境の比較 2025年版

あらすじ

近年、supply-chain attackとかこわい。 特にCLIでnpmやらcodexやらclaude codeやらgeminiやら動かすのに裸んぼでは心もとないのではないか。

というわけでナウなLinuxサンドボックスを調べて比較してみる。

比較のやつ

firejail

老舗?のイメージ。 利用方法などはarch wikiがまとまっている。 わりと使いやすい、かつsuid+chroot+namespace+seccomp-bpfという必要なのがそろっている構成で、experimentalだがlandlockサポートも入っている。 じゃあfirejailでいいじゃん、という気もするのだが、そもそもfirejail使うでいいのか?というところが出発点でもある。

余談だがfirejailのコードはけっこう学びが多かった、このhackとかfirejailで知ったはず。

nsjail

officialなやつではないけどgoogleのやつ。 これもけっこう老舗感ある。 あまりfirejailと違わないような?出てきたときはnamespace対応だったり(ns-prefixはそこからきたんだったはず)があったかもしれないけど。 ebpfとかなんかがんばってる雰囲気。けどやっぱりあんまり違いはわからない。

systemd-nspawn

systemd-nspawnはデフォルトで存在しているのでそこは便利なのだが、決して使いやすいものではない。Nix(内部的にsystemd-nspawn使ってる)にするのがどうせならいいけど、これはこれで重い作業なんだよな。

そういえばfirejailは不十分なことがあってsystemd-nspawn(+systemd-run)にしてるってはなしあるけどよくわからんな。

まあでもプロセス隔離ならchroot+Linux namespace+seccomp-bpfでよくて、image buildやらpull/pushとかしたいとかでなければ要らん気もするな。

bubblewrap

flatpackで使われている(sandbox部分を切り出した?)setuidサンドボックス環境。 unshareのラッパーみたいなプリミティブなやつで、下で紹介するbubblejailのほうもあるんだけど、bubblewrap自体もbwrapコマンドでお手軽にサンドボックス環境を作ることはできる。 まあやっぱりfirejailと変わらない雰囲気。

これは直接本論とは関係ないのだが、OCamlのパッケージマネージャであるopamをLinuxで利用する場合にbubblewrapが必要となる(opamはsandbox環境でコマンドを実行することを強制しているのだ、賢い!)

bubblejail

bubblejailは上のbubblewrap(bwrap)の土台にしたもの。 READMEにfirejailとの違いがあって、

firejailの最大の問題点の1つは、誤ってサンドボックス化されていないアプリケーションを実行しても気付かない可能性があることです。 bubblejailは既存のホームディレクトリを透過的に上書きしようとする代わりに独立したホームディレクトリを作成します。 各インスタンスは独立したホームディレクトリを表します。通常、サンドボックス化されたアプリケーションはそれぞれ独自のホームディレクトリを持ちます。

とのこと。 あとまあbwrapは全部引数で渡す必要があるので設定ファイルで分離するとか。

crabjail

これもbubblewrap相当のcrablockというプログラムのラッパーになっている。 crablock自体bubblewrapのようにコマンドとして利用でき、crabjailはcrablockを呼び出す構造になっている。 特徴としてはcrablockも含めてRustで書かれている点とかか。

cage

内部実装にlandlockを使っている。 landlockはこんな話もあって、うーん不十分!wという気もする。

クロスプラットフォームなのが特徴といえばそうなのだが、とくに求めていないしな。 macOSの人はこれ使ったらいいと思いました。seatbeltよく知らないけど。

landrun

だいたいcageと同じと思われる。

結論

人類車輪の再発明好きすぎでは? bubblejailがよさそうな雰囲気あるけどbubblewrapのラッパーを自作するのがいい気がしてきたのでやる(はいまた車輪の再発明を繰り返す)(高速な伏線回収)