Upgrade to Pro — share decks privately, control downloads, hide ads and more …

golang binary hacks

golang binary hacks

Alpine Linux などで使われる musl libc では動かないプログラムの問題を Go 言語で簡単に解決する方法についてお話します。

YAEGASHI Takeshi

June 18, 2019
Tweet

More Decks by YAEGASHI Takeshi

Other Decks in Technology

Transcript

  1. golang binary hacks
    2019-06-18 Takeshi Yaegashi
    golang.tokyo #25

    View Slide

  2. 自己紹介
    八重樫 剛史 Takeshi Yaegashi
    ● 株式会社バンダイナムコスタジオ所属
    ● Linux・Unix・OSS・低レベルなことが好きなエンジニア
    ● もちろん Go 言語も大好きです!
    ● Go を使ったお仕事
    ○ IoT 案件の Raspberry Pi 制御プログラム
    ○ スマホゲームアプリのサーバ

    View Slide

  3. https://l0w.dev
    最近レベルの低いドメインを取得したのでホームページとブログを作り直した
    Hugo で生成したサイトを gitlab.com の GitLab Pages でホスト
    下図は l0w.dev と low.dev の価値の比較

    View Slide

  4. Hugo
    Hugo は Go 言語で書かれた静的サイトジェネレータ
    ● https://gohugo.io
    ● Hugo には 2 種類あり、それぞれのバイナリが配布されている
    ○ Hugo
    ■ 基本機能のみの Hugo
    ○ Hugo Extended
    ■ SASS/SCSS のアセット処理もできる拡張版 Hugo
    ■ C++ で書かれたライブラリ libsass を組み込んでいる (cgo)

    View Slide

  5. Hugo Extended on Docker
    先日 gitlab.com/pages/hugo に対してコントリビュートした
    ● https://gitlab.com/pages/hugo/merge_requests/83
    ● プロジェクト Docker レジストリに hugo_extended イメージを追加
    ○ registry.gitlab.com/pages/hugo/hugo
    ○ registry.gitlab.com/pages/hugo/hugo_extended
    ● GitHub の Release からビルドされたバイナリをダウンロードして
    Alpine Linux のコンテナで動かす、よくある構成

    View Slide

  6. Hugo Extended on Alpine Linux
    その後 hugo_extended イメージが動かないという報告がきた
    ● https://gitlab.com/pages/hugo/issues/31
    ● 複雑な SCSS を含むサイトで Segmentation fault が起きるらしい
    ● 同じ問題を抱えている人が結構多いらしい
    ○ Alpine Linux を使っているとだめ
    ○ musl libc だと動かない glibc だと動く

    View Slide

  7. musl libc
    musl は glibc と ABI 互換性のある Linux 用軽量 C ランタイムライブラリ
    ● https://www.musl-libc.org
    ● コンテナ容量削減の目的で Alpine Linux などで使われている
    ● glibc でビルドした実行ファイルがそのまま動くが一部動かないものがある
    ● musl が原因で Alpine Linux で動かない場合の一般的な解決策
    a. Alpine Linux をやめて Debian などを使う → コンテナサイズふえる
    b. Alpine Linux の中に glibc もインストールする → めんどくさい
    https://gitlab.com/dar/hugo/blob/feat-postCSS/Dockerfile

    View Slide

  8. 原因の調査
    $ git clone https://gitlab.com/pirivan/pirivan.gitlab.io
    $ cd pirivan.gitlab.io
    $ git checkout development
    $ docker run --rm -it -v $PWD:/src --workdir /src registry.gitlab.com/pages/hugo/hugo_extended /bin/sh
    /src # hugo
    Building sites ... Segmentation fault
    まずは報告のあったサイトで hugo を動かしてみる

    View Slide

  9. gdb による調査
    $ docker run --rm -it -v $PWD:/src --workdir /src --privileged \
    registry.gitlab.com/pages/hugo/hugo_extended /bin/sh
    /src # apk add gdb
    /src # gdb hugo
    (gdb) run
    Starting program: /usr/bin/hugo
    [New LWP 15]
    [New LWP 16]
    [New LWP 17]
    [New LWP 18]
    [New LWP 19]
    [New LWP 20]
    [New LWP 21]
    Building sites … [New LWP 22]
    [New LWP 23]
    [New LWP 24]
    [New LWP 25]
    [New LWP 26]
    次にデバッガで hugo を動かしてみる

    View Slide

  10. gdb による調査
    Thread 6 "hugo" received signal SIGSEGV, Segmentation fault.
    [Switching to LWP 19]
    0x0000000000f5807e in Sass::Parser::parse_expression() ()
    (gdb) info threads
    Id Target Id Frame
    1 LWP 11 "hugo" 0x00000000004e63e3 in ?? ()
    2 LWP 15 "hugo" 0x00000000004e5e4d in ?? ()
    3 LWP 16 "hugo" 0x00000000004e63e3 in ?? ()
    4 LWP 17 "hugo" 0x00000000004e63e3 in ?? ()
    5 LWP 18 "hugo" 0x00000000004e63e3 in ?? ()
    * 6 LWP 19 "hugo" 0x0000000000f5807e in Sass::Parser::parse_expression() ()
    7 LWP 20 "hugo" 0x00000000004e63e3 in ?? ()
    8 LWP 21 "hugo" 0x00000000004e63e3 in ?? ()
    9 LWP 22 "hugo" 0x00000000004e63e3 in ?? ()
    10 LWP 23 "hugo" 0x00000000004e63e3 in ?? ()
    11 LWP 24 "hugo" 0x00000000004e63e3 in ?? ()
    12 LWP 25 "hugo" 0x00000000004e63e3 in ?? ()
    13 LWP 26 "hugo" 0x00000000004e63e3 in ?? ()
    スレッドのひとつが libsass 関数の中で SIGSEGV

    View Slide

  11. gdb による調査
    (gdb) bt
    #0 0x0000000000f5807e in Sass::Parser::parse_expression() ()
    #1 0x0000000000f59520 in Sass::Parser::parse_relation() ()
    #2 0x0000000000f5a7c0 in Sass::Parser::parse_conjunction() ()
    ...
    ...
    #90 0x0000000000f94988 in sass_compiler_parse ()
    #91 0x0000000000ea0f87 in _cgo_a9af2a58ca08_Cfunc_sass_compiler_parse ()
    #92 0x00000000004e3d10 in ?? ()
    #93 0x0000000001ea6100 in __bss_start ()
    #94 0x0000000000000001 in ?? ()
    #95 0x0000000001ec0006 in __bss_start ()
    #96 0x00007ffff5a221f0 in ?? ()
    #97 0x000000c000fecee8 in ?? ()
    #98 0x0000000000002b40 in ?? ()
    #99 0x000000c000001e00 in ?? ()
    #100 0x00000000004b9690 in ?? ()
    #101 0x0000000000000000 in ?? ()
    スタックが深い... これはスタックオーバーフロー?

    View Slide

  12. musl スレッドスタックサイズ
    Thread stack size
    The default stack size for new threads on glibc is determined
    based on the resource limit governing the main thread’s stack
    (RLIMIT_STACK). It generally ends up being 2-10 MB.
    musl provides a default stack size of 80k. This does not
    include the guard page, nor does it include the space used for
    TLS unless total TLS size is very small. So the actual map size
    may appear closer to 90k, with around 80k usable by the
    application. This size was determined empirically with the goals of
    not gratuitously breaking applications but also not causing large
    amounts of memory and virtual address space to be committed in
    programs with large numbers of threads. Programs needing
    larger stacks, or which explicitly want a smaller stack, should
    make this explicit with pthread_attr_setstacksize. For largely
    unrestrained use of the standard library, a minimum of 12k is
    recommended, but stack sizes down to 2k are allowed.
    https://wiki.musl-libc.org/functional-differences-from-glibc.html
    ● muslはスレッドに対して極端に少ない
    スタックを割り当てる
    ● 80KBと書いてあるが現在は128KBに
    増量されている
    ● 解決方法はスレッドを作る前に
    pthread_attr_setstacksize() などでス
    タックサイズを増やすこと
    ● Ruby・Python含む様々なプロジェクト
    がmuslのスタックオーバーフローに辛
    酸を舐めさせられている
    ● Goではlibsassのような外部ライブラリ
    を呼び出すときに問題になる

    View Slide

  13. 解決策の模索
    ● Go 言語のランタイムライブラリや Hugo を修正するのは難しい
    ● なんとかスタックサイズをランタイムで増加させられないか、muslのコードを読み始
    める
    ● だいたいは DEFAULT_STACK_SIZE といった定数にハードコードされているのだが、
    ELFダイナミックリンカであやしげなコードを発見
    } else if (ph->p_type == PT_GNU_STACK) {
    if (!runtime && ph->p_memsz > __default_stacksize) {
    __default_stacksize =
    ph->p_memsz < DEFAULT_STACK_MAX ?
    ph->p_memsz : DEFAULT_STACK_MAX;
    }
    }
    http://git.musl-libc.org/cgit/musl/tree/ldso/dynlink.c#n635

    View Slide

  14. musl 新機能の発見
    support setting of default thread stack size via
    PT_GNU_STACK header
    this facilitates building software that assumes a large default
    stack size without any patching to call pthread_setattr_default_np
    or pthread_attr_setstacksize at each thread creation site, using
    just LDFLAGS.
    normally the PT_GNU_STACK header is used only to reflect
    whether executable stack is desired, but with GNU ld at least,
    passing -Wl,-z,stack-size=N will set a size on the program header.
    with this patch, that size will be incorporated into the default
    stack size (subject to increase-only rule and
    DEFAULT_STACK_MAX limit).
    http://git.musl-libc.org/cgit/musl/commit/?id=7b3348a98c139b4
    b4238384e52d4b0eb237e4833
    ● なんと ELF のプログラムヘッダの
    PT_GNU_STACK セクションのサイズで
    デフォルトスタックサイズを変更できる
    機能が追加されていることがわかった
    (ELF = Linux などで使われる実行ファ
    イルの形式のこと)
    ● リンク時のオプションで
    -Wl,-z,stack-size=N のように設
    定することが前提だが…
    ● 実行ファイルしかなくても ELF プログラ
    ムヘッダにバイナリパッチできればス
    タックサイズは増やせる!

    View Slide

  15. Hugo Extended 実行ファイル (Linux amd64) のプログラムヘッダ
    $ objdump -p hugo
    hugo: file format ELF64-x86-64
    Program Header:
    PHDR off 0x0000000000000040 vaddr 0x0000000000400040 paddr 0x0000000000400040 align 2**3
    filesz 0x0000000000000230 memsz 0x0000000000000230 flags r-x
    INTERP off 0x0000000000000270 vaddr 0x0000000000400270 paddr 0x0000000000400270 align 2**0
    filesz 0x000000000000001c memsz 0x000000000000001c flags r--
    LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
    filesz 0x00000000018266da memsz 0x00000000018266da flags r-x
    LOAD off 0x0000000001826e60 vaddr 0x0000000001e26e60 paddr 0x0000000001e26e60 align 2**21
    filesz 0x0000000000074801 memsz 0x00000000000a7718 flags rw-
    DYNAMIC off 0x0000000001830cf0 vaddr 0x0000000001e30cf0 paddr 0x0000000001e30cf0 align 2**3
    filesz 0x0000000000000220 memsz 0x0000000000000220 flags rw-
    NOTE off 0x000000000000028c vaddr 0x000000000040028c paddr 0x000000000040028c align 2**2
    filesz 0x00000000000000a8 memsz 0x00000000000000a8 flags r--
    TLS off 0x0000000001826e60 vaddr 0x0000000001e26e60 paddr 0x0000000001e26e60 align 2**3
    filesz 0x0000000000000000 memsz 0x0000000000000008 flags r--
    EH_FRAME off 0x00000000017e30fc vaddr 0x0000000001be30fc paddr 0x0000000001be30fc align 2**2
    filesz 0x00000000000079b4 memsz 0x00000000000079b4 flags r--
    STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
    filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
    RELRO off 0x0000000001826e60 vaddr 0x0000000001e26e60 paddr 0x0000000001e26e60 align 2**0
    filesz 0x000000000000a1a0 memsz 0x000000000000a1a0 flags r--
    この memsz を
    書き換えたい
    こっちは execstack(8)
    で書き換え可能
    PT_GNU_STACK
    is これ

    View Slide

  16. Go 言語 debug/elf でバイナリパッチ
    debug/elf は ELF オブジェクトファイルを操作するためのパッケージ
    ● https://golang.org/pkg/debug/elf/
    ● ELF ヘッダ、プログラムヘッダの struct や各種定数の定義がある
    ● encoding/binary と組み合わせて使うことができる
    ● 標準パッケージでこういうことができる Go 言語は非常にすばらしい

    View Slide

  17. muslstack
    Binary patch utility to set default thread stack size for musl libc
    ● https://github.com/yaegashi/muslstack
    ● Go 言語の debug/elf を利用した ELF ヘッダ書き換えユーティリティ
    ● PT_GNU_STACK ヘッダが対象という点で execstack(8) に類似するツール
    ● 様々な Linux ELF 実行ファイルに対応
    ○ 32-bit / 64-bit
    ○ little endian / big endian
    ○ x86, x86-64, arm, arm64, etc.
    ● 効果を確認できるテストケースあり

    View Slide

  18. func patch(path string, setStackSize bool, stackSize uint64) (uint64, error)
    switch fh.Class {
    case elf.ELFCLASS32:
    eh := &elf.Header32{}
    if err := binary.Read(f, fh.ByteOrder, eh); err != nil {
    return 0, err
    }
    phoff := int64(eh.Phoff)
    phentsize := int64(eh.Phentsize)
    phnum := int(eh.Phnum)
    for i := 0; i < phnum; i++ {
    off := phoff + int64(i)*phentsize
    f.Seek(off, io.SeekStart)
    ph := &elf.Prog32{}
    if err := binary.Read(f, fh.ByteOrder, ph); err != nil {
    return 0, err
    }
    if ph.Type == ptGnuStack {
    if setStackSize {
    ph.Memsz = uint32(stackSize)
    f.Seek(off, io.SeekStart)
    if err := binary.Write(f, fh.ByteOrder, ph); err != nil {
    return 0, err
    }
    }
    return uint64(ph.Memsz), nil
    }
    }
    https://github.com/yaegashi/muslstack/blob/master/main.go

    View Slide

  19. Dockerfile での muslstack 利用例
    FROM golang:1.12-alpine
    ARG HUGO=hugo
    ARG HUGO_VERSION=0.55.6
    ARG HUGO_SHA=39d3119cdb9ba5d6f1f1b43693e707937ce851791a2ea8d28003f49927c428f4
    ARG HUGO_EXTENDED_SHA=8962b8cdc0ca220da97293cea0bb1b31718cb4d99d0766be6865cb976b1c1805
    RUN set -eux && \
    case ${HUGO} in *_extended) HUGO_SHA="${HUGO_EXTENDED_SHA}" ;; esac && \
    apk add --update --no-cache ca-certificates openssl git && \
    wget -O ${HUGO_VERSION}.tar.gz \
    https://github.com/spf13/hugo/releases/download/v${HUGO_VERSION}/${HUGO}_${HUGO_VERSION}_Linux-64bit.tar.gz && \
    echo "${HUGO_SHA} ${HUGO_VERSION}.tar.gz" | sha256sum -c && \
    tar xf ${HUGO_VERSION}.tar.gz && mv hugo* /usr/bin/hugo
    RUN go get github.com/yaegashi/muslstack
    RUN muslstack -s 0x800000 /usr/bin/hugo
    FROM alpine:edge
    ARG HUGO=hugo
    COPY --from=0 /usr/bin/hugo /usr/bin
    RUN set -eux && \
    ...
    muslstackでhugo
    実行ファイルを修正
    alpine:edge でないと
    効果がないので注意
    マルチステージビルドで活用

    View Slide

  20. 余談
    alpine:edge だと Hugo Extended がなかなか落ちてくれない
    ● musl デフォルトスタックが 80KB→128KB に増えたから?
    ● SCSS 版 Ackermann 関数を食わせてようやく半々の確率で落ちる
    /usr/bin/hugo: stackSize: 0x800000
    STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
    filesz 0x0000000000000000 memsz 0x0000000000800000 flags rw-
    Building sites ... ERROR 2019/06/16 14:22:30 error: failed to transform resource: SCSS processi
    ng failed: file "stdin", line 6, col 25: Stack depth exceeded max of 1024
    Error: Error building site: logged 1 error(s)
    Total in 2372 ms
    /usr/bin/hugo: stackSize: 0x0
    STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
    filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
    Building sites ... Segmentation fault

    View Slide

  21. まとめ
    Hugo Extended
    Alpine Linux (musl libc) スタックサイズ問題
    musl libc 新機能 PT_GNU_STACK ヘッダでスタックサイズ設定
    Go 言語 debug/elf パッケージ
    https://github.com/yaegashi/muslstack

    View Slide

  22. おわり
    Alpine Linux (musl libc) コンテナで動かないプログラムにお悩みの方は
    muslstack と alpine:edge を試してみてください!

    View Slide