VMware でホスト時刻同期を完全にオフにしたい

例えば 2038 年問題のテストしたいとか、ホスト時刻同期をオフにしたいケースは多々あると思う。

これが VMware-Tools(open-vm-tools) なら 公式ドキュメント の仰せの通り、適当なエディタで *.vmx を編集して

tools.syncTime = "FALSE"
time.synchronize.continue = "FALSE"
time.synchronize.restore = "FALSE"
time.synchronize.resume.disk = "FALSE"
time.synchronize.shrink = "FALSE"
time.synchronize.tools.startup = "FALSE"

を設定すればいい、ちなみに本体もツールも最新版を使ってるならば

  • tools.syncTime … 定期的な時刻同期
  • time.synchronize.tools.startup … 初回のみの時刻同期

の2つが設定されていれば他は不要なはずである。

ただし旧バージョンを使ってる場合、仕様がコロコロ変わってるので上記全て設定してもダメなケースもある。 よってちゃんと無効になってるかの確認はとてもだいじ、怖いですねいきあたりばったり仕様やらかすクソ実装は。

ところで現状 vmt(4) については O でも N でもこれらの設定を一切参照しないのだ。 そのため設定画面で変更しようが *.vmx ファイルを直接編集しようがなんら動作に影響を与えない。 なんてこったい!

O ではそもそも vmt(4) に時刻同期機能がない

まず O においての vmt(4)sensor_attach(9) というフレームワークで実装されていて、あくまでホストの時刻情報を取得できるセンサーデバイスという位置づけなんですわ。

It also provides access to the host machine’s clock as a timedelta sensor.

なのでそもそもドライバに時刻同期の機能は無いのだ、やるならば sensorsd(8) からホスト時刻を取得して time(1)で時刻合わせしろって感じ。 訂正、 ntpd(8) すなわち OpenNTPd の時刻ソースとして vmt(4) を指定すればいいのか、次回改めて解説します。

まぁ最初っから完全オフの方がマシだと思う、仮想マシンの機能よりNTPでもGPSでもPTPでも使うべき。

N で追加された実装がとにかく雑ゥ!

そして N に移植された時に、センサーデバイスを扱う統一フレームワークなんて無いしぃ…で 唐突に ドライバ自身が時刻同期を行う という実装に書換えられてしまった、まぁいいけど…

しかもこの時点ではオフにする方法も無く強制的に時刻が同期されてしまう雑さである。

その後 sysctl(8) を使って時刻同期のインターバルを設定する 変更が入った時に、インターバル秒数に0以下を指定すれば無効になるようにはなった。

しかし注意深くコードを読んでいくと、時刻同期はデバイスアタッチ時にも問答無用で実行されるんだよね。

118 void
119 vmt_common_attach(struct vmt_softc *sc)
120 {
...
215 	callout_schedule(&sc->sc_clock_sync_tick,
216 	    mstohz(sc->sc_clock_sync_period_seconds * 1000));
217 
218 	vmt_sync_guest_clock(sc);

しかも callout_schedule(9) で時刻同期用のタイマがまだ / がマウントされる前から回りだしてるから sysctl.conf(5)

machdep.vmt0.clock_sync.period=0

を設定しても無意味なのである、これはひどい。

さらにサスペンドからの復帰時にも問答無用で時刻合わせは行われる。

470 static void
471 vmt_do_resume(struct vmt_softc *sc)
472 {
473 	device_printf(sc->sc_dev, "guest resuming from suspended state\n");
474 
475 	vmt_sync_guest_clock(sc);

つまりはだ、N でホスト時刻同期を無効にしたければ vmt(4) ごと無効にしないとダメってこと。 kernel を作り直してドライバ削るか boot.cfg(5)

userconf=disable vmt*

とでも書いて無効化して、電源管理についてはもうあきらめるしかない。

N の実装の改善案

そもそもするならするで VMware-Tools の行儀作法に従おうよ。

じゃどうするのが正しいのかというと、VMwareで時刻同期の設定を変更すると

  • Time_Synchronize
  • Set_Option synctime
  • Set_Option synctime.period
  • Set_Option time.synchronize.tools.enable
  • Set_Option time.synchronize.tools.startup
  • Set_Option time.synchronize.tools.startup.backward

というイベントが TCLO 側の RPC でゲストに通知されることになってるのだ。 TCLOって何?は 過去回 で簡単に説明したので省略。

以下、それぞれのイベントハンドラ内の処理をクッソ雑に実装したものを貼る。

つーかイベントハンドラは if 文じゃなくてテーブルに書換えた方が、O はそう修正してるけど。

Time_Synchronize [Boolean] を実装する

引数が1(true)ならば時刻合わせを実行する。

--- vmt_subr.c.orig	2024-02-04 17:27:53.322098853 +0900
+++ vmt_subr.c	2024-02-04 17:28:25.794986070 +0900
@@ -687,6 +687,10 @@ vmt_tclo_tick(void *xarg)
 				sc->sc_rpc_error = 1;
 			}
 		}
+	} else if (strcmp(sc->sc_rpc_buf, "Time_Synchronize 1") == 0) {
+		vmt_sync_guest_clock(sc);
+		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
+			sc->sc_rpc_error = 1;
 	} else {
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_ERROR) != 0) {
 			device_printf(sc->sc_dev, "error sending unknown command reply\n");

Set_Option synctime [Boolean] を実装する

synctime.period で指定された秒数をインターバルとして時刻同期を行う。

--- vmtvar.h.orig	2024-02-01 18:40:19.583944132 +0900
+++ vmtvar.h	2024-02-04 15:31:24.956456368 +0900
@@ -72,6 +72,8 @@ struct vmt_softc {
 	struct callout		sc_tick;
 	struct callout		sc_tclo_tick;
 
+#define VMT_CLOCK_SYNC		0x1
+	unsigned int		sc_clock_sync_flags;
 #define VMT_CLOCK_SYNC_PERIOD_SECONDS 60
 	int			sc_clock_sync_period_seconds;
 	struct callout		sc_clock_sync_tick;
--- vmt_subr.c.orig	2024-02-04 17:28:25.794986070 +0900
+++ vmt_subr.c	2024-02-04 17:30:46.425072766 +0900
@@ -164,6 +164,7 @@ vmt_common_attach(struct vmt_softc *sc)
 	callout_init(&sc->sc_tclo_tick, 0);
 	callout_init(&sc->sc_clock_sync_tick, 0);
 
+	sc->sc_clock_sync_flags = 0;
 	sc->sc_clock_sync_period_seconds = VMT_CLOCK_SYNC_PERIOD_SECONDS;
 
 	rv = vmt_sysctl_setup_root(self);
@@ -208,8 +209,6 @@ vmt_common_attach(struct vmt_softc *sc)
 	callout_schedule(&sc->sc_tclo_tick, hz);
 
 	callout_setfunc(&sc->sc_clock_sync_tick, vmt_clock_sync_tick, sc);
-	callout_schedule(&sc->sc_clock_sync_tick,
-	    mstohz(sc->sc_clock_sync_period_seconds * 1000));
 
 	vmt_sync_guest_clock(sc);
 
@@ -241,7 +240,8 @@ vmt_common_detach(struct vmt_softc *sc)
 	callout_halt(&sc->sc_tclo_tick, NULL);
 	callout_destroy(&sc->sc_tclo_tick);
 
-	callout_halt(&sc->sc_clock_sync_tick, NULL);
+	if (sc->sc_clock_sync_flags & VMT_CLOCK_SYNC)
+		callout_halt(&sc->sc_clock_sync_tick, NULL);
 	callout_destroy(&sc->sc_clock_sync_tick);
 
 	if (sc->sc_rpc_buf)
@@ -332,11 +332,17 @@ vmt_sysctl_update_clock_sync_period(SYSC
 		return error;
 
 	if (sc->sc_clock_sync_period_seconds != period) {
-		callout_halt(&sc->sc_clock_sync_tick, NULL);
+		if (sc->sc_clock_sync_flags & VMT_CLOCK_SYNC) {
+			callout_halt(&sc->sc_clock_sync_tick, NULL);
+			sc->sc_clock_sync_flags &= ~VMT_CLOCK_SYNC;
+		}
 		sc->sc_clock_sync_period_seconds = period;
-		if (sc->sc_clock_sync_period_seconds > 0)
+		if (sc->sc_clock_sync_period_seconds > 0) {
 			callout_schedule(&sc->sc_clock_sync_tick,
 			    mstohz(sc->sc_clock_sync_period_seconds * 1000));
+			sc->sc_clock_sync_flags |= VMT_CLOCK_SYNC;
+		} else
+			sc->sc_clock_sync_period_seconds = VMT_CLOCK_SYNC_PERIOD_SECONDS;
 	}
 	return 0;
 }
@@ -691,6 +697,23 @@ vmt_tclo_tick(void *xarg)
 		vmt_sync_guest_clock(sc);
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
 			sc->sc_rpc_error = 1;
+	} else if (strncmp(sc->sc_rpc_buf, "Set_Option synctime ", 20) == 0) {
+		if (sc->sc_rpc_buf[20] == '0') {
+			if (sc->sc_clock_sync_flags & VMT_CLOCK_SYNC) {
+				/* synctime on -> off */
+				callout_halt(&sc->sc_clock_sync_tick, NULL);
+				sc->sc_clock_sync_flags &= ~VMT_CLOCK_SYNC;
+			}
+		} else {
+			if ((sc->sc_clock_sync_flags & VMT_CLOCK_SYNC) == 0) {
+				/* synctime off -> on */
+				callout_schedule(&sc->sc_clock_sync_tick,
+				    mstohz(sc->sc_clock_sync_period_seconds * 1000));
+				sc->sc_clock_sync_flags |= VMT_CLOCK_SYNC;
+			}
+		}
+		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
+			sc->sc_rpc_error = 1;
 	} else {
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_ERROR) != 0) {
 			device_printf(sc->sc_dev, "error sending unknown command reply\n");

sysctl(8) まわりはこれまでの挙動との互換性を保ちたかったので安易に MIB 増やさなかった。 でも別に増やしてもいいんだけどね、どうせ使ってるやつなんて世界で3人も居ねえんだから。

Set_Option synctime.period [Integer] を実装する

引数を synctime での定期的な時刻同期のインターバルとして設定する。

--- vmt_subr.c.orig	2024-02-04 17:30:46.425072766 +0900
+++ vmt_subr.c	2024-02-04 17:33:04.888500711 +0900
@@ -523,7 +523,7 @@ vmt_tclo_tick(void *xarg)
 	struct vmt_softc *sc = xarg;
 	u_int32_t rlen;
 	u_int16_t ack;
-	int delay;
+	int delay, period;
 
 	/* By default, poll every second for new messages */
 	delay = hz;
@@ -714,6 +714,21 @@ vmt_tclo_tick(void *xarg)
 		}
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
 			sc->sc_rpc_error = 1;
+	} else if (strncmp(sc->sc_rpc_buf, "Set_Option synctime.period ", 27) == 0) {
+		period = (int)strtoll(&sc->sc_rpc_buf[27], NULL, 10);
+		if (period <= 0)
+			period = VMT_CLOCK_SYNC_PERIOD_SECONDS;
+		if (sc->sc_clock_sync_period_seconds != period) {
+			if (sc->sc_clock_sync_flags & VMT_CLOCK_SYNC) {
+				callout_halt(&sc->sc_clock_sync_tick, NULL);
+				sc->sc_clock_sync_period_seconds = period;
+				callout_schedule(&sc->sc_clock_sync_tick,
+				    mstohz(sc->sc_clock_sync_period_seconds * 1000));
+			} else
+				sc->sc_clock_sync_period_seconds = period;
+		}
+		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
+			sc->sc_rpc_error = 1;
 	} else {
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_ERROR) != 0) {
 			device_printf(sc->sc_dev, "error sending unknown command reply\n");

Set_Option time.synchronize.tools.enable [Boolean]

引数が1(true)なら時刻合わせを実行する。

--- vmtvar.h.orig	2024-02-04 16:16:48.511301653 +0900
+++ vmtvar.h	2024-02-04 16:18:49.409033677 +0900
@@ -73,6 +73,7 @@ struct vmt_softc {
 	struct callout		sc_tclo_tick;
 
 #define VMT_CLOCK_SYNC		0x1
+#define VMT_CLOCK_SYNC_ENABLE	0x2
 	unsigned int		sc_clock_sync_flags;
 #define VMT_CLOCK_SYNC_PERIOD_SECONDS 60
 	int			sc_clock_sync_period_seconds;
--- vmt_subr.c.orig	2024-02-04 17:33:04.888500711 +0900
+++ vmt_subr.c	2024-02-04 17:34:25.844694925 +0900
@@ -729,6 +729,21 @@ vmt_tclo_tick(void *xarg)
 		}
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
 			sc->sc_rpc_error = 1;
+	} else if (strncmp(sc->sc_rpc_buf, "Set_Option time.synchronize.tools.enable ", 41) == 0) {
+		if (sc->sc_rpc_buf[41] == '0') {
+			if (sc->sc_clock_sync_flags & VMT_CLOCK_SYNC_ENABLE) {
+				/* time.synchronize.tools.enable on -> off */
+				sc->sc_clock_sync_flags &= ~VMT_CLOCK_SYNC_ENABLE;
+			}
+		} else {
+			if ((sc->sc_clock_sync_flags & VMT_CLOCK_SYNC_ENABLE) == 0) {
+				/* time.synchronize.tools.enable off -> on */
+				vmt_sync_guest_clock(sc);
+				sc->sc_clock_sync_flags |= VMT_CLOCK_SYNC_ENABLE;
+			}
+		}
+		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
+			sc->sc_rpc_error = 1;
 	} else {
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_ERROR) != 0) {
 			device_printf(sc->sc_dev, "error sending unknown command reply\n");

Set_Option time.synchronize.tools.startup [Boolean]

引数が1(true)かつ一度も時刻合わせを行ってなければ実行する。

--- vmtvar.h.orig	2024-02-04 16:23:50.073672598 +0900
+++ vmtvar.h	2024-02-04 16:24:57.282052134 +0900
@@ -74,6 +74,7 @@ struct vmt_softc {
 
 #define VMT_CLOCK_SYNC		0x1
 #define VMT_CLOCK_SYNC_ENABLE	0x2
+#define VMT_CLOCK_SYNC_STARTUP	0x4
 	unsigned int		sc_clock_sync_flags;
 #define VMT_CLOCK_SYNC_PERIOD_SECONDS 60
 	int			sc_clock_sync_period_seconds;
--- vmt_subr.c.orig	2024-02-04 17:34:25.844694925 +0900
+++ vmt_subr.c	2024-02-04 17:35:43.952915960 +0900
@@ -744,6 +744,16 @@ vmt_tclo_tick(void *xarg)
 		}
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
 			sc->sc_rpc_error = 1;
+	} else if (strncmp(sc->sc_rpc_buf, "Set_Option time.synchronize.tools.startup ", 42) == 0) {
+		if (sc->sc_rpc_buf[42] != '0') {
+			if ((sc->sc_clock_sync_flags & VMT_CLOCK_SYNC_STARTUP) == 0) {
+				/* time.synchronize.tools.start off -> on */
+				vmt_sync_guest_clock(sc);
+				sc->sc_clock_sync_flags |= VMT_CLOCK_SYNC_STARTUP;
+			}
+		}
+		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
+			sc->sc_rpc_error = 1;
 	} else {
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_ERROR) != 0) {
 			device_printf(sc->sc_dev, "error sending unknown command reply\n");

Set_Option time.synchronize.tools.startup.backward [Boolean]

ホスト時刻に同期する際にゲスト時刻より過去になるのを許容する。

--- vmtvar.h.orig	2024-02-04 16:32:20.605750158 +0900
+++ vmtvar.h	2024-02-04 16:33:05.152814049 +0900
@@ -75,6 +75,7 @@ struct vmt_softc {
 #define VMT_CLOCK_SYNC		0x1
 #define VMT_CLOCK_SYNC_ENABLE	0x2
 #define VMT_CLOCK_SYNC_STARTUP	0x4
+#define VMT_CLOCK_SYNC_BACKWARD	0x8
 	unsigned int		sc_clock_sync_flags;
 #define VMT_CLOCK_SYNC_PERIOD_SECONDS 60
 	int			sc_clock_sync_period_seconds;
--- vmt_subr.c.orig	2024-02-04 17:35:43.952915960 +0900
+++ vmt_subr.c	2024-02-04 17:36:51.839369643 +0900
@@ -754,6 +754,13 @@ vmt_tclo_tick(void *xarg)
 		}
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
 			sc->sc_rpc_error = 1;
+	} else if (strncmp(sc->sc_rpc_buf, "Set_Option time.synchronize.tools.backward ", 43) == 0) {
+		if (sc->sc_rpc_buf[43] == '0')
+			sc->sc_clock_sync_flags &= ~VMT_CLOCK_SYNC_BACKWARD;
+		else
+			sc->sc_clock_sync_flags |= VMT_CLOCK_SYNC_BACKWARD;
+		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_OK) != 0)
+			sc->sc_rpc_error = 1;
 	} else {
 		if (vm_rpc_send_str(&sc->sc_tclo_rpc, VM_RPC_REPLY_ERROR) != 0) {
 			device_printf(sc->sc_dev, "error sending unknown command reply\n");

実装はダミーである、そもそも現状の N HEAD の実装だと

408 static void
409 vmt_sync_guest_clock(struct vmt_softc *sc)
410 {
411 	struct vm_backdoor frame;
412 	struct timespec ts;
413 
414 	memset(&frame, 0, sizeof(frame));
415 	frame.eax = VM_MAGIC;
416 	frame.ecx = VM_CMD_GET_TIME_FULL;
417 	frame.edx = VM_REG_CMD(0, VM_PORT_CMD);
418 	vm_cmd(&frame);
419 
420 	if (__SHIFTOUT(frame.eax, VM_REG_WORD_MASK) != 0xffffffff) {
421 		ts.tv_sec = ((uint64_t)(
422 		    __SHIFTOUT(frame.esi, VM_REG_WORD_MASK) << 32)) |
423 		    __SHIFTOUT(frame.edx, VM_REG_WORD_MASK);
424 		ts.tv_nsec = __SHIFTOUT(frame.ebx, VM_REG_WORD_MASK) * 1000;
425 		tc_setclock(&ts);
426 	}
427 }

ちゅーかんじで、ドキュメントすらない tc_setclock() という API で時刻が逆行しようがお構いなしに同期してるので、常時 time.synchronize.tools.startup.backwardtrue であるといえる。

この辺は次回改めて正しい実装はどうすればいいのか説明する予定。

ついでに不要な強制時刻同期にさよなら

デバイスアタッチ時とサスペンドからの復帰の強制同期は不要なので消す。 もし必要があったとしても

  • reset イベントハンドラで
  • synctime の値に従う

必要があるかと。

--- vmt_subr.c.back	2024-02-04 18:17:35.630901476 +0900
+++ vmt_subr.c	2024-02-04 18:18:33.477167272 +0900
@@ -210,8 +210,6 @@ vmt_common_attach(struct vmt_softc *sc)
 
 	callout_setfunc(&sc->sc_clock_sync_tick, vmt_clock_sync_tick, sc);
 
-	vmt_sync_guest_clock(sc);
-
 	return;
 
 free:
@@ -474,8 +472,6 @@ vmt_do_resume(struct vmt_softc *sc)
 {
 	device_printf(sc->sc_dev, "guest resuming from suspended state\n");
 
-	vmt_sync_guest_clock(sc);
-
 	/* force guest info update */
 	sc->sc_hostname[0] = '\0';
 	sc->sc_set_guest_os = 0;

次回

いちおうこれで時刻同期について VMware で設定したポリシーにしたがうようになったはずである(backward 以外は)。

ワイの筆力が足りると思えんけどディープな時刻合わせの話に突入する、 めんどくさくなって別の話してる可能性が99%くらいあるけど。