MariDB Connector/C がデフォルトでSSLを強制するようになった

MariaDB 11.4 がGAになり、SSLが自動で有効になるようになりました。合わせてMariaDB Connector/Cも3.4からデフォルトでSSLを利用し、かつサーバーの証明書を検証するようになりました。

これにより、MariaDB Connector/C 3.4以降のライブラリを利用してMySQLや古いMariaDBに接続する場合に、SSLで接続できないとか、証明書の検証に失敗するといった問題が発生するようになりました。 これらをリンクしたPythonのmysqlclientやRubyのmysql2のようなクライアントライブラリには続々と問題報告が上がってきています。

この問題に対処するいくつかの方法を紹介します。まずはConnectorをリンクしたライブラリの利用者ができることから。

  • MariaDB Connector/C 3.3を使う
  • ~/.my.cnf 等が利用されるなら、 [client] セクションに disable-ssl を書いてSSLを無効化するか、 disable-ssl-verify-server-cert を書いて証明書の検証を無効化する。
  • MariaDB Connector/C 3.4 をビルドする時のcmakeのオプションに -DDEFAULT_SSL_VERIFY_SERVER_CERT=0 を追加する。(証明書の検証をデフォルトでオフにできるだけで、SSLをデフォルトでオフにはならない) (参考)
  • MariaDB Connector/C 3.4.2 (未リリース)以降を使い、 MARIADB_TLS_DISABLE_PEER_VERIFICATION 環境変数を設定する。(同じく検証のみがオフになる)(参考)

また、MariaDB Connector/CやMySQL Connector/Cを利用するクライアントライブラリを開発している人向けのヒントも紹介します。

  • MySQLMYSQL_OPT_SSL_MODE オプションを持つが、MariaDBには存在しない。
  • MySQLは8.0で MYSQL_OPT_SSL_ENFORCE オプションを削除したが、MariaDBでは現役で、SSLの有効、無効を切り替える手段。
  • Connector/CがデフォルトでSSLを無効だという前提が崩れたので、SSLを有効にするオプションだけでなく、無効にするオプションを実装する必要がある。
    • 後方互換性やMySQLとの一貫性を考えると、ユーザーがSSL有効を指定しなかった場合は MYSQL_OPT_SSL_ENFORCE を 0 にしてSSL無効化する方法がある。
    • MariaDBの「デフォルトでセキュアに」というポリシーを尊重するのであれば、ユーザーがSSL無効化や検証無効化を明示的に指定したときに、デフォルトで無効になっている事を前提にせずちゃんと無効化するようにする。

小さい文字列からASCII外のバイトを見つけたい

Pythonの内部処理で、与えられたUTF-8の文字列がASCII文字のみを含むかをテストするコードを書いています。

size_t型が8バイトの時、ポインタを char* から size_t* にキャストして、0x8080808080808080ullと論理積を取れば8バイトを一度に処理できます。 単純化すると次のようなコードになります。

#define ASCII_MASK 0x8080808080808080ull

size_t find_first_nonascii(const char *start, const char *end)
{
    const char *p = start;

    // 8バイトずつ処理
    while (p <= end - 8) {
        if (*(size_t*)p & ASCII_MASK) {
            break;
        }
        p += 8;
    }
    // 1バイトずつ処理
    while (p < end) {
        if (*p & 0x80) {
            return p-start;
        }
        p++;
    }
    return end-start;
}

1バイトずつ処理する部分が気になります。これは最下位ビットから1になっているビットを探して位置を返してくれるGCC/clangのビルトイン関数 __builtin_ctzll を使えば、前半部分をこのように高速化できます。

    while (p <= end - 8) {
        size_t x = *(size_t*)p & ASCII_MASK;
        if (x) {
            // p[0] がASCII外の時、 ctzll(0x80) == 7, (7-7)/8 == 0
            // p[1] がASCII外の時、 ctzll(0x8000) == 15, (15-7)/8 == 1
            return p-start + (__builtin_ctzll(x) - 7) / 8;
        }
        p += 8;
    }

しかし問題は後半部分です。 8バイト未満の端数部分をsize_tに格納できれば同じ高速化ができます。たとえば次のようになります。

    // (end-p)が3のとき、 3*8=24, 1ull << 24 = 0x1000000, 0x1000000-1 = 0xffffff.
    // これとmaskしてやれば、4バイト以降は無視できる。
    size_t x = *(size_t*)p & ((1ull << (end - p) * 8) - 1) & ASCII_MASK;
    if (x) {
        return p - start + (__builtin_ctzll(x) - 7) / 8;
    }
    return end - start;

これでバイト数*2回だった分岐が1回に減らせてめでたしめでたし、、、なのですが、残念ながら入力の文字列の範囲外を読んでからマスクしているだけなので、範囲外アクセスが発生しています。 試しに clang の AddressSanitizer を使ってみるとエラーが出てしまいました。

アライメントさえ合っていれば、8バイトのreadがセグメンテーション違反を起こすことは無いと思いますが、範囲外アクセスはやはり避けたいです。 そこで次のように switch 文を書いてみました。

    size_t u = (size_t)(p[0]);
    switch (end - p) {
        default:
            u |= (size_t)(p[7]) << 56ull;
        // fall through
        case 7:
            u |= (size_t)(p[6]) << 48ull;
        // fall through
        case 6:
            u |= (size_t)(p[5]) << 40ull;
        // fall through
        case 5:
            u |= (size_t)(p[4]) << 32ull;
        // fall through
        case 4:
            u |= (size_t)(p[3]) << 24;
        // fall through
        case 3:
            u |= (size_t)(p[2]) << 16;
        // fall through
        case 2:
            u |= (size_t)(p[1]) << 8;
            break;
        case 1:
            break;
    }
    if (u & ASCII_CHAR_MASK) {
        return p - start + (__builtin_ctzll(u & ASCII_CHAR_MASK) - 7) / 8;
    }

これで範囲外アクセスはなくなりました。 clang が生成するアセンブリ(intel形式)は次のような感じになります。

...
                u |= (size_t)(p[7]) << 56ull;
 147:   44 0f b6 59 07          movzx  r11d,BYTE PTR [rcx+0x7]
 14c:   49 c1 e3 38             shl    r11,0x38
 150:   4d 09 da                or     r10,r11
                u |= (size_t)(p[6]) << 48ull;
 153:   44 0f b6 59 06          movzx  r11d,BYTE PTR [rcx+0x6]
 158:   49 c1 e3 30             shl    r11,0x30
 15c:   4d 09 da                or     r10,r11
                u |= (size_t)(p[5]) << 40ull;
 15f:   44 0f b6 59 05          movzx  r11d,BYTE PTR [rcx+0x5]
 164:   49 c1 e3 28             shl    r11,0x28
 168:   4d 09 da                or     r10,r11
                u |= (size_t)(p[4]) << 32ull;
...

1バイトずつリードして、左シフトして、ORしてますね。switchのジャンプテーブルは使われているので分岐を減らす目的は達成できました。

しかし、アライメントを保証することで、範囲外リードを安全に行うような方法があればそっちのほうがいいので、その方法を現在探しています。

"Missing Signed-By in the sources.list(5) entry for" を解消する

開発機をUbuntu 24.04 にアップデートしてしばらく経って、 apt update したときに表示される次のwarningが今更気になった。

N: Missing Signed-By in the sources.list(5) entry for 'http://ftp.riken.go.jp/Linux/ubuntu'

試しに /etc/apt/sources.list をのぞいてみると、次の1行になっていた。

# Ubuntu sources have moved to /etc/apt/sources.list.d/ubuntu.sources

/etc/apt/sources.list.d には third-party.sources, ubuntu.sources の2つのファイルがあった。

/etc/apt/sources.list.d$ cat ubuntu.sources
Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

/etc/apt/sources.list.d$ cat third-party.sources
Types: deb
URIs: http://ftp.riken.go.jp/Linux/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse

Ubuntuをアップグレードした時に sources.list 内のソースが、公式のものは ubuntu.sources に、ミラーのものは third-party.sources に分割されたようだ。

third-party.sources には Signed-By: が付いていないが、本家のミラーなので ubuntu.sources の Signed-By: をコピーしてみたところ、冒頭のwarningが消えた。

ワイヤレスイヤホンの高音質通話をやっと体験できた

SOUNDPEATS Air4 Pro というイヤホンを使っていて、結構通話品質が良い。去年発売のモデルだと結構有名なメーカーの製品でも金属管の中を通したかのようなエコーがかかる製品があったりするけれども、このイヤホンはかなり自然な音で通話できる。今はセール価格とクーポン合わせると7000円以下で買えるのでけっこうお勧め。

このイヤホンは高音質で通話できるaptX VoiceとLE Audioに対応しているのだが、Pixel 8でLE Audioを使ってみるとマイクが動作しない。イヤホン側の問題なのかスマホ側の問題なのかはまだ不明。最近aptX Voice対応スマホを安く譲っていただいて、やっと高音質通話を体験することができた。

Pixel 8と接続したときは、十分にクリアだとはいえ、やはり高音が無いこもり感や、狭い押し入れの中で通話しているかのようなエコーがある。

一方でaptX Voiceで接続したときは圧倒的に音質がクリアになり、こもり感もエコーも感じない。有線イヤホンと比べても遜色ないだろう。

aptX Voiceを使えるのは、だいたいaptX Losslessに対応したイヤホンとスマホだ。Snapdragon Sound認証が必要になる。aptX Losslessはオーディオにお金を払う人がいるからまだわかるが、aptX Voiceまで抱き合わせでプレミア化する必要はあったのだろうか?スマホもイヤホンもQualcommのチップを使ってれば通話音質がいいよ、って方が、ユーザーにとってもQualcommにとっても良かったのではないか。

あと、YouTubeでイヤホンのレビューをしてくれている方達は、aptX Lossless対応イヤホンの通話性能のテストをするときは、録音するスマホ側もaptX Lossless対応スマホで録音してみて欲しい。

ちなみに、現在の高音質通話機能はざっくりとこれくらいある。

  • LE Audio - Sonyの最近のイヤホンとXperia、Pixel Buds Pro2とPixel 8以降では実用化済み?まだまだ不安定な組み合わせが多いが将来的に主流になるはず。
  • SWB - Pixel 7 Pro以降とPixel Buds Proで使える。BluetoothのクラシックオーディオのHFP v1.9で追加された、コーデックにLC3を使うもの。標準規格なので、後述の独自コーデックと違って今後使える機種が増えるだろう。でも同時にLE Audioにも対応するだろうから、どれくらい利用場面が増えるかは不明。
  • aptX Voice - QualcommのSoCを利用した一部のスマホ&イヤホンの組み合わせで使える。aptX Losslessに対応してたら使える可能性が高い。仕組みはSWBと同じで、コーデックにaptX Voiceを使う。
  • AAC-ELD - AppleiPhoneAirPods 第三世代以降で使えるもの。同じくHFPのコーデックにAAC-ELDを使う。従来のHD Voice(mSBC)のサンプリングレートが16kHz、SWBやaptX Voiceが32kHzなのに対して、AAC-ELDは24kHzらしいので、中間くらいの性能と思われる。

Python 3.12.7 で msgpack におけるメモリリークが修正された

github.com

Python 3.12から参照カウントを固定化するImmortal Objectが導入されたのですが、それがinterned string全てを強制的に固定参照カウントにしてしまっていました。

Pythonの文字列はimmutableなので、短くて大量に同じ値が出てくる可能性がある文字列は積極的にオブジェクトを1つにまとめて省メモリ化したいです。 さらにその文字列が変数名などと共通する可能性があるときは、インターン化することで単にインスタンスを一つにまとめることによる省メモリ効果だけでなく、その文字列をキーにして名前を参照するときに文字列比較がポインタ比較だけで終わるという効果も期待できます。

ということで、Pythonpathlibの一部sys.intern() が使われていたり、私が開発しているmsgpack-pythonでもオブジェクトのキーに対してインターンを実施していました。 それがインターン文字列のimmortal化の影響で、pathlibでランダムなディレクトリ名(ハッシュ値ディレクトリ名になってるとか)を扱ったり、外部から入力されるmsgpackのキーが固定の文字列ではなく多様な文字列を含んでいる場合などに、緩やかにメモリ使用量が増え続けることになってしまっていました。

それがrevertされてintern文字列全てがimmortalにならなくなったことで、Python 3.12.7で問題が解消されました。該当するユースケースを持つアプリは早めにアップデートをお勧めします。

他に影響を受けそうなモジュールとして標準ライブラリのjsonモジュールとorjsonを見てみたのですが、jsonインターン化せず独自のdictを使ってオブジェクト再利用を行なっていて、orjsonもassociative_cacheを使った再利用を行なっているので、インターン文字列永続化の影響はありませんでした。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。