PulseAudio (32bit版)がDocker上で動作しない問題の解決法

PulseAudioの32bit版(i386 build)をDockerで動かそうとするも、うまく動かなくて苦労したので書き残しておく。

TL;DR

  • dokcer run のオプションに --security-opt seccomp=unconfined をつけたら解決する
  • ただし、セキュリティ理由の制限を外すことになるので注意

気づいたきっかけはWine

今どきなぜ32bit版のPulseAudioを使っているのかというと、Wineで32bitのWindowsアプリを起動したことがきっかけだった。

アプリは起動するが、音声が鳴らない。 Wineの出力を見てみると次のようにPulseAudioの内部でエラーが発生していた。

Assertion 'clock_gettime(CLOCK_REALTIME, &ts) == 0' failed at pulsecore/core-rtclock.c:93, function pa_rtclock_get(). Aborting.

WINEARCH=win32を設定後、winecfg で "Audio" のタブを開くと次のような画面になり、アプリごと落ちてしまった。 Wine上でサウンドドライバの認識ができてないようだった。

困ったことに、64bitのWineは正常なのに32bitの場合のみエラーが出てしまうという状況だった。

f:id:castaneai:20200919143929p:plain

PulseAudioが落ちる原因を調査する

まだ救いなのは、PulseAudioのエラーメッセージがわかりやすいことだった。 文字通りclock_getres 関数が0を返すことを期待したが、それ以外の値を返したので強制終了したということらしい。

Assertion 'clock_gettime(CLOCK_REALTIME, &ts) == 0' failed at pulsecore/core-rtclock.c:93, function pa_rtclock_get(). Aborting.

再現方法

再現させるためには64bit LinuxがベースのDocker containerで32bit版のPulseAudioを起動すればよい。 筆者は以下の環境と手順で再現をさせることができた。

  • ホストマシン: iMac 5K (2017)
  • Docker: Docker for Mac 2.3.0.5

Dockerfile

FROM ubuntu:20.04

ENV DEBIAN_FRONTEND noninteractive

RUN dpkg --add-architecture i386 \
    && apt update -y \
    && apt install -y pulseaudio:i386

ENTRYPOINT ["pulseaudio", "-v"]

Output

docker build -t pulse32 .
docker run --rm -it pulse32

...

I: [pulseaudio] main.c: This is PulseAudio 13.99.1
I: [pulseaudio] main.c: Page size is 4096 bytes
I: [pulseaudio] main.c: Machine ID is a9a6ee927f5b4b64b33bf688bd29d4e2.
I: [pulseaudio] main.c: Using runtime directory /root/.config/pulse/a9a6ee927f5b4b64b33bf688bd29d4e2-runtime.
I: [pulseaudio] main.c: Using state directory /root/.config/pulse.
I: [pulseaudio] main.c: Using modules directory /usr/lib/pulse-13.99.1/modules.
I: [pulseaudio] main.c: Running in system mode: no
E: [pulseaudio] core-rtclock.c: Assertion 'clock_getres(CLOCK_REALTIME, &ts) == 0' failed at pulsecore/core-rtclock.c:139, function pa_rtclock_hrtimer(). Aborting.

clock_getres の失敗原因を探る

clock_getresのMan pageを見ると、失敗時のエラーコードはerrnoに格納されるとのこと。

RETURN VALUE top clock_gettime(), clock_settime(), and clock_getres() return 0 for success, or -1 for failure (in which case errno is set appropriately).

というわけで、このエラーが出た直後にerrnoの変数の値を取得すればエラーコードがわかりそう。

gdbを使ってエラーコードを取得してみる。 取得のやり方は https://stackoverflow.com/a/18518122 を参考にした。

apt install -y gdb
gdb --args pulseaudio -v
(gdb) run
...
E: [pulseaudio] core-rtclock.c: Assertion 'clock_getres(CLOCK_REALTIME, &ts) == 0' failed at pulsecore/core-rtclock.c:139, function pa_rtclock_hrtimer(). Aborting.

Program received signal SIGABRT, Aborted.
0xf7eefc49 in __kernel_vsyscall ()

(gdb) print *((int*(*)())__errno_location)()
$3 = 1

errnoの値は 1 だった。moreutilsに含まれてるerrnoコマンドを使えば、1 がどのエラーに対応するか表示できる。 1番は EPERM Operation not permitted らしい。

apt install -y moreutils
errno 1
EPERM 1 Operation not permitted

clock_getresと x86, x64 システムコール

上の調査からclock_getresの呼び出しをしたが、権限が足りずにエラーになっているとわかった。 しかし、この問題で不思議なのは、64bitの場合は普通に成功していることだ。そこで、clock_getresに32bitと64bit(x86とx64)に違いがあるのか調べた。

x86 Linux の 32bit と 64bit のシステムコールの違い

このページによると、clock_getresはシステムコールの一種で、x86とx64ではシステムコール番号が異なっているらしい。なるほど。

Dockerとシステムコールの制限

また、clock_getres permission denied などでWeb検索すると、次のページがヒットした。

c - Linux syscall clock_settime(...) fails with EPERM in Docker container - Stack Overflow

Dockerではセキュリティ上の理由でいくつかのシステムコールはデフォルトで制限されている。よって特殊なオプションを与えない限り permission denied になるとのこと。 この記事では --privileged オプションを勧めているが、このオプションではなくてもシステムコールの制限を外す方法はある。それがseccomp オプション。

というわけで、seccomp=unconfined をつけて実行したらエラーが出なくなり解決した!

$ docker run --rm -it --security-opt seccomp=unconfined pulse32

...

I: [pulseaudio] main.c: Daemon startup complete.
I: [pulseaudio] module-device-restore.c: Storing port for device sink:auto_null.
I: [pulseaudio] module-device-restore.c: Storing volume/mute for device+port sink:auto_null:null.
I: [pulseaudio] module-device-restore.c: Storing port for device source:auto_null.monitor.
I: [pulseaudio] module-device-restore.c: Storing volume/mute for device+port source:auto_null.monitor:null.
I: [snapd-glib] module-snap-policy.c: Starting GLib main loop

しかし、もちろんこのオプションによりセキュリティ上の制限を外すのでリスクは上がってしまうことは考慮しておく必要がある。

まとめ

clock_getresというシステムコールはx86とx64で番号が異なっており、筆者の環境のDockerではx86のほうのシステムコールのみ制限されている状況となっていたようだった。

一見どうしたらいいのかわからないようなエラーでも、1ステップごとにじっくり調べていくと勉強にもなって解決につながるのはよかった。