再・令和最新式 CardBus スロット付ノート PC 買いました

このシリーズの初回「令和最新版 CardBus スロットのあるノート PC を探せ」で Let’s Note CF-S10 を買ったという話をしたのだけど、これは Core i 第 2 世代つまり Sandy Bridge おじさんマシンなのだ。 これだと AVX2 命令の必要な Ableton Live 12 Lite が動かない問題があるのだが、それは 11 にダウングレードも可能なので別段気にしてなかった。

ところが来年予定されてる Windows 11 27H2 の雲行きが怪しいのである、どうも AVX2 命令を必須にして第 3 世代 Ivy Bridge 以前の CPU 切り捨てするなんて噂があるのよな。 すでに第 6 世代 Skylake はサポート対象外なのでいつやられても不思議じゃねえ!

当時候補として Fujitsu LIFEBOOK シリーズも候補にしてたのだが CardBus スロットはオプション扱いなのでレア過ぎて入手は難しいと思ってたんだよね。 ところが記事を書いたことによる引き寄せ効果(頭スピ系かよ)なのか AVX2 対応の第 4 世代 Haswell Core i5-4310M 搭載の Lifebook A574 CardBus スロット付きに出会ってしまったのである。

これ CF-S10 よりも 2.5 倍くらい高く貧乏な俺は躊躇したのだが、もう次出会える可能性は限りなく低いから金も無いのに買っちゃった。 高いのは SSD が抜き取られてないからで SSD 買ったと思えばまぁいいか…でも元はノーブランドの安物で 120GB だしなぁ、滅びろ AI データセンター。

メモリも DDR3 使えるので手持ちパーツが流用できてありがたい、というか DDR3 ですら倍くらいに値上がりしてる狂気の時代、DDR4 だったらまず買えねえ。

ちなみに LIFEBOOK シリーズは第 7/8 世代搭載の A577/747/748 までほとんどデザインの変更が無く CardBus スロットの部品も共通っぽいので移植もおそらくできるはず。 第 4 世代すら切り捨てられたらここらへんのシステムボードと交換だけで延命できるかも、USBポートが増えてる分ケースに加工必要かもしれんが。

なお CardBus スロットありなしモデルでボトムは別成型パーツになってるのでスロット無しの A577/747/748 に A574 から抜いた CardBus スロットつけるのは穴あけ加工が必要で大変だと思われる。 どうせこんなレガシー機能は数出ないからいちいちスロット部を別パーツで塞ぐ構造ににするよか安いという判断かね。

まぁ第 4 世代が完全足切りになる頃って DDR4 が安くなるより物価高で餓死してるか80年前と同じく侵略戦争に加担させられどっかで野垂れ死にだから考えなくていいや。

これが自分が買う最後の PC になるだろうと覚悟はしてるんだけどやっぱ 15.6 インチはデカ過ぎるしデザインがもう壊滅してるしこれで最後かぁ…となっている。 最後くらいもっとカッコいいマシン欲しかったな、まぁ ThinkPad もアレだしもう欲しくなるようデザインのノート PC 無いんだけどね。

令和最新式 Unix C がちょっと書けるだけの俺による Visual C++/Win32 API マルチスレッドプログラミング入門

前回までに

  1. レジストリの変更を監視するスレッドから投げるユーザー定義イベント (0x445)
  2. 新たなカーネルストリーミングデバイス (KSCATEGORY_CAPTURE) が装着された時のデバイス変更通知 (WM_DEVCECHANGED + DBT_DEVTYP_DEVICEINTERFACE)
  3. 既存の E-MU オーディオデバイスが取り外された時のデバイス変更通知 (WM_DEVCECHANGED + DBT_DEVTYP_HANDLE)

という 3 つのイベントを発火させるコードを復元したのだけど、このうち 1. のイベントハンドドラについては MFC のメッセージマップ機能で

#define WM_USER_REGISTRY_CHANGED	(WM_USER+0x45)

BEGIN_MESSAGE_MAP(CCtHelperDlg, CDialog)
	ON_MESSAGE(WM_USER_REGISTRY_CHANGED, &CCtHelperDlg::OnUserRegistryChanged)
END_MESSAGE_MAP()

afx_msg LRESULT CCtHelperDlg::OnUserRegistryChanged(WPARAM wParam, LPARAM lParam)
{
	size_t i;

	/* wParam may be NULL */
	/* lParam may be NULL */

	for (i = 0; i < arraycount(keys); ++i) {
		if (hkeys[i] != NULL) {
			RegCloseKey(hkeys[i]);
			hkeys[i] = NULL;
		}
		if (events[i] != NULL) {
			CloseHandle(events[i]);
			events[i] = NULL;
		}
	}
	if (registryWatchdogThread != NULL) {
		CloseHandle(registryWatchdogThread);
		registryWatchdogThread = NULL;
	}
	return ERROR_SUCCESS;
}

ちゅー感じのコードが呼ばれることがわかった、 Ghidra のザルな解析結果だとメッセージマップ使ってると CCtHelperDlg の仮想関数テーブルから追跡できねえので特定に時間がかかってしまった。 そもそもクラスメソッドとすら判定されねえし、IDA Pro ならもっと C++ 解析マシなんすかね。

俺も Visual C++/MFC での Windows プログラミング経験が皆無だから大目に見て欲しい。

そんでだな、過去回で再現したイベント送信側のコードを若干修正したものを再掲すると

#define arraycount(array)	(sizeof(array)/sizeof(array[0]))

static LPTSTR keys[3] = {
	_T("Software\\Creative Tech\\Cthelper\\PCI&VEN_1102&DEV_0002\\Enable"),
	_T("Software\\Creative Tech\\Cthelper\\PCI&VEN_1102&DEV_0004\\Enable"),
	_T("Software\\Creative Tech\\Cthelper\\PCI&VEN_1102&DEV_0008\\Enable")
};
BOOL canceled = FALSE;
static HKEY hkeys[arraycount(keys)];
static HANDLE events[arraycount(keys)];

DWORD WINAPI RegistryChangedWatchdog(LPVOID param)
{
	DWORD ret;
	HWND window;
	size_t i;

	for (i = 0; i < arraycount(keys); ++i) {
		hkeys[i] = 0;
		events[i] = CreateEvent(0, TRUE, FALSE, NULL);
	}
	for (;;) {
		for (i = 0; i < arraycount(keys); ++i) {
			if (RegOpenKey(HKEY_CURRENT_USER, subkeys[i], &hkeys[i]) == ERROR_SUCCESS)
				RegNotifyChangeKeyValue(hkeys[i], FALSE, REG_NOTIFY_CHANGE_NAME|REG_NOTIFY_CHANGE_ATTRIBUTES|REG_NOTIFY_CHANGE_LAST_SET, events[i], TRUE);
		}
		ret = MsgWaitForMultipleObjects(arraycount(keys), events, FALSE, INFINITE, QS_ALLEVENTS|QS_RAWINPUT);
		assert(ret < arraycount(keys));
		assert(events[ret] != NULL);
		if (canceled == TRUE)
			break;
		window = FindWindow(NULL, ident);
		if (window != NULL)
			SendNotifyMessage(window, WM_USER_REGISTRY_CHANGED, NULL, NULL);
		ResetEvent(events[ret]);
	}
	return 0;
}

BOOL CCtHelperDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
…
	registryWatchdogThread = CreateThread(NULL, 0, &RegistryChangedWatchdog, NULL, 0, NULL);
	return TRUE;
}

となってるので、コードの意図としては 3 つのうちのひとつでもレジストリ変更が検出されたら レジストリハンドルとイベントハンドルそしてスレッドハンドルを解放して終了したいのだと思われる。

ところでこのコード、もう感のいい人はお気づきだろうけど(うおwww壁と会話しとるwww)、ジャンジャンバリバリ大解放してるのにそもそもスレッドを終了してなくて大草原。 もしかしてスレッドハンドルをクローズすればスレッドも終了すると思っている可能性…?

その結果 RegistryChangedWatchdog と OnUserRegistryChanged は非同期処理であるため MsgWaitForMultipleObjects で待ってるイベントがメインスレッド側で解放されてしまう競合状態が発生しますわな。 これまでも目を疑うコードだらけだったがさすがにこれは太く堅い棒で思いっきり殴らないとアカンわ(ゴンッ)。

いちおうイベントループを終了させるためグローバル変数 canceled なんてものがご用意されているのだが、呼んでる場所が MsgWaitForMultipleObjects の前でなく後になってるせいでガードにならない上に、そもそも誰もこの値を TRUE にしてないのである。 というか普通に SendNotifyMessage した後にイベントループから脱出するでいいよね、しかもウインドウにレジストリハンドルとイベントハンドルを解放してと通知する意味も無いし。

まぁいいや、最小限のコード修正で済ますか。

diff --git a/CtHelper/CtHelperDlg.cpp b/CtHelper/CtHelperDlg.cpp
index cadad0e..c029a5f 100644
--- a/CtHelper/CtHelperDlg.cpp
+++ b/CtHelper/CtHelperDlg.cpp
@@ -93,7 +93,7 @@ static DWORD WINAPI RegistryChangedWatchdog(LPVOID param)
 		hkeys[i] = NULL;
 		events[i] = CreateEvent(0, TRUE, FALSE, NULL);
 	}
-	for (;;) {
+	while (canceled == FALSE) {
 		for (i = 0; i < arraycount(keys); ++i) {
 			if (RegOpenKey(HKEY_CURRENT_USER, keys[i], &hkeys[i]) == ERROR_SUCCESS)
 				RegNotifyChangeKeyValue(hkeys[i], FALSE, REG_NOTIFY_CHANGE_NAME|REG_NOTIFY_CHANGE_ATTRIBUTES|REG_NOTIFY_CHANGE_LAST_SET, events[i], TRUE);
@@ -101,11 +101,11 @@ static DWORD WINAPI RegistryChangedWatchdog(LPVOID param)
 		ret = MsgWaitForMultipleObjects(arraycount(keys), events, FALSE, INFINITE, QS_ALLEVENTS|QS_RAWINPUT);
 		assert(ret < arraycount(keys));
 		assert(events[ret] != NULL);
-		if (canceled == TRUE)
-			break;
 		window = FindWindow(NULL, ident);
-		if (window != NULL)
+		if (window != NULL) {
 			SendNotifyMessage(window, WM_USER_REGISTRY_CHANGED, NULL, NULL);
+			canceled = TRUE;
+		}
 		ResetEvent(events[ret]);
 	}
 	return 0;

とでもしときゃ競合状態は回避できるはず、うんやっぱループ自体が要らねえわリソース解放を無意味にウィンドウに任せるにしても

DWORD WINAPI RegistryChangedWatchdog(LPVOID param)
{
	DWORD ret;
	HWND window;
	size_t i;

	assert(param != NULL);

	window = (HWND)param;
	for (i = 0; i < arraycount(keys); ++i) {
		hkeys[i] = 0;
		events[i] = CreateEvent(0, TRUE, FALSE, NULL);
		if (RegOpenKey(HKEY_CURRENT_USER, subkeys[i], &hkeys[i]) == ERROR_SUCCESS)
			RegNotifyChangeKeyValue(hkeys[i], FALSE, REG_NOTIFY_CHANGE_NAME|REG_NOTIFY_CHANGE_ATTRIBUTES|REG_NOTIFY_CHANGE_LAST_SET, events[i], TRUE);

	}
	ret = MsgWaitForMultipleObjects(arraycount(keys), events, FALSE, INFINITE, QS_ALLEVENTS|QS_RAWINPUT);
	assert(ret < arraycount(keys));
	assert(events[ret] != NULL);
	SendNotifyMessage(window, WM_USER_REGISTRY_CHANGED, NULL, NULL);
	ResetEvent(events[ret]);
	return 0;
}

BOOL CCtHelperDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
…
	registryWatchdogThread = CreateThread(NULL, 0, &RegistryChangedWatchdog, this, 0, NULL);
	return TRUE;
}

でええわクソが、そもそも過去回でも書いたけど this を param として渡しとけば FindWindow してウィンドウハンドル探す必要無いんすわ、ピタゴラスイッチかお前は。

非同期処理というものを理解できない自称プログラマにメッセージドリブンなコード書かせてはならない(戒め)、だけどメッセージって何かカッコいい!が当時の流行だからしゃーない。

まぁ Windows なんてセキュリティなんて概念の希薄な時代に生まれた OS が無駄に長生きし過ぎたよね、それより古臭い *BSD のコード弄ってた自分は棚に上げるけど。

にもかかわらず今の時代もスキルの低い WEB 屋ほど async! async! 叫んでる世の中という地獄である。 そういう WEB 屋が蔓延った結果がここ数日続々と俺のメールボックスに届く個人情報が流出したというメールなのである、過去の某流出案件でとっくにフル開示済だから今更だけどな、インターネットごと滅べカス業者ども。

そいや今話題沸騰の PHP2Shell も IT 業界名物の驚き屋おじさんが OpenAI にしか発見できなかった数千件のバグうおおおおしてるけど、大多数は 20 年前からあった Coverity のような静的解析ツールで潰せたバグじゃねーのと思ってるんだがどうなんすかね、あの界隈だもんな。

話を戻す、そしてここまできてとても恐ろしいことに気づきませんか?このレジストリ変更監視スレッドって実質何もしてなくね?である。 なんと OnUserRegistryChanged イベントハンドラ内では CCtHelperDlg の挙動を変えるような副作用を何一つ生んでないのだ。 つまりこいつらはなんら必要のないコードなのである、うーんマジで頭痛がしてきた。

レジストリの書換自体はそもそもメインスレッドでこの監視スレッドを産む前にとっくに終わってるのよね。 だとすれば CtHelper.exe 以外の外部要因でレジストリを変更されたことを検知したくて監視してるはずなんだけど、検知しても「何もしない」のだ。 どこかに見落としがあるのかと思ったのだがマジで何も無さそうである。

ということでまた無駄な作業をしてしまった…クソが…

RegisterDeviceNotification で登録したデバイス着脱イベントハンドラも見つからない

のである、しかしこれが実装されてないとデバイス取り外した時に E-MU PatchMix DSP が自動で終了する挙動が説明つかなくなるのだ。

だが前回復元した RunPatchMixDSP のコードを読み返すとそれも怪しくなってきた、一部抜粋すると

STARTUPINFO startupInfo;
ZeroMemory(&startupInfo, sizeof(startupInfo));
startupInfo.cb = sizeof(startupInfo);

PROCESS_INFORMATION processInfomation;
processInfomation.hProcess = NULL;
processInfomation.hThread = NULL;
processInfomation.dwProcessId = 0;
processInfomation.dwThreadId = 0;
...
TCHAR run[MAX_PATH];
...
if (CreateProcess(NULL, run, NULL, NULL, 0, 0, NULL, NULL, &startupInfo, &processInfomation) != 0) {
	CloseHandle(processInfomation.hProcess);
	CloseHandle(processInfomation.hThread);
	ret = S_OK;
}

run はレジストリから取得した PatchMixDSP 実行ファイルのパス、うーんこのデバイス着脱を通知しようにもプロセス間通信に必要なハンドル閉じてんじゃねーか。

ただまぁ Win32 API なら PatchMixDSP のウインドウ名が判ってりゃ FindWindow でウィンドウハンドル取得すればセキュリティ上の制限はあるもののメッセージは投げられるので閉じてしまってもプロセス間通信は可能である。 やることは WM_CLOSE 投げるだけで良さそうだし制限には引っかからんだろ。

だがどこにもそれっぽいコードが無いんだよね。

つーわけで CtHelper.exe はスタートアップ時に PatchMixDSP を起動するだけのスタートアップフォルダのショートカットレベルの仕事しかしてない疑惑が出てきた。

でも PatchMixDSP は自分自身でデバイスが外されたかを監視し自分を終了させることは可能だけど、デバイスを装着した時に勝手に起動することはできないので謎は深まるばかりである。

ということでこの謎はまた次以降ということで。