👋

2038年問題 (2232年問題)

2022/03/27に公開

2038年問題 (2232年問題)

2038年問題

2038 年問題という問題があります。
これは時間を 1970/1/1 00:00:00 (UTC)からの経過秒数を 32bit 変数(time_t) であらわしていることが原因で 2038/1/19 03:14:07 (UTC) 以降の日付を扱えなくなる問題です。

1970/1/1 00:00:00 からの経過秒数のことを Epoch time と呼びます。
従来 time_tlong として定義されているので、最大値は 2**31-1 (0x7fffffff) です。

64bit のシステムの場合、time_t は 64bit 変数であらわされるのでこの問題は起きません。

参考

time_t の上限に対応する時刻を python で確認します。

$ python3
Python 3.9.7 (default, Sep 10 2021, 14:59:43)
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> datetime.datetime.utcfromtimestamp(0x7fffffff)
datetime.datetime(2038, 1, 19, 3, 14, 7)

Linux での対応

Linux 5.6 以降では 32bit OS であっても、内部的に 64bit で管理することにより 2038 年以降も使えるように対策がされています。
https://www.zaikei.co.jp/article/20200205/551832.html

ところがこれで終わりではなく、システムとして対応するために、アプリケーションも 2038年問題に対応する必要があります。アプリケーションが依存している C言語のライブラリ(通常は glibc) も 2038 年問題に対応する必要があります。

ここでは Cライブラリの time_tの64bit対応のみに限定していますが、アプリケーション側で time_t が 4バイトを仮定しているコードがあればそれも修正が必要です。

システム内で閉じている場合には再コンパイルするだけで問題にならないと考えられますが、
ファイルに書き出している、あるいはネットワーク経由で time_t のデータを送受信する場合に問題になる可能性があります。

相互互換性が問題にならないように設計する必要があります。

参考 (twitter の API での 64bit 対応)

整数のid が 32bit を超えたときに、id_str という文字列のエントリーを用意して、文字列として返したうえで解釈側で正しく64bit変数を利用して処理できるようにする対策です。

https://developer.twitter.com/ja/docs/basics/twitter-ids

http://westplain.sakuraweb.com/translate/twitter/API-Overview/Twitter-IDs.cgi

Cライブラリの対応

glibc での対応

glibc での対応はいけてなくて -D_FILE_OFFSET_BITS=64-D_TIME_BITS=64 のコンパイルオプションを指定してコンパイルする必要があります。

glibc 、各種ライブラリ含めて、すべてのアプリケーションで上記オプションを有効にしてコンパイルする必要があります。もし上記オプションを指定せずにコンパイルしたバイナリが混ざっているとクラッシュしてしまう可能性があります。

ちなみに -D_TIME_BITS=64 のみを指定して、-D_FILE_OFFSET_BITS=64 を指定せずにコンパイルするとコンパイルエラー になります。

$ git remote -v
origin  https://sourceware.org/git/glibc.git (fetch)
origin  https://sourceware.org/git/glibc.git (push)

$ git tag --contains 47f24c21ee38701ae275aa9e451f70fa3e77478c
glibc-2.34
glibc-2.34.9000
glibc-2.35
glibc-2.35.9000

yocto での対応 (2022/5/4 追記)

Yocto Project 4.0 (=kirkstone) では -D_TIME_BITS=64 および -D_FILE_OFFSET_BITS=64 が指定されて glibc がコンパイルされました。(32bit 版の raspberry pi 4 で確認)

date コマンドで 2222/1/1 に設定することが可能でした。

musl での対応

musl libc という MIT ライセンス の libc の実装があります。

musl はデフォルトで2038年問題に対応しているので musl を使用するようにコンパイルするだけで対応できます。

yocto での musl の使用方法

Yocto を使用してビルドする場合、

conf/local.conf に以下を追加してコンパイルします。

TCLIBC = "musl"

https://www.yoctoproject.org/docs/2.2.3/mega-manual/mega-manual.html#var-TCLIBC

2232年問題

Linux を 5.6 以降を使用して、適切なコンパイルオプションを指定して glibc をコンパイルする、あるいは musl を使用して、2038年問題に対応したとします。

Epoch time が 64bit になったので普通に考えると事実上好きな日付に設定できると思いますが、
Linux では設定可能な時刻が このコミット で意図的に 2232年までに制限されています。

timespec64_valid_settod という関数で設定する時間が TIME_SETTOD_SEC_MAX より大きい場合にはエラーではじくようになっています。

制限値の確認

#define NSEC_PER_SEC	1000000000L
#define KTIME_MAX			((s64)~((u64)1 << 63))
#define KTIME_SEC_MAX			(KTIME_MAX / NSEC_PER_SEC)
#define TIME_UPTIME_SEC_MAX		(30LL * 365 * 24 *3600)
#define TIME_SETTOD_SEC_MAX		(KTIME_SEC_MAX - TIME_UPTIME_SEC_MAX)
  • KTIME_MAX は 64bitの符号あり整数の最大値 (= 2**63-1 =0x7fffffffffffffff)
  • NSEC_PER_SEC10**9

カーネル内部は ktime_t という型の 64bit変数で nano-sec 単位で管理されています。
なので KTIME_MAX / NSEC_PER_SEC であらわされる秒数が linux で管理される時間の上限となります。

さらに時間設定した後、再起動されることなく 30年稼働できるように、KTIME_SEC_MAX から TIME_UPTIME_SEC_MAX (=30年分の uptime) を引いた値が時刻として設定できる上限になります。

$ python3
Python 3.9.7 (default, Sep 10 2021, 14:59:43)
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> TIME_UPTIME_SEC_MAX = 30  * 365 * 24 *3600
>>> KTIME_MAX = 0x7fffffffffffffff
>>> NSEC_PER_SEC = 10**9
>>> KTIME_SEC_MAX = KTIME_MAX // NSEC_PER_SEC
>>> TIME_SETTOD_SEC_MAX = KTIME_SEC_MAX - TIME_UPTIME_SEC_MAX
>>> datetime.datetime.utcfromtimestamp(TIME_SETTOD_SEC_MAX)
datetime.datetime(2232, 4, 18, 23, 47, 16)

なので ktime_t という型の変数が 64bit で管理されていてかつ nano-second 単位で管理されているために、意図的に設定可能な時刻が 2232 年までに制限されています。

多分 200年後までに ktime_t は 128bitまたはそれより大きい型に変更されると思うので大丈夫でしょう。

参考ソースコード

https://github.com/torvalds/linux/blob/56c244382fdb793986097df8e29f9f9320bc2c60/include/linux/time64.h#L110-L118

static inline bool timespec64_valid_settod(const struct timespec64 *ts)
{
	if (!timespec64_valid(ts))
		return false;
	/* Disallow values which cause overflow issues vs. CLOCK_REALTIME */
	if ((unsigned long long)ts->tv_sec >= TIME_SETTOD_SEC_MAX)
		return false;
	return true;
}

https://github.com/torvalds/linux/blob/56c244382fdb793986097df8e29f9f9320bc2c60/include/linux/time64.h#L23-L41

/* Located here for timespec[64]_valid_strict */
#define TIME64_MAX			((s64)~((u64)1 << 63))
#define TIME64_MIN			(-TIME64_MAX - 1)


#define KTIME_MAX			((s64)~((u64)1 << 63))
#define KTIME_MIN			(-KTIME_MAX - 1)
#define KTIME_SEC_MAX			(KTIME_MAX / NSEC_PER_SEC)
#define KTIME_SEC_MIN			(KTIME_MIN / NSEC_PER_SEC)

/*
 * Limits for settimeofday():
 *
 * To prevent setting the time close to the wraparound point time setting
 * is limited so a reasonable uptime can be accomodated. Uptime of 30 years
 * should be really sufficient, which means the cutoff is 2232. At that
 * point the cutoff is just a small part of the larger problem.
 */
#define TIME_UPTIME_SEC_MAX		(30LL * 365 * 24 *3600)
#define TIME_SETTOD_SEC_MAX		(KTIME_SEC_MAX - TIME_UPTIME_SEC_MAX)

https://github.com/torvalds/linux/blob/56c244382fdb793986097df8e29f9f9320bc2c60/tools/include/linux/time64.h#L10

#define NSEC_PER_SEC	1000000000L

https://github.com/torvalds/linux/blob/56c244382fdb793986097df8e29f9f9320bc2c60/include/linux/ktime.h#L28-L29

/* Nanosecond scalar representation for kernel time values */
typedef s64	ktime_t;

Discussion