glibc のバージョンアップでアプリケーションが動かなくなった件

2020年1月にリリースされた glibc-2.17-307.el7.1 を CentOS7 にインストールすると、今まで動いていたプログラムコードが動作しなくなる可能性があります。この問題に遭遇する可能性は低いと思われますが、影響範囲が広いため情報を共有したいと思います。

## まずは結論

従来 IP アドレス文字列は末尾に空白が含まれる事が許されていました。例えば `127.0.0.1[空白]` という文字列は valid な IP アドレスとして許容されていました。これが許されなくなったので、IP アドレス文字列はトリミングしてから利用する必要があります。この問題は多くのツールや言語環境で影響を受けます。

## 何が問題なのか？

glibc のバージョンアップに伴い [getaddrinfo(3)](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/getaddrinfo.3.html) の動作仕様が一部変更になりました。getaddrinfo(3) は IP アドレス文字列を入力とし、TCP/IP 接続を行うための情報を返す C 言語の関数です。前述の通りこれまで末尾に空白等が含まれる事が許容されていましたが、今後はそれが許されなくなっています。

getaddrinfo(3) は TCP/IP 接続を行うための基本的な仕組みなので、非常に多くの言語環境やアプリケーションで利用されています。従って直接的には利用していなくても影響を受けるシステムはかなり多いと考えられます。筆者の場合はある会社の PHP アプリケーションが突然データベースに接続できなくなった事からこの問題を知りました。

対策は簡単です。IP アドレス文字列を利用する時にトリミングして末尾のホワイトスペースを削除すればよいだけです。

## 現象の確認 - ping

以下は glibc の古いバージョンである、glibc-2.17-260.el7 がインストールされている環境で、ping コマンドを実行している様子です。

glibcのバージョン確認

```shell_session
sh-4.2$ yum list installed|grep glibc
Repodata is over 2 weeks old. Install yum-cron? Or run: yum makecache fast
glibc.x86_64                             2.17-260.el7_6.3            @updates
glibc-common.x86_64                      2.17-260.el7_6.3            @updates
glibc-devel.x86_64                       2.17-260.el7_6.3            @updates
glibc-headers.x86_64                     2.17-260.el7_6.3            @updates
```

末尾に空白がある IP アドレスで ping を実行

```shell_session
sh-4.2$ ping -c 1 '127.0.0.1    '
PING 127.0.0.1     (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.011 ms

--- 127.0.0.1     ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.011/0.011/0.011/0.000 ms
sh-4.2$ echo $?
0
```

末尾に改行がある IP アドレス文字列で ping を実行

```shell_session

sh-4.2$ ping -c 1 '127.0.0.1
> '
PING 127.0.0.1
 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.011 ms

--- 127.0.0.1
 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.011/0.011/0.011/0.000 ms
sh-4.2$ echo $?
0
```

このように従来は問題なく ping が実行できました。しかし、glibc-2.17-307.el7.1 がインストールされている環境では以下のようにエラーになります。

```shell_session
sh-4.2$ yum list installed|grep glibc
glibc.x86_64                             2.17-307.el7.1              @base
glibc-common.x86_64                      2.17-307.el7.1              @base
glibc-devel.x86_64                       2.17-307.el7.1              @base
glibc-headers.x86_64                     2.17-307.el7.1              @base
sh-4.2$ ping -c 1 '127.0.0.1    '
ping: 127.0.0.1    : Name or service not known
sh-4.2$ echo $?
2
sh-4.2$ ping -c 1 '127.0.0.1
> '
ping: 127.0.0.1
: Name or service not known
sh-4.2$ echo $?
2
```

## 現象の確認 - php

php でも確認してみましょう。以下は glibc-2.17-260.el7 の環境で、PDO を利用してデータベース接続を行っている例です（適当に改行しています）。

```shell_session
sh-4.2$ php -r "new \PDO('mysql:host=127.0.0.1;dbname=dbname', 'user', 'pass');"
PHP Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[HY000] [1045]
 Access denied for user 'user'@'localhost' (using password: YES)' in Command line code:1
Stack trace:
#0 Command line code(1): PDO->__construct('mysql:host=127....', 'user', 'pass')
#1 {main}
  thrown in Command line code on line 1
```

データベース名、ユーザー名、パスワードすべてが存在しない物なのでエラーが発生しています。ただしローカルホストで MySQL が動作しているので、エラーメッセージは `Access denied for...` になっています。以下はホスト名の末尾に空白を入れた場合ですが、まったく同じ結果でした。

```shell_session
sh-4.2$ php -r "new \PDO('mysql:host=127.0.0.1      ;dbname=dbname', 'user', 'pass');"
PHP Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[HY000] [1045]
 Access denied for user 'user'@'localhost' (using password: YES)' in Command line code:1
Stack trace:
#0 Command line code(1): PDO->__construct('mysql:host=127....', 'user', 'pass')
#1 {main}
  thrown in Command line code on line 1
```

次の例は glibc-2.17-307.el7.1 がインストールされた環境での結果です。glibc 以外はインストールされているミドルウェアはすべて同一です。

```shell_session
sh-4.2$ php -r "new \PDO('mysql:host=127.0.0.1;dbname=dbname', 'user', 'pass');"
PHP Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[HY000] [1045]
 Access denied for user 'user'@'localhost' (using password: YES)' in Command line code:1
Stack trace:
#0 Command line code(1): PDO->__construct('mysql:host=127....', 'user', 'pass')
#1 {main}
  thrown in Command line code on line 1
```

IP アドレスの末尾に空白が入っていなければ結果に変化はありません。空白を入れてみると以下の通りです。

```shell_session
sh-4.2$ php -r "new \PDO('mysql:host=127.0.0.1      ;dbname=dbname', 'user', 'pass');"
PHP Warning:  PDO::__construct(): php_network_getaddresses: getaddrinfo failed: 
 Name or service not known in Command line code on line 1
PHP Stack trace:
PHP   1. {main}() Command line code:0
PHP   2. PDO->__construct() Command line code:1
PHP Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[HY000] [2002]
 php_network_getaddresses: getaddrinfo failed: Name or service not known' in Command line code:1
Stack trace:
#0 Command line code(1): PDO->__construct('mysql:host=127....', 'user', 'pass')
#1 {main}
  thrown in Command line code on line 1
```

エラーメッセージは `php_network_getaddresses: getaddrinfo failed` になりました。getaddrinfo(3) が失敗していると推測されます。

## 現象の確認 - C言語

直接 getaddrinfo(3) を実行してみた結果も確認しましょう。以下のプログラムを用意しました。

```test1.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
  struct addrinfo hints, *res;
  int err;

  memset(&hints, 0, sizeof(hints));
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_family = AF_INET;

  if ((err = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {
    printf("error = %d\n", err);
    return 1;
  }
  puts("Ok");
  return 0;
}
```

glibc-2.17-260.el7 の環境では以下の通りとなりました。

```shell_session
sh-4.2$ gcc test1.c
sh-4.2$ ./a.out '127.0.0.1'
Ok
sh-4.2$ ./a.out '127.0.0.1      '
Ok
```

glibc-2.17-307.el7.1 の環境は以下のようになりました。

```shell_session
sh-4.2$ gcc test1.c
sh-4.2$ ./a.out '127.0.0.1'
Ok
sh-4.2$ ./a.out '127.0.0.1     '
error = -2
```


## 最後に

今回、getaddrinfo(3) の仕様変更により、ping コマンドと php の PDO ライブラリが影響を受ける事を説明しました。しかし、影響はこれに限定されるわけではなく、非常に広い範囲に影響していると思われます。一方、IP アドレスの末尾に空白が入ってしまうという事自体はそれほど多くはないかも知れません。ただし今まで許容されていただけに、思わぬ所でこの問題に遭遇する可能性があります。例えば改行区切りの IP アドレスの一覧等を利用する時、末尾の改行を削除せずに利用していないか、WEB-UI から入力された IP アドレスをトリミングしないで利用していないか等は、要チェックポイントかも知れません。

なお、今回は CentOS7 でのみ確認していますが、glibc 自体は他の OS でも利用されてますので影響はあると思います。状況は ping コマンドで簡単に確認できますので、各自試してみて下さい。