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. 自己紹介 八重樫 剛史 Takeshi Yaegashi • 株式会社バンダイナムコスタジオ所属 • Linux・Unix・OSS・低レベルなことが好きなエンジニア •

    もちろん Go 言語も大好きです! • Go を使ったお仕事 ◦ IoT 案件の Raspberry Pi 制御プログラム ◦ スマホゲームアプリのサーバ
  2. Hugo Hugo は Go 言語で書かれた静的サイトジェネレータ • https://gohugo.io • Hugo には

    2 種類あり、それぞれのバイナリが配布されている ◦ Hugo ▪ 基本機能のみの Hugo ◦ Hugo Extended ▪ SASS/SCSS のアセット処理もできる拡張版 Hugo ▪ C++ で書かれたライブラリ libsass を組み込んでいる (cgo)
  3. 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 のコンテナで動かす、よくある構成
  4. Hugo Extended on Alpine Linux その後 hugo_extended イメージが動かないという報告がきた • https://gitlab.com/pages/hugo/issues/31

    • 複雑な SCSS を含むサイトで Segmentation fault が起きるらしい • 同じ問題を抱えている人が結構多いらしい ◦ Alpine Linux を使っているとだめ ◦ musl libc だと動かない glibc だと動く
  5. 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
  6. 原因の調査 $ 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 を動かしてみる
  7. 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 を動かしてみる
  8. 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
  9. 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 ?? () スタックが深い... これはスタックオーバーフロー?
  10. 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のような外部ライブラリ を呼び出すときに問題になる
  11. 解決策の模索 • 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
  12. 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 プログラ ムヘッダにバイナリパッチできればス タックサイズは増やせる!
  13. 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 これ
  14. Go 言語 debug/elf でバイナリパッチ debug/elf は ELF オブジェクトファイルを操作するためのパッケージ • https://golang.org/pkg/debug/elf/

    • ELF ヘッダ、プログラムヘッダの struct や各種定数の定義がある • encoding/binary と組み合わせて使うことができる • 標準パッケージでこういうことができる Go 言語は非常にすばらしい
  15. 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. • 効果を確認できるテストケースあり
  16. 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
  17. 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 でないと 効果がないので注意 マルチステージビルドで活用
  18. 余談 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
  19. まとめ Hugo Extended Alpine Linux (musl libc) スタックサイズ問題 musl libc

    新機能 PT_GNU_STACK ヘッダでスタックサイズ設定 Go 言語 debug/elf パッケージ https://github.com/yaegashi/muslstack