The Man Who Fell From The Wrong Side Of The Sky:2019年5月分

2019/5/1(Wed)

[Windows] 新元号対応更新プログラム(その2)

どうも100年以上の和暦を表示できないバグがある?>Windowsの日付書式指定文字。

左の和暦表示(ggy)だと有効期間100年の証明書の期限が今年で切れるかのように見える、実際は右の西暦表示(yyyy)通りに令和101年で表示されないとおかしい。

まぁさすがに公文書でも西暦併記の時代だし100年契約したつもりが1年になってトラブルはおきないよね!(ニッコリ)

2019/5/2(Thu)

[Windows] オレオレ証明書によるコード署名の手順 (その2 - certreq編)

先日の記事の続き、今回はcertreqを使用するのだけどそもそもcert「req」というくらいで認証局(CA)に対して署名要求(CSR)を行うためのコマンドなので、コード署名用証明書の作成目的で使うのは本来の用途ではないことに注意。

前回のmakecertを使った方法では

という形をとったのだけど、この方法のメリットは

というとこですかね、自宅内でそんなものが必要になるかはどうかは知らんけど(いわゆる誤家庭ってやつ)。

しかし今回は

することになるのでこのコマンド単体では他の鍵に署名を行うことができないのだ(縛りプレイ)。 よって今回は認証局を置かずにいきなりコード署名用証明書を発行し、その公開鍵をルート証明書として登録してもらうという形になるよ。

そんでコマンドに渡すパラメーターは引数オプションではなくinfファイル形式による応答ファイルが必要になる。

certreq -new apps.inf apps.req

と叩いて署名要求を作成する。

まず正しい使い方、認証局に送りつけるCSRを作成するだけなら

[Version]
Signature="$Windows NT$"

[NewRequest]
Subject="CN=Example Apps,O=Example Corp.,OU=Example Div.,C=JP,DC=com,DC=example"
KeySpec=AT_SIGNATURE
KeyUsage="CERT_DIGITAL_SIGNATURE_KEY_USAGE|CERT_KEY_ENCIPHERMENT_KEY_USAGE"
RequestType=CMC

[EnhancedKeyUsageExtension]
OID=1.3.6.1.5.5.7.3.1

くらいで問題ないはず、これで作成された署名要求(apps.req)を認証局に送りつければコード署名用証明書を発行してくれる。認証局の手順書にしたがって作成してくれ。

今回はこれを書換えて自己署名証明書を作るようにする。

[Version]
Signature="$Windows NT$"

[NewRequest]
Subject="CN=Example Apps,O=Example Corp.,OU=Example Div.,C=JP,DC=com,DC=example"
RequestType=Cert
Exportable=true
ExportableEncrypted=true
HashAlgorithm=SHA256
KeyAlgorithm=ECDSA_P256
KeySpec=AT_SIGNATURE
KeyUsage="CERT_DIGITAL_SIGNATURE_KEY_USAGE|CERT_KEY_ENCIPHERMENT_KEY_USAGE"
SMIME=false
NotBefore="5/01/2019 12:00 AM"
NotAfter="12/31/2039 11:59 PM"
FriendlyName="Example Apps"

[EnhancedKeyUsageExtension]
OID=1.3.6.1.5.5.7.3.3

ポイントは以下の通り

あたりですかね、作成された署名要求は要らないので捨ててヨシ。

makecertに対するメリットは

あたり、ただし問題も多くて

というかんじ、またしても帯に短し襷にも短しじゃねーか。

[追記] certreqの場合でもCAかEnd Entityかの基本制限をinfのExtensionセクションに記述することで追加できますな。

ただしやっぱりWindows 8以降のcertreqでないとパースエラーで死ぬので意味ナッシングですが。

ちなみに証明書の管理(Certmgr)のGUIからもcertreq同様に署名要求が可能なんだけど、こいつの「カスタム要求の作成」ウィザードを使えば

のだけどね…

@次回予告

次はMicrosoftが推奨するPowerShellを使った方法、PowerShell歴もだいぶ長いはずなんだがまるで覚えられなくて何年た経っても初心者気分やぞ(老い)。

そして可能なら実際に作成したコード署名用証明書でスクリプトやドライバなどにコード署名してみる(さすがに次々回になるかな…)

2019/5/3(Fri)

[i18n] POSIX locale LC_TIME ERAそしてstrptime(3)の"%E"書式指定子は欠陥APIだったね

いまさら .NET Framework 用の日本の新元号対応更新プログラムの概要読んでたんだけど、 先日書いた

というややこしい実装になってるのは、すでに存在する「平成99年1月1日」みたいな文書のパースのためにやむをえないのかなぁと思った。 でも結局これだと2019年5月1日までに「平成99年1月1日」みたいな文書はすべて訂正する必要があるので、やっぱりあんま意味ないと思う。

なお POSIX localeのLC_TIME ERAフィールドとそれを内部で使ってる strptime(3)の"%Ec"書式指定子は、新元号をデータベースに追加した瞬間「平成99年1月1日」はパースできなくなるので欠陥APIだよなぁという思い、なんせPOSIX localeだって今回が改元初経験のはずなので許してクレメンス。

ただこれはstrptime(3)の実装をちょいちょい修正して

#include <locale.h>
#include <time.h>
#include <stdio.h>
#include <string.h>

int
main(void)
{
	const char *s[] = {
		"西暦2019年5月1日",
		"明治152年5月1日",
		"大正108年5月1日",
		"昭和94年5月1日",
		"平成31年5月1日",
		"令和元年5月1日",
		NULL
	}, **p;
	struct tm tm;
	char buf[1024];

	setlocale(LC_ALL, "");
	for (p = s; *p; ++p) {
		memset(&tm, 0, sizeof(struct tm));
		strptime(*p, "%Ec", &tm);
		strftime(&buf[0], sizeof(buf), "%Ec", &tm);
		puts(buf);
	}
}

を実行するとstrptime(3)での変換はすべて成功し、strftime(3)側の変換は

令和元年5月1日
令和元年5月1日
令和元年5月1日
令和元年5月1日
令和元年5月1日
令和元年5月1日

となるよう実装するのはわりと簡単にできるよね、エラーにしたい人の為には懐かしの環境変数POSIXLY_CORRECTあたりで挙動切替できるようにしときゃええやろ(鼻ホジ)。

だって年月の計算の方はstruct tmで-1月や13月に-1日や32日を扱うわけでさ、これやっぱカタテ=オチだったと思うよ。 ちなみにLC_TIME ERAは 西暦0年問題まで考慮して設計されてるので、令和0年が無くて令和-1年なんてのも正しく扱えるはずだがlocaledefデータベースは修正要るかも。

ンまぁ*BSDはどいつもこいつもstrftime/strptimeどちらも%Eなんぞ実装されてなかったはずなので、規格に欠陥があろうがなかろうが関係ない話であろう。 ワイの 完成目前のはずのコードもバックアップデータのどこに埋もれてんのか探すのめんどいし作業するとPTSDも発症するので永遠に日の目をみることはないだろう。

あとCygwinはWindows 7に新元号対応パッチ適用しても相変わらず平成なので、APIからひっぱってこずに自前で定義してるのね。 手元に転がってた何時のとも知れぬsnapshotのソース読んだらwinsup/cygwin/lc_era.hに定義があるようなので パッチ書いた、あとは好きにすればよい。

2019/5/4(Sat)

[Windows] PowerShellで生活するために - Get-Dateコマンドレット編(その1)

@惨めなUNIXシェルスクリプトプログラミング

いきなりタイトルに反してタッパーウェアの話をするけど、日付計算をシェルスクリプトでやろうとするとちょっと大変だ *1

おそらく大多数の人はdate(1)コマンドを使って計算しとるんじゃないか。

$ LANG=ja_JP.eucJP date -d "2019/05/01+114514 days" +"%Ec"
平成344年11月10日 00時00分00秒

西暦2019年5月1日から114514日後を計算した結果、まだ令和パッチ適用前なので平成344年で表示されている(一瞬334にみえるやつ)。

これは何かというと、*BSDやGNUのdate(1)実装は-dオプションの引数をparsedate(3)という関数に喰わせてtime_t型に変換している。

 68 static time_t tval;
 69 static int aflag, jflag, rflag, nflag;

 90         while ((ch = getopt(argc, argv, "ad:jnr:u")) != -1) {
 91                 switch (ch) {

 96                 case 'd':
 97                         rflag = 1;
 98                         tval = parsedate(optarg, NULL, NULL);
 99                         if (tval == -1)
100 badarg:                          errx(EXIT_FAILURE,
101                                     "Cannot parse `%s'", optarg);
102                         break;

この関数は マニュアルを読むと

-1 month
last friday
one week ago
this thursday
next sunday
+2 years

といった、ある日時からの相対日時を表現するさまざまなキーワードが実装されているのだ。 詳しい仕様についてはマニュアルもろくな内容じゃないので ソースを読め、なおオリジナルが1992年に書かれたyacc(1)による構文解析器なもんでタイムゾーンはハードコード国際化なにそれという酷い実装で、さらに伝播していった先々で独自に拡張されてるから移植性考えたらあまり使うべきではないのだ。

そうそう、移植性ハードコア勢であるならPOSIX縛りのドMコーディングなんだけど、その場合 そもそもPOSIX date(1)に-dオプションは無いから *2日付計算には使えないのだ、というかどうやってんですかあの辺の人種は。

だから言ってんだろーがdate(1)は時刻の設定と取得のコマンドなんだってば、日付計算に使うのは邪道なんだよ。

@豊かなPowerShellプログラミング

一方でUNIXなんぞより洗練され…洗練?…洗練ってなんだ?…まあいいPowerShellでは、日付計算はDateTimeオブジェクトを使うだけで簡単にできる。

文字列(String)から日時(DateTime)への変換もコマンドなんぞ介さず型キャストでおわり。

$date = [DateTime]"2019/05/01"

変数が型付きで宣言されてるなら型推論に任せてそのまま代入できる。

[DateTime] $date = "2019/05/01"

いちどDateTimeに変換してしまえばさまざまなメソッドで日付計算が可能だ。

$date = [DateTime]"2019/05/01"
$newdate = $date.AddDays(114514)

はい便利ですね。

そんでここからが本題、PowerShellには Get-Dateというコマンドレットが存在する、 Set-DateとあわせてUNIX date(1)と同等の働きを担うコマンドレットだ。

なんせ名前もGet-Date & Toughというくらいでひとりでは解けない現在日時刻を取得するコマンドなんだけれど、UNIX date(1)を真似したのか

  • -Dateオプションの引数で渡された文字列を解釈し
  • その日時を表すDateTimeオブジェクトを生成する

なんて機能もある。

$date = (Get-Date -Date "2019/05/01")

デフォルト引数は-Dateとして扱われるので

$date = (Get-Date "2019/05/01")

と書いてもよい。

ところがDA、恐ろしいことに

  • DateTimeへのキャストによる変換
  • Get-Timeを介しての変換

それぞれ動作が異なるのですわな、そしてそれが和暦表示と絡むとかなりの大災害が発生する。 特にUNIX使いがシェルスクリプトのノリで日付操作しようとするとdate(1)の代わりはっとでGet-Date使ってしまいがちなのでな…

@大災害

まずは論より証拠、PowerShellを開いて以下のコードを実行してみよう。

Write-Host ([DateTime]"2019/05/01").ToLongDateString()
Write-Host (Get-Date "2019/05/01").ToLongDateString()

この短いコードの出力結果は以下の通り。

2019年5月1日
2019年5月1日

はいどちらも同じ日をさしてますね、当たり前だよなぁ?

それでは以下の手順でカレンダーの種類を「和暦」に変更してみよう。

  • コントロールパネルから「地域と言語」ダイアログを開く
  • 「形式」タブの「追加の設定」ボタンを押して「形式のカスタマイズ」ダイアログを開く(ダイアログの天丼は最悪のUIだぞ)
  • 「日付」タブで「カレンダーの種類」を「西暦(日本語)」から「和暦」に変更する

さっきのコードを実行した結果は以下の通り。

令和元年5月1日
令和2019年5月1日

はいGet-Date使ってる方が2019年後に化けたよ!

改元のタイミングで設定変更して遊んでた人、その間に裏で動いてたPowerShellスクリプトがもしかしたら明日期限のタスクを2019年後にしてしまったかもね!

あ、そういえば先日ちらっと書いた 和暦表示が100年以上を扱えないバグ?の話だけど

  • PowerShellは令和2019年を扱える
  • にもかかわらずNew-SelfSignedCertificateコマンドレットで証明書を出力すると令和19年となってしまう

という現象も確認したので、証明書関連のAPIのどこかに

  • カレンダーが和暦の場合は西暦から変換してやらんとアカンやろうなぁ…
  • せや!CCyy形式(2019)からyy形式(19)に変換したで!

という国際化への造詣あふれるコードが存在してる可能性が高い、素晴らしいセキュリティエンジニア雇って書かせたコードなんやろうなぁ(しろめ)。

@次回予告

どうしてこうなった!を検証するよ。

*1:シェルスクリプトがshebang含めて3行以上になりそうならperlのようなもので書くワイには関係ない話なんだけどね…
*2:実際Solaris 9にはdate(1)に-d無かったりしたので使わないのは正義ではあった、今はLinux以外全滅したので縛りプレイは無意味でしょ。

2019/5/5(Sun)

[Windows] PowerShellで生活するために - Get-Dateコマンドレット編(その2)

試験運用中のWindows 10 1809に KB4495667が降ってきて令和表示されるようになったな、5/3リリースは早いのか遅いのか。

そいや銀行系で 令和元年が平成元年に化けて振込予定日が1989年にタイムスリップというバグ出したようだけどどんなクソコード書いてるのか空恐ろしいですわね、ワイなら預金全額以下略(風説の呂布)。

@謎解き

そんでは 昨日の続き。

  • DateTime型へのキャスト
  • Get-Dateの-Dateオプションによる文字列からのDateTimeオブジェクトへの変換

これはどちらも内部的には.Net Frameworkの DateTimeクラスのParse(String)メソッドを呼び出しているのだ、PowerShellからだと

[DateTime]::Parse("2019/05/01")

と書く。

そんでマニュアル(クソみてえな精度の自動翻訳に脳血管が切れないようen-usを推奨)を読むとParse(String)は

  • Parse(String, IFormatProvider)
  • Parse(String, IFormatProvider, DateTimeStyles)

と2つのオーバーライドが存在するのだ。

そしてこいつらの2つめの引数である IFormatProviderインタフェースに注目しよう、こいつはある特定の言語/地域の日付形式をパースしたり表示したい場合に指定するものなんですわ。

そんでPowerShellにおけるDateTime型へのキャストにおいては

  • 現在のOSの言語/地域設定は無視される
  • 必ずen-USとして日時を表す文字列はパースされる

ということで

[DateTime]"2019/05/01"

[DateTime]::Parse("2019/05/01", [System.Globalization.CultureInfo]::InvariantCulture)
[DateTime]::Parse("2019/05/01", [System.Globalization.CultureInfo]::CreateSpecificCulture("en-US"))

と等価なんですな、この InvariantCultureについてMicrosoftは

Gets the CultureInfo object that is culture-independent (invariant).

と「文化に依存しない(普遍の)」とかふざけたこと抜かしてるわけだけどen-USとかどこの田舎者だクソが。

一方でGet-Dateを使った場合は、現在のスレッドにおける言語/地域の設定を尊重するので

Get-Date "2019/05/01"

[DateTime]::Parse("2019/05/01")
[DateTime]::Parse("2019/05/01", $null)
[DateTime]::Parse("2019/05/01", [System.Threading.Thread]::CurrentThread.CurrentCulture)

あるいは現在の言語地域設定が日本語/日本であるならば

[DateTime]::Parse("2019/05/01", [System.Globalization.CultureInfo]::CreateSpecificCulture("ja-JP"))

と等価になるわけ。

その結果Get-Dateを使ったコードは、システムのカレンダーを和暦に変更すると2019年を西暦ではなく令和2019年と解釈してしまうのだ。

つーかこれさぁja-JPの場合であったとしても

  • 「2019/05/01」のように元号指定がなければ西暦として扱う
  • 「R1/05/01」のように先頭に元号指定があれば和暦として扱う

方が混乱無くていいんじゃないですかね、もはやこれバグの類だと思うゾ (日本人に限らずカレンダーの種類複数ある文化圏ならどっちか選ぶというより併用だよね)。

@結論

個人的な見解としては

  • DateTime型へのキャストがen-US固定なのはバグ、ちゃんと現在のスレッドの言語設定を尊重しろ
  • しかしDateTime.Parse("ja-JP") + 和暦カレンダーの場合、先頭に元号(MTSHR)を表す文字が無くても西暦ではなく和暦として変換するのもバグ

なんだけどまあ「仕様です」で終わりだろうな、ということで前回「UNIXシェルスクリプトで日付計算するやつはバカだ」と書いたけど、PowerShellで日付計算するやつもバカなのだ。

対策としていっちゃん簡単なのは国際化アプリケーションとしては失格だけど、そもそもその国際化がバグってて日付が2019年先に化ける危険性があるなら

  • 入力は西暦のみで和暦は禁止
  • Parse()ではなくParseExact()を使ってカスタム書式を使って厳密化

とでもしたほうがよっぽどマシってとこなんだけど、これも仕様をちゃんと読まずにコード書いて

Write-Host ([DateTime]::ParseExact("2019/05/01","yyyy/MM/dd", $null)).ToLongDateString()

とか書いちゃうと

令和2019年5月1日

に化けるのよね、勘違いしやすいところだけどWindowsそして.Net Frameworkの カスタム日時書式指定文字列において「yyyy」は西暦の意味ではなくカレンダーが和暦ならそれは和暦になるのよな。 そして第3引数のIFormatProviderにNULL指定してるのでシステムのカレンダーが使われ、そいつが和暦に設定してあればそりゃ令和2019年に化けるよという。

これがUNIXだったら「%Y」が西暦で「%EY」が和暦だし、そもそも同じMicrosoft製品でもOfficeだと「yyyy」が西暦で「gee」が和暦と明確に分けられてるのにね、そっちに慣れてると混同してしまい易いのよね。

なので必ずParseでもParseExactでも第2引数の言語/地域は指定しておけ。

$loc = [System.Globalization.CultureInfo]::InvariantCulture
Write-Host ([DateTime]::Parse("2019/05/01", $loc)).ToLongDateString()
Write-Host ([DateTime]::ParseExact("2019/05/01","yyyy/MM/dd", $loc)).ToLongDateString()

はーつっかえ、じゃ俺はPerlで書くから!

2019/5/6(Mon)

[Windows] PowerShellで生活するために - Requires宣言編

だいたいのスクリプト言語においては前方互換性のない変更、例えばPerl 5.10で導入されたswicth文なんかの新文法(今更かよ)を使いたい場合には

use 5.10;
use feature ":5.10";

のように記述して、これより古い環境ならエラーにするとか新機能をアンロックするみたいな機能が備わってることが多いと思われる。

同様の機能はPowerShellにも存在して

#Requires -Version メジャー番号(.マイナー番号)

と指定することで特定のバージョン以下の環境での実行を禁止する機能がある。

#Requires -Version 5.1

と指定しておけば5.1に満たないバージョンのPowerShellで実行するとエラーになる。

ちなみにこれはコマンドレットではなくコメント内に書くステートメントいう扱いなので、スクリプト中にではなくプロンプトで手打ちした場合は文字通りコメントとして扱われるので無意味だ、うーんこの。

まあええわこっから本題、移植性のあるスクリプトを書こうと思ったらチェック条件としてはRequires -Versionだけじゃ全く足りないのよね。

どういうことかというと、Windows 7/Server 2008 R2に WMF5.1を導入するとWindows 10 1809と同じPowerShell 5.1環境になるのだけれど、実はサポートされるモジュールの数がまったく別物といっていいくらい異なるのだ。 後者にあって前者にないモジュールはおよそ50前後存在するし、それらはInstall-Moduleコマンドレットを使っても追加できないOSバージョン依存のモジュールだ。

例えば同じPowerShell 5.1にも関わらずWindows 7環境だとPKIモジュールがない、Windows 10なら以下のようにヒットするんだけどWindows 7では死んだオウムのごとし。

Get-Module -ListAvailable | Where-Object { $_.Name -eq 'PKI' }

    ディレクトリ: C:\Windows\system32\WindowsPowerShell\v1.0\Modules

ModuleType Version    Name                                ExportedCommands                                                                            
---------- -------    ----                                ----------------                                                                            
Manifest   1.0.0.0    PKI                                 {Add-CertificateEnrollmentPolicyServer, Export-Certificate, Export-PfxCertificate, Get-Ce...

じゃあモジュール追加すりゃいいっすねとInstall-Module叩いても

Install-Module -Name PKI

PackageManagement\Install-Package : 指定された検索条件とパッケージ名 'PKI' と一致するものが見つかりませんでした。登録さ
れている使用可能なすべてのパッケージ ソースを確認するには、Get-PSRepository を使用します。
At C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.0.0.1\PSModule.psm1:1772 char:21
+ ...          $null = PackageManagement\Install-Package @PSBoundParameters
+                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Microsoft.Power....InstallPackage:InstallPackage) [Install-Package], Ex
   ception
    + FullyQualifiedErrorId : NoMatchFoundForCriteria,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackage

ちゅー感じ、そもそもオーエスがWindows 8 Pro/Windows Server 2012以降でないとダメなのだ、ご愁傷さまでした。

ちなみにそれぞれのOSにおけるPowerShell 5.1 PSVersionの結果

なるほど、じゃあWindows 10のみで実行できるようRequiresにBuildも指定してやればいいと思うよね…

#Requires -Version 5.1.17763

でもこれだとWindows 7/10どちらでも実行できてしまう、というのはRequiresはMajorとMinorまでしか認識せずBuildやRevisionは無視するのだ、どんな判断だ。

そもそもこのBuildやRevisionが導入されたのはバージョン5.0からなんだけれども、TechNetでの Windows PowerShell build numbersの管理は早々に放棄されてるのでBuildとRevisionにはまともな意味づけはされていないと思っていい、公式が忘れてんだから俺達もスルー推奨やってられっかクソバージョン管理。

それではWindows 10にはあるけどWindows 7にはモジュールが存在しないからスクリプトの実行を禁止したい場合にはどうするか?それはやはりRequiresを使用して

#Requires -Modules モジュール名1,モジュール名2...

と指定したモジュールの有無を検査すればよい、モジュール自体のバージョンも指定できる(詳しくはマニュアル嫁)。

でもPerlみたいにモジュールをuseしたりrequireするわけじゃないからいちいちコマンドレットからモジュール名調べるのめんどくさいから…と思うのが人情。 Windows 10 Proでだけ実行可能とするバージョン指定がしたいなぁと考える人は多いのではないだろうか。

しかしこれが困ったことにPowerShellにOSバージョンを取得する方法はいくつかあれど、まともに機能してるもんが今のところ無いんですわ。

つーことでそれぞれ問題があってろくなもんじゃない。

ということでめんどくさかろうがOS名でのチェックではなくRequires -Modulesを使って機能ごとにチェックすべし、たぶんそれでも非互換性ある気はしないでもないが。

2019/5/7(Tue)

[Hardware] Tekram DC-315U SCSIカードはWindows 10 64bitで使えるか?

先日 20年前のSCSIカードであるTekram DC-315Uが未だTekram USAで売ってると書いたんだけど、Windows 10 64bit対応ドライバあるかと思ったら付属ドライバがフロッピーッディスク入りなあたり20年前のまま売ってるっぽくて断定できねぇなあ。

ちなみにすでに消失しとるftp.tekram.com.twの過去アーカイブ漁ると「DC3x5 for Win64.zip」という名前で2003 Server向けamd64とia64用の署名無しドライバ(trm3x5.sys)は存在してた。 つーことでTekram USAがほんのちょっとやる気と予算を出してカーネルモードドライバにMicrosoftの署名貰っていればWindows 10 64bitでも動くんだが、そこまでするほど売れてるとも思えんしな。

オレオレ署名してテストモード起動すれば使えるとは思うけど手間だしセキュリティレベル下がるしな、それと隙あらばあなたのお探しのドライバ!とみせかけて悪意のあるソフトウェアをインストールさせようとする転載クソサイトを回避し、まともそうなミラー探してダウソしてくるのが一番難易度高い。 もはやArchie Gatewayは絶滅してしまいGoogle検索はクソまみれだから、インターネットの利便性はインターネット以前より悪化したのだ。

ドライバ署名を強制したことでドライバ難民が発生しそこにこういうマルウェアばら撒き隊がつけいる構造、他にもWindowsの意味不明のエラーコードやKB番号で検索しても同じ構図なんだが、こういうカスどももDNSフィルタリングとかSWAT送って銃殺しあとレッドモンドとマウンテンビューにICBM WhereToして焼き払った方がいいのでは感ある。 インターネットの自由?そんなもんはとっくに死体ですよ。

2019/5/11(Sat)

[Software] 日干し煉瓦作成用雲

アドビでは、一部のアプリケーションの古いバージョンを非認定とし、使用を認めていません

一部でCreative Cloud(CC)でガッポリ儲ける為に、以前のCreative Suite(CS)を使い続けるやつはアンインストールしないと訴えるからな?と勘違いされて絵描きや印刷屋が泡吹いとるようだけど全然違う話やん。

要するに「知財訴訟沙汰で負けて一部の機能を削除することで合意したからAdobe CCの過去バージョンは回収対象なので削除してね、買い切りのCSは関係ないよ」という話なんだけど、 これがAdobeの中の人間による隠せない和解条件への不満というフィルターを通すと「弊社による認定外のバージョンを継続利用すると第三者に権利侵害を主張されるリスク」という謎文章に化けるわけだ、駄目だこの会社(知ってた)。

つまりMSDN加入してれば過去バージョンのOSも使えるはずが、Sun Microsystemとの裁判の結果MSJVM入りのWindows 2000はダウンロード不可になったって昔あった出来事のセカンドカミングみたいな話だ(カレー臭漂うネタ)。

Adobeのダメさというと今ちょうど別件で頭抱えてるのよな、家族マシンに入れてるPhotoshop Elements 10がWindows 8.1以降は動作保証無いのだ。

この件に関しては最新版にアップグレードすりゃいい問題ではないのがほんとクソ、というのもAdobeが

と戦略を改めたからってのがあるのだ。

つーか11以降のの操作性の酷さって、いつまでもElementsにしがみついてるアマチュア写真家に憎しみを感じてさえいて、わざと年々使い勝手を幼稚化させてバーカバーカやってるとしか思えんのだ。 たぶんその年の新入社員に再実装させその中から最もできの悪いモジュールを選りすぐって置きかえていってるんだと思う、ヘイトアプリという枠があったら認定していいレベル。

しかしPhotoshop CC + Lightroom CC使えやバーカ簡易どころかフル版が月980円やぞ喜べよオラァン!ってクソ理論、もはや新しい事覚えたくない学習能力の低い老人が相手は通用せんのですわ。

そもそも前回XPから7移行の時にやはりPhotoshop Elements 5の動作保証ネーヨ問題があったので、当時の最新版(12だったかな?)にアップグレード検討したんだけど、あまりに操作性が変わってしまった上に よく使う機能が削られててああこりゃ絶対に無理ですわとなり、泣く泣く自分のマシンからPhotoshop CS6を消して家族マシンに譲る方向で考えてたのよこっちも。

しかしPhotoshop Elements 5 → Photoshop CS6ですら些細な操作性の違いにつっかかるので、結局以前の操作性かつWindows 7での動作保障のあるPhotoshop Elements 10の在庫を探して買う羽目になったという経緯があったのでな。 つーか学習能力あったらとっくにCorel Paintshop Proあたりに逃げてますわクソが。

もちろん貧乏人な身分としては、買い切りでないクラウド定額制は所得に対する累進性の問題とか社会問題として看過できんというのもある、オープンソースに逃げるとしてもGimpなんかも1/4世紀経ってもあの体たらくなのが頭痛い。 そもそもPhotoshop買えないし当時まだMotifで書かれてたGimpを使うためにPC-UNIXの世界に入ったワイですらあまりの進捗の無さに諦めてPhotoshop CSを買ったくらいだからな…

2019/5/12(Sun)

[Software] 続・日干し煉瓦(Adobe)作成用(Creative)雲(Cloud)

昨日の Adobeのお話、誤解あるようなんだが同じCreative Suite 6っても実際には公式に以下の2つのSKUが存在して、内部バージョンすら異なるいわば別のアプリなんですわ。

そんで今回のAdobeの通達は「後者の」使用許諾を停止する措置(なんせ買い切りでないレンタルだからね)であって、前者(CS6だけでなくそれ以前のバージョンも)を買った人にはなんも関係の無い話なのだ。

そして混乱を助長してるのが「うちではダウンロードまだできてる」と主張してる人だけどこれは前者のダウンロード販売 *1のもの、後者はみな平等に不可になっている。

ところで古いバージョンを使ってると訴えてくるとAdobeが主張しとる第三者って誰なんだろうね、昨年 DolbyがAdobeを訴えてた なんて話があるから時期的にもこいつかなーという気がするんだけど、OracleのJavaライセンス変更が原因だなんて明らかにソースは脳内なガセっぽい情報も流れてくるしで続報が欲しいところである。 本当にJavaの問題だったらMicrosoft vs Sun MicrosystemのMSJVM訴訟、そしてGoogle vs OracleのAPI訴訟といい呪われた言語としかいいようがない、ただでさえオワコンのJava離れがよりいっそう加速してありがたいのだが、まあねぇなあったら訴状見たいわ。

まー音楽なんかもストリーミングなんぞ有難がってると、グルーヴ(謎)した時に配信停止になるからCD買うのが一番ってことや。

*1:ちなみに箱で買った人はシリアル登録してもダウンロードはできないのだ、インストールメディアを破損 or 紛失した人は試用版をダウンロードしてシリアル入力という方法もあったんだけど、昨日の件でそっちも消された模様。

2019/5/14(Tue)

[Security] SHA-1 collision attacks

前回の SHAtteredは実用性は無いからへーきへーきで逃避してるひとがようけおったけど、また SHA-1が不幸にも黒塗りの高級車に衝突してしまう。後輩をかばいすべての責任を負ったMURに対し、車の主、暴力団員TNOKが言い渡した示談の条件とは…。

攻撃にかかる費用が10万ドルまで下がったというくだり、独裁者アリアスが元コマンドー部隊のベネットを雇った金額が「10万ドル、PONっとくれたぜ」なので、いよいよもって現実世界の問題やね。 ある種の組織(お察しください)にとってはポケットマネーだし「(SHA-1をぶち頃せと言われたら)タダでも喜んでやるぜ」という人もいる。 まあでも実用性は無いといってた連中が「セキュリティ2010年問題ってくらいでとっくの昔にSHA-256に移行してるからへーきへーき」でテノヒラクルーするだけなので以下略。

つか次のWindows 10大型アップデートでいきなりSHA-1なドライバ署名を遮断とか起きたら阿鼻叫喚なのではやまってみて欲しい、Vista時代のドライバが利用できなくなってWindows 7 EOL直前に移行計画白紙に戻ってやりなおしになる人の顔がみたい。

ちなみにMD5だと2007年にchosen-prefix collision attackが可能になり、その翌年にはなりすまし証明書が作成可能と発表されてるのだけど、Microsoftはなんだかんだ2012年まではソフトウェア署名にMD5有効にしてたっぽい。 このタイムスパン感覚でいくとWindows 8.1 EOLの2023年あたりに持ってきそうではある *1

ということで我が家はもう大型アップデートのたびにちゃぶ台返し必至なWindows 10に疲れたので、Windows 8.1で2023年まで耐え忍んで、それまでに自分自身がEOLになる事を祈ろうという案に傾きつつある。 幸運なことに過去に仮想マシン用に買って使わないまま放置してた部品紐付きなしDSPライセンス未開封品が1本だけあったので家族用マシンだけはなんとかそれでいけそう。

まさかタダでも要らないといわれた8の優待アップグレード4980円を買い漁っておけばよかったという気分なので、いま倍以上の値段で負けプレやオク出てるのをみると、転売ヤーは商才あったな(ぉ そもそも一度クソが出てきた穴から次カレーが出てくるなんて期待するナイーヴさは捨てろってことですな。

ちなみに大型アプデ嫌ならLTSC(旧LTSB)買えばなんてドヤ顔する輩をたまにみかけるけど、あれ個人でも3ライセンスから買えるけど金額はとうぜん個人向けではない。 そしてなによりそもそもの位置づけとしてEmbeddedやPOSReadyの後継なのでMS Officeなんかをインストールして普段使いする端末に入れるのはライセンス上アウトなのよね、 実際Office 365はブロックされるとかいう噂。 つーかいまどきレジ端末なんてiPadか泥タブだしATMとか自動販売機なんかからもWindowsって消えていくんじゃねぇかな…そっちの業界よう知らんけど。

*1:そういえばVisual Studio 2013もあわせてEOLになるのでついにVC++ Runtimeが Universal CRTのみになるのもこの年か。 一方でVisual Basic 6.0 Runtimeはおそらく32bit OSと同じ命日になると思われる。

2019/5/15(Wed)

[Hardware] 続・Windows 10 64bitで使えるSCSIカード

手持ちのSCSIケーブルが短過ぎてスキャナの配置に困るるからハーフピッチ50pinとフルピッチ50pin or D-SUB 25pinなケーブル探しにジャンク屋行ったんだけど、さすがに今時はどこも置いてないので困る。 もはや新品(つーか流通在庫の成れの果て)は結構なお値段するので手が出ない…

そんで代わりといってはなんだが2枚SCSIカードが増えた(しろめ)。

あと Buffalo MCR-SというPCMCIAなフラッシュATAカードをSCSI経由で使う古代のカードリーダーがあったのだけどスルーしてしまったんだが、今考えると PCMCIAなCFアダプタ介せば Aztec Monsterの代用として我が家の骨董宅録機材であるAKAI DR-4dというハードディスクレコーダーの内蔵ドライブに使える可能性を見落としていた、次回行った時に残ってたら&安ければ買うか…

[Windows] Windows 8.1/Windows Server 2012 R2 用パッチ一覧

Windows 10がどうにもアレなので、Windows 8.1移行を視野にパッチ統合インストーラー作成用に現在必要なパッチ一覧を作成してみた。

後はdismなりPowerShellなりNTLiteのようなサードパーティー製パッチ統合ツール使って適用済インストーラー作成すればおk。 試用版で確認しただけなのでRetailだと不足するパッチはあるかもしれない。

ちなみに現在運用中のWindows 7/Windows Server 2008 R2用のパッチ一覧はこちら

2019/5/16(Thu)

[Security] Zombie Load Attack/MDS(Microarchitectural Data Sampling)

今度は ゾンビロードだと、Intel公式の説明は こちら、俺が説明できるわけもないので以下略

性能劣化といわれても新しいパソコン買う金あったらスマホ買い替えが先ですわなので受け入れるしかないのだが、Meltdown/Spectreはなんだかんだ 最新microcodeでベンチ取ったら当初の予測よりはマシなところで落ちついたので、今回もIntelの技術者が我が家のSandy/Ivy/Haswellもなんとかしてくれるやろ(鼻ホジ)。

そういえば脆弱性「緩和」って「令和」っぽいよね何いってんだお前。

それにしてもいつ頃からだっけ脆弱性発見でいちいち命名してはドメイン取得する謎の風習が生まれたのって、Heartbleed以前とかもう思い出せないんだけど。 国が広告代理店のいいなりにsenkyo20XXみたいなクソドメイン取得してたのと同じだよな…

[Windows] RDS remote code execution

リモートデスクトップサービスにリモートコード実行の脆弱性だと、SMBの時みたいにWindows XP/Server 2003にすらパッチが提供されるあたり緊急度の高さがうかがえる。

まぁでもWindows Server 2016以降はヘッドレスなServer Coreがデフォで Windows Admin Centerで操作しろだし 2019なら公式でOpenSSH提供ちゅーことでもうサーバー用ではRDS/RDP有効にする意味無くなりつつあるよね。

クライアントで使う分にはWindows 10が省電力張り切りすぎてリモートデスクトップ中に居眠りしやがる方が重大な問題なのだけど、いろいろ設定してるんだが解決しない…

2019/5/18(Sat)

[Windows] Twentieth Century Fox

20世紀から21世紀にかけてインターネットの出現によって人類は進歩したかというと、ここ最近はUnicode絵文字によるコミュニケーションで人は文章作成・読解力を失い、広告収入めあてに文字なら数行読めば3秒で理解できる記事をわざわざ数分の動画にして小遣い稼ぐ輩どもを地に栄えさせ、それを検索エンジンが結果上位に押し上げるのだから、口頭伝承の原始時代に戻っただけなのでは感がある。 なお冗談っぽく書いているがわりとマジに絶望していて、自分が神だったら楽園から追放したり洪水を起こしたり塔を崩し言葉を乱すプログラム書いてると思う。

なぜ人心がここまで乱れたか、それはブラウザが右肩で地球儀をぐるぐる回転させるのを止めてしまったからだ、このインターネットが世界と繋がっている事をこのgifアニメーションを消したことで人々は忘れてしまったのだな。

なんでそんな事を考えたかというと、ちょいとSCSIカードの動作確認に10年ぶりにWindows 2000インストールし、IE6.0SP1の右肩でぐるぐる回る地球に感動したのだ。 おそらくガガーリンがはじめて宇宙から地球をみたとき、アポロ8号のクルーが月周回軌道ではじめて地球の出に遭遇した時、同じような畏敬の念を抱いたに違いない。

地球は青かった。

それにしても月だけにLunaとかAeroとかMetroといかにこの10年いかにグラフィカルユーザーインタフェースがクソデザイナーのオナニーで劣化してきたか気づかされますね。 ユーザーインタフェースはどうあるべきか自分には文才がないので「粉末 Motif」で検索することで出会える稀代の奇跡の名文をぜひ一度読んでいただきたい、「GUIの価値は"プッシュボタン"で決まる」「ボタンの押し心地」正しくこれなのである。 何が悲しゅうてフラットデザインでAthena Widgetのような石器時代にまで退化せにゃあかんのだアホか。

話を戻して、どうせ隔離環境でちょっと動かすだけだからする必要は無いんだけど興味本位でWindows Updateで最新の状態にまで更新してみた。

とすればWindows Updateで100個くらいパッチ振ってきて労せずに延長サポート時点での「最新」状態はなるもよう。

ただし延長サポート終了時点で提供されてたはずのパッチが重要なのも含めていくつか消えてるっぽいな、理由はわからん。

こいつらVML除いてwww.catalog.update.microsoft.comにも無いのいだけど、わざわざどっか落ちてるの探して適用しようが脆弱でネット繋げないから無意味だし忘れていいけどな。

あと追加の機能もこのへん消えてるっぽいな、いくつかはインストーラーは落とせるし手動で追加すれば更新パッチWindows Updateで降ってくる。

まぁこっちもどうでもいいやなこれ。

テレホマンの時代は終わり地にADSLが満ちブロードバンドの時代が訪れた後も当時諸事情によりドコモのP-inしか回線無い時期が長かった、@FreeDで定額制になる前に事故により料金7万円とかパケ死したトラウマが掘り起こされる。 つーかその後もWilcom AirH"しか回線無い時期もけっこう長かったのだ…あんなんでよくcvs checkoutとかcvsupとかrsyncとかやっとったと今更ながら思う。

2019/5/19(Sun)

[Windows] PowerShellで生活するために - ForEach-Objectコマンドレット編

@這いずり回る混乱

PowerShell初心者(ワイもやで)が混乱するポイントに、イテレーション処理にforeachステートメントとForEach-Objectコマンドレットのふたつある事が上げられる。

まずforeachステートメントなんてみた瞬間「テメーどの面下げて関数型言語を名乗ってんだ *1オルァン!」と血圧が上昇する関数型言語ラーの方々はおいといて

foreach ($elm in "item1", "item2", ...) {
	Write-Host $elm
}

とまぁこれだけみると普通の手続き型プログラミング言語でございますな。

しかしこれだとfor文禁止じゃ高階関数で後悔させてやるオルァン!と息を巻く原理主義の方々が激怒必至なので

"item1", "item2", ... | ForEach-Object { Write-Host $_ }

と書くことも可能なわけです、どうですか…心が落ち着きましたか…どうぞご着席ください。 なお今度はだいたい兼任してると思われるUNIX哲学原理主義が目覚め、パイプを流れるストリームが汎用インタフェースのテキストでなくオブジェクトであることに激怒して以下略。

ちなみに後者はこれ関数型言語Java(しろめ)で書くなら

Arrays.asList("item1", "item2", ...).forEach(System.out::println);

と同じですわなこれ、なぜJavaで書いた!(突然暴れだす患者)

@混乱を助長する余計なもの

なおForEach-Objectにはエイリアスが存在して紛らわしいことに

"item1", "item2", ... | foreach { Write-Host $_ }

とも書けてしまう、しかしforeachエイリアスはforeachステートメントとは明確に違うので混同してはならない(戒め) *2

さらにややこしいことにこうも書ける。

"item1", "item2" | % { Write-Host $_ }

この場合もforeachと同様に「%」はForEach-Objectコマンドレットのエイリアスなのだ。

おそらく「%」なんてエイリアスを用意したのは、シェルで使う場合にForEach-Objectは長過ぎるからだろう、他にもWhere-Objectのエイリアスである「?」などがあり

(Invoke-WebRequest 'http://www.microsoft.com/').AllElements | Where-Object { $_.tagName -eq 'h1' } | ForEach-Object { Write-Host $_.innerText }

(wget 'http://www.microsoft.com/').AllElements | ? { $_.tagName -eq 'h1' } | % { Write-Host $_.innerText }

と書き直せる、わーいタイプ量が減ったよ!って疑問符とか特攻の拓ならまだしもプログラミング言語としては検索性最悪でどんな判断だとしか言いようが無い、こういう可読性クソなところがPerlっぽくて嫌いなのに使ってしまうビクンビクン(クソ言語ソムリエ)。

なので現在では推奨されない書き方になってるはず、Set-StrictModeで禁止できたような記憶があったんだが今マニュアル確認したら無いな。 単にVisual Studio Codeが警告だしてただけかもしれない、あと使いづらいのでPowerShell ISE for PowerShell 6はやくだしてやくめでしょ。

@混乱による悲劇

話をもどす、foreachとForEach-Objectの混同でありがちなのが、デフォルト変数(Perl用語)である$_の扱いが違うことに気づかないサルコーダーが、気楽にStackOverflowやQiitaとかいうYahoo!知恵袋の類似サイトからコピペしてバグるパターン、ほんまインターネットとPowerShellは地獄だぜ!

foreach ($i in Get-Content 'unko.txt') {
	if ($_ -match '^\s*#') {
		...
	}
}

$_に入ってる値はその前にやった処理で実行結果は神のみぞ知るってやつ。

これPerlみたいにデフォルト変数使えるようにして

foreach (Get-Content 'unko.txt') {
	if ($_ -match '^\s*#') {
		...
	}
}

と書ければある程度は事故防げたんじゃねぇかな…まぁでもコピペコーダーを滅するのが正道か…

ちなみに$_もエイリアスで本当は$PSItemであり呼び方も自動変数というのだけど、まあPerl使いなのでこっち使って/呼ばせて貰いますわ(老害)。

あと初心者的には

foreach ($line in Get-Content 'unko.txt') {
	if ($line -match '^\s*#') {
		continue
	}
	Write-Host $line
}

みたいにある条件(この場合コメント行を無視する)の場合continueで処理をすっ飛ばすなんてコードを、違いを理解せずにForEach-Objectで書き直して

Get-Content 'unko.txt' | ForEach-Object {
	if ($_ -match '^\s*#') {
		continue
	}
	Write-Host $_
}

が期待通りに動かないとかあるあるなんやな。

これbreak/continueステートメントがfor/foreach/while/doステートメントに続くブロック内では期待通りに動作するのは当然だけど、なぜForEach-Objectに続くブロックでは動かないのか初心者はそりゃ混乱しますわね。

答えは簡単、後者はただのブロックではなくベンザブロックでもなく「スクリプトブロック」と呼ばれるもので、他の言語だとラムダ式とか無名関数を引数として渡してるのと同じことなんですわ。

なので関数ポインタのように変数にスクリプトブロックを代入することもできる。

$doit = { Write-Host $_ }
"item1", "item2", ... | ForEach-Object $doit

ただしスクリプトブロックはclosureつまり関数(は木を切るヘイ)閉包ではないので束縛変数を持たないから引数も渡せないとかがね…(また回を改めて書く)。

そんでスクリプトブロック中でbreak/continueが呼ばれると、そこでForEach-Object処理そのものを大域脱出してしまうのだ、ていうか文法エラーの方がよくない?これ。

ちなみに初心者が困り果てて世の害悪こと教えて掲示板で「助けて!動かない!」すると80%の確立でcontinueやめてreturnにすれば動くよ!というポイント稼ぎ回答が返ってくると思う。

Get-Content 'unko.txt' | ForEach-Object {
	if ($_ -match '^\s*#') {
		return
	}
	Write-Host $_
}

せやな、とりあえず期待した通りの動きにはなるかもしれんけど不正解。

正解はですね、関数型スタイルならそもそもこんなとこにifステートメント書く自体が間違いなのでfilterを使えなのだ、Where-Objectを使って

Get-Content 'unko.txt' | Where-Object { $_ -notmatch '^\s*#' } | ForEach-Object { Write-Host $_ }

と書くべきなのである。

@混乱こそ美

ある程度PowerShellへの理解が進むと、手続き型と関数型が混在するとソース読み辛いから関数型スタイルで統一するね…という気分になるのだけど、実はそうもいかないところがPowerShellのつらいところ。 実はForEach-Objectはとてつもなく遅い処理なのだ、ベンチとるのめんどくさいしググれば先人による記事あるからワイが書くまでもないしそっち読んで。

ちなみに最速はforeachステートメントですらなくforステートメントなので

$list = "item1", "item2", ...
for ($i = 0; $i -lt $list.length; ++$i) {
	Write-Host $list[$i]
}

というモダンさの欠片も存在しないコードが大正義だったりする、この故郷のような安心感。

しかし巨大なテキストファイルを全部配列に読み込んだりすると今度はメモリ使用量が問題になったりするわけで

スタイル 速度 メモリ使用量 可読性
for/foreach × ×
ForEach-Object ×

を考慮しつつ結局はケースバイケースなのだ。

@ここで明かされるForEach-Objectの驚くべき正体

そして最後に衝撃的な事実を告げよう、ForEach-Objectって上の例ではわざと使わなかったんだけど

"item1", "item2", ... | ForEach-Object { Write-Host "header" } { Write-Host $_ } { Write-Host "footer" }

のようにスクリプトブロックを3つ引数として指定できるのですわ、これオプションを省略せずに書くと

"item1", "item2", ... | ForEach-Object -Begin { Write-Host "header" } -Process { Write-Host $_ } -End { Write-Host "footer" }

となる、さあだんだん前世で体験した何かを思い出しましたね?そうこれはawk(1)のBEGINとENDブロック!

つまりはだ、ForEach-Objectってーのは関数型言語だ高階関数だmapだなんて小難しい事より *3、UNIX文化におけるawk(1)の代替コマンドレットだったんだよ!そのためのパイプでなんという恐ろしいUNIXの再発明!

これコマンドレット名はForEach-ObjectではなくAho-Weinberger-Kernighan、エイリアスもforeachとか%じゃなくてawkで良かったのでは…Invoke-WebRequestにwget(1)なんてエイリアスつけるくらいあっちチラチラみてるわけだし…

なおawk(1)より億倍使いづらい、なんせマッチする行にifステートメント書いたり区切り文字での分割も自分でせんとあかん。

awk -F'\t' '!/^\s*#/ { print $2 }' unko.txt

がPowerShellだと

Get-Content 'unko.txt' | ForEach-Object  { if ($_ -notmatch '^\s*#') { Write-Host ($_ -split '`t')[1] } } }

になるってどう考えても劣化しすぎよね(もっと簡単に書ける書き方あったらスマン)、複数条件ある場合はifステートメントの代わりにWhere-Object使いづらいしGet-Contentすなわちcat(1)の代わりに-InputObject使えないから Useless Use Of Cat問題もあるわけだし。まぁパイプをオブジェクトストリームとしたのが失敗なのだ。

関数型言語の思想はUNIX哲学と似ているとはいうけど、Microsoftはメシマズアレンジが得意だったな…ワイ語れるほど関数型言語もUNIXも知らんけど。

*1:そもそも名乗ってたっけかな…自信無くなってきたぞ…でも開発名Monadだしな…
*2:混乱に拍車をかけるのがPowerShellの大文字小文字同一視な文法でもあり、MicrosoftをMicrosoftたらしめるBASICの息吹を感じる(しろめ)。
*3:勘のいい人はそもそもreduceに相当するコマンドレットがせいぜいMeasure-Objectくらいしか無さそうな時点で気づくな(しろめ)

[Hardware] BUFFALO IFC-USP SCSIカードはWindows 10 32bitならWindows 2000用のドライバで動作する

先月くらいに BUFFALO IFC-USPというAdvanSys ASC3050Bというコントローラー搭載のSCSIカードを108円で買ってたんだが記事にしてなかったな。

調べた限りではAdvansysはWindows 2000までのドライバしか出してなくて、XP/2003 ServerやVista/2008 Server向けに64bitドライバは提供してなかったようで、Windows 10なんかで使うにはWindows 2000用の32bit用の未署名ドライバを入れるしかない。 まぁ32bitならドライバ署名云々いわれることは無いので警告無視してインストールするだけでOK。

ちなみにRATOC Systemが同チップを採用しているREX PCI-30にWindows 10 64bit対応の有償ドライバを販売してるんだよな。 おそらく自分ところでわざわざスクラッチでドライバ書いたのかもね、ベンダチェックとかなければ購入すればIFC-USPも動くかもしれない。

2019/5/20(Mon)

[Windows] PowerShellで生活するために - Import-Csvコマンドレット編(その1)

前回はForEach-Objectコマンドレットの正体はawk(1)であると看破したけど(おい)、PowerShellはMicrosoftで生まれました、ベル研でもCSRGでもMITの発明品じゃありません、Jeffrey Snoverフェローのオリジナルです、しばし遅れをとりましたが今や巻き返しの時です。

しかし実際のところ世間一般でawk(1)の代替はImport-Csvコマンドレットの方だと思われているかもね、ただawk(1)ってやつは区切り文字でテキストを分割しての処理に適してるとはいうけど、CSVってやつはもうちょっと複雑なデータ形式でクオート文字の中に区切り文字や改行コードまで含めることが可能だから、本当はawk(1)向きではないのだ。

ワイはそもそもshebang含めて3行超えそうならPerlで書いてしまうおじさんなので *1、Text::CSV_XSあたりCPANから拾ってささっと書いとったのだが、世の中にはGNU awkの拡張(FPATとか)を駆使して頑張る人もいるらしい。

いっぽうImport-Csvはこのようなクオート中の区切り文字や改行コードも、どうぞ回してみてください…いい音でしょう?余裕の処理だ、馬力が違いますよ。

とりあえず書き方のサンプルはこんな感じ。

Import-Csv -Delimiter '`t' -Encoding OEM -Path '1.tsv', '2.tsv', ... | ForEach-Object { Write-Host $_.カラム名 }

うん、Import-CsvそのものはCSV形式のファイルをオブジェクトに変換してパイプラインに流すだけだからやっぱりForEach-Objectとセットでawk(1)やね、Import-Csvは特殊なcat(1)相当でしかないっすわ。

そんでこのプロパティ名って何を元に決めてるのかというと、1行目がヘッダ行として扱われそこでの値が使われるのですよな。

"アクセスURL","参照元URL","アクセス日時","ホスト名","User-Agent"
"/","","2019/05/01 00:01:16 JST","host1-113-0-203.example.com","NCSA_Mosaic/2.0"
...

なんてCSVファイルであれば

Import-Csv -Path 'access.log' | ForEach-Object { Write-Host $_.ホスト名 }

と書けば特定の列、このコード例なら「ホスト名」にアクセスできるわけ、またの名を源氏名。

これがもしヘッダ行の無いCSVなら(そっちの方が多いよね…)

Import-Csv -Path 'access.log' -Header "アクセスURL","参照元","アクセス日時","ホスト名","User-Agent" | ForEach-Object { Write-Host $_.ホスト名 }

のようにいちいち-Headerオプションで明示的に指定する必要があるのがちょっとめんどくせえなという感じ。

そんでこれは文法の話に脱線しちゃうけど、カラム名に「-」や「$」などの特殊文字やスペース等が含まれる場合

$_.{User-Agent}
$_.'User-Agent'
$_."User-Agent"

とクオートしてやらんと演算子や変数と間違われて予期しない結果になるのは注意。

なおSelect-Objectの-Propertyオプションの引数ではクオートしなくてよいとか

$_ | Select-Object -Property User-Agent

なーんか一貫性が無いよな…

はいお次、最後の列だけ取り出すにもawkなら組込変数NFを使えばいいのだけれどもPowerShellではちょいと厄介だ。 ヘッダを明示的に指定しているパターンであれば、配列の最後の要素は添字に-1を指定することで取り出せるので

$header = "アクセスURL","参照元","アクセス日時","ホスト名","ユーザーエージェント"
Import-Csv -Path 'access.log' -Header $header | ForEach-Object { Write-Host $_.($header[-1]) }

とすることで何とかなるのだけど、CSVファイル中のヘッダ行を使う場合には

Import-Csv -Path 'access.log' | ForEach-Object { $_.($_.PSObject.Properties.Name[-1]) }

とだいぶ薄汚れたコードを書かないとあかんっぽい、なんやこのクソ言語…

@次回

まだまだImpotent-Csvへの文句は続くよ…

*1:なおPowerShellと同様に記憶喪失かと思うくらい覚えられないのだよね>Perl、何年書いてるんだっけ俺…毎回同じような事を調べてる気がする…

2019/5/24(Fri)

[Windows] Dismコマンドで永続的パッケージを無理矢理アンインストールする

今家族マシン用に確保してあるWindows 8のDSPライセンスだけど、8をインストールしてパッチ適用してストア経由で8.1にアプグレしてからまたパッチ適用というアホみたいな手順は踏みたくない。 しかしWindows 8.1はサービスパックみたいに単体でWindows 8のインストールイメージに統合できる形式で提供されてないのでアレ。

なのでせっかくMicrosoft公式が

を提供してるので、こいつ使ってクリーンインストールすることにする。 なんせWindows 8のシリアル番号はそのままWindows 8.1のインストールに有効だから利用条件は満たしてるので以下略

ちなみにISOの中身はRTM版ではなくUpdate 3なんだけどそれでも150近いパッチの適用が必要で、Security Rollup適用すれば70程度で済むWindows 7よりひどいことになっている、うーんこの。

そしてしれっと悪名高いKB2976978いわゆるテレメトリパッチが混入しているのがクソ、ワイはパラノイアなので

dism /Image:mount /Remove-Package /PackageName:Package_for_KB2976978~31bf3856ad364e35~amd64~~6.3.2.1

を実行してアンインストールを試みたんだけど

1 / 1 を処理しています - Package_for_KB2976978: 永続的パッケージはアンインストールできません。
 エラー: 0x800f0825

エラー: 0x800f0825

DISM が失敗しました。操作は実行されませんでした。

というエラーが出てアンインストールできねえやんけクソが。

これはなぜかというとシステム回復時にUpdate3の更新ロールアップまでリセットされないように

dism /Image:mount /Cleanup-Image /StartComponentCleanup /ResetBase

を実行してそれらのパッチ(テレメトリまで含めて)に永続化をマークしてるからなのよね、詳しいことは これ読め

ところがこの永続化フラグってのは実にいいかげんなものなようで、もういちどAdd-Packageし直したら外れることが判った。

dism /Image:mount /Add-Package /PackageName:Package_for_KB2976978~31bf3856ad364e35~amd64~~6.3.2.1
dism /Image:mount /Remove-Package /PackageName:Package_for_KB2976978~31bf3856ad364e35~amd64~~6.3.2.1

ポイントはPackagePathではなくPackageNameにインストール済のパッケージ名を指定してAdd-Packageを実行するのがミソ。 もちろんこのKB2976978がベースシステムの置き換えで無い単純な増分でしかないから可能な技で、Cleanup-ImageでWinSxSの下にある古いバージョンは消されちゃった永続的パッケージは削除できないとは思うが。

そんでPowerShellのAdd-WindowsPackageコマンドレットは-PackageNameオプションが無いけれどそれなら-PackagePathを使えばいいじゃないと、最新のKB2976978(今はv24)のパッチをダウンロードしてきて

Add-WindowsPackage -Path .\mount -PackagePath .\windows8.1-kb2976978-v24-x64_edb8f26452e645838dc6797fa23374fb24cfd2df.msu
Get-WindowsPackage -Path .\mount | Where-Object { $_.PackageName -match 'KB2976978' } | ForEach-Object {
	Remove-WindowsPackage -Path .\mount -PackageName $_.PackageName
}

とすることでこっちでも削除できたゾ。

2019/5/25(Sat)

[Windows] PowerShellで生活するために - Import-Csvコマンドレット編(その2)

前回はImport-Csvというのは awk(1)というより、CSVファイルを読込んでオブジェクト(=PSCustomObject)のストリームとして流す特殊な cat(1)だと書いた。

なお特殊ではないcat(1)に最も近いコマンドレットには Get-Contentというのがある。

Get-Content -Path 'unko.csv' -Encoding OEM | ForEach-Object { Write-Host $_ }

こちらはファイルをやはりオブジェクト、つまりString *1に変換してパイプに流す。

細かいことをいえばcat(1)が「con CAT inate(結合)」であるのに対し「Get Content(中身をとりだす)」という思想の違いがあるけど、そこは目を瞑っておこう。 そもそもcat(1)でファイル結合ができた時代なんてのは遠い過去の歴史上の話だ、CSVみたいな古色蒼然としたファイルフォーマットですらヘッダ行が存在するなら結合時に読み飛ばさんとおかしなことになる。

なによりテキストファイルの文字コードがUS-ASCIIしかない時代ならまだしも、現代では数えきれないほどの文字コードが存在し異なる文字コードのファイルをcat(1)したら即文字化けだ。 Unicodeですら複数CESが存在してあまつさえBOMなんてシロモノがある時点で論外なんやで。

ということでファイルフォーマットの数だけcat(1)が増えるのは致し方ない、三毛とか鯖虎とかね…

@UNIX哲学の負の側面

そんでcat(1)というと思い出すのが Useless Use Of Cat、つまり「無駄にネコさまの手をわずらわせる」と呼ばれる性能問題なんですわ。

簡単な例としては

$ cat unko1.txt unko2.txt | awk -F',' '{ print $NF }'

というやつ、awk(1)では

$ awk -F',' '{ print $NF }' unko1.txt unko2.txt

と引数でファイルを指定できるので、ここでのネコさま登場させるのはパーフェクトに無駄骨でありプロセス生成とプロセス間通信のコストだけ性能は確実に劣化するわけ。

そんでこちらもまったく意味の無いファイルの最後5行を表示するのにネコの尻尾踏んづけるコード

$ cat unko.txt | tail -n 5

尻尾ひとふりするだけで終わる仕事なんよな。

$ tail -n 5 unko.txt

他にも

$ cat unko.txt | sort | uniq

なんて書かずに

$ sort -u unko.txt

とsort(1)だけで書けるよとかね。

この問題はcat(1)にとどまらないので、はつみみですというネコは米Yahoo!でスケーラビリティーに関する仕事に携わってた、NなんとかBSDの開発者による「 Useless Use of *」というプレゼンでも読んでみてどうぞ。

そんでよくUNIX哲学と関数型言語は似ているなんていわれるけど、それはすなわち同じ欠点を持ってるってことなのだ。Useless Use Of Higher-Order Functionとでも呼ぶんすかねこれ。

これはPowerShellも同じで

Get-Content unko.txt | Select-Object -First 5 | ForEach-Object {
	Write-Host $_
}

なんてコードを書かずに

  • -TotalCount … 実質head(1)コマンド
  • -Tail … 実質tail(1)コマンド

とSelect-Objectの-First/-Lastオプションに相当するfilterが実装されてるのでそちらを使った方が性能的に有利なはず。

Get-Content -TotalCount 5 | ForEach-Object {
	Write-Host $_
}

同じ理屈はそのまんまImport-Csvにも当てはまるんだけど、こっちには読込む行数を指定するオプションが存在しないのよね、あくまでパイプで渡した先のfilter側で件数を絞らざるをえない。

Import-Csv 'unko.csv' -Header "Column1", "Column2" | Select-Object -First 5 | ForEach-Object {
	Write-Host $_.Column1
}

まぁパイプで繋いでるのだから

  • Select-Objectがパイプラインから5レコード読んだら
  • SIGPIPEだかバグパイプ的なものがピーヒャララと飛んで
  • それを受取ったImport-Csvは読込処理をそこで終了

するだろうからCSVファイル全体を読み込むわけでもなし、そもそもコマンドレットはプロセスではないからUNIXシェルスクリプトよかコストは格段に低いとは思うけど。

ただ 過去回でさらっと触れたforeach vs ForEach-Object対決をみる限り、ほんとうに無視できるほどのコストなんですかね?って疑ってしまうのですな。

ちなみに性能測定にはMeasure-Commandというコマンドレットがあるけど、困ったことにImport-CsvはCSVファイルをパイプラインから読み込むという動作ができんので

Measure-Command {
	Get-Content 'unko.txt' | Import-Csv | Select-Object -First 5
}

Measure-Command {
	Get-Content -TotalCount 5 'unko.txt' | Import-Csv
}

の差を測定するみたいな性能テストができないのよね、PowerShellはオープンソースなので ソース落としてきて読み込む行数を指定するオプションを自分で実装してどれくらい改善するか調べりゃいいんだが、あまり読みたいコードじゃねぇんだよなぁ…

@パイプを流れるストリームがテキストではなくオブジェクトであることの弊害

そしてここでもうひとつImport-Csvの、ひいてはPowerShellそのもののバッドデザインが明らかになりましたね、そうImport-CsvはパイプラインからCSVファイルを読み込めないんですわ。

例えばcat(1)はパイプからもデータを読み込める、だから下のような完全に意味の無い多頭飼いネコの数珠繋ぎができる、これにはムカデ人間のヨーゼフ・ハイター博士もニッコリ。

$ cat - | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat

まぁこの節操の無さこそがUseless Use Of *問題を生むんだけどね!

しかしPowerShellにおいてGet-ContentやImport-Csvのようなコマンドレットは、ファイルをオブジェクトに変換してパイプにストリームとして流す起点にはなれるんだけど、自身がパイプからストリームを読み込むができないんですわ。

これすげー地味に利便性悪く、インターネットから例えば「非国民の休日.csv」なんてものをダウンロードしてきて処理するのに

[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
(Invoke-WebRequest -Method Get -Uri 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv').RawContent `
    | Import-Csv -Encoding OEM |  Where-Object { ([DateTime]$_.{国民の祝日・休日月日}).Year -eq 2019 } | ForEach-Object {
	Write-Host $_.{国民の祝日・休日月日}
}

みたいに書けないのよね、いちいち一時ファイルとして保存して

[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$tmpfile = New-TemporaryFile
Invoke-WebRequest -Method Get -Uri 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv' -OutFile $tmpfile
Import-Csv -Encoding OEM $tmpfile | Where-Object { ([DateTime]$_.{国民の祝日・休日月日}).Year -eq 2019 } | ForEach-Object {
	Write-Host $_.{国民の祝日・休日月日}
}
Remove-Item -Force $tmpfile

と余計な処理が増えるのがいろいろ制約でてきてつらい。

UNIX哲学においてはパイプを流れるのはテキストという汎用インタフェースなのだけど、PowerShellにおいてはオブジェクトなのでImport-CsvもCSVファイルを読込んでせっせとオブジェクトを作ってパイプに流すけど 便所に紙以外のもの流すと詰まるよなーという話、Microsoftのオフィスのトイレはベル研より配管が太いのだろう、まぁベル研は音響カプラで300ボーという便所だったしな…

そういえばGoogle翻訳は「トイレ スッポン」を「Toilet Suppon」と訳すので、こっちもさぞや詰まらない立派な便所があるんだろう。 ちなみに正解は「Toilet Plunger」、でも「Toilet Softshell Turtle」と訳されて動物愛護団体と深刻な文化摩擦を引き起こされりよかマシか…。 なお「すっぽん」だとなぜか「Happy」と訳されるので、Googleオフィスはみな裸族であり川とか流れててそこで用を足してると推察される。

@次回

もうちょっとだけImport-Csvへの文句は続く、そして今度はCSVをファイルに出力するExport-Csvの話までいけたらいいね。

*1:あるいは-AsByteStreamオプションでByte、6.0以降より

2019/5/26(Sun)

[Windows] PowerShellで生活するために - New-TemporaryFileコマンドレット編(その1)

ちょいと前回のサンプルにNew-TemporaryFileコマンドレットが登場したので、Import-Csv/Export-Csvの話より先に書いておく。

@UNIXのmktemp(1)とは何か

毎回WindowsカテゴリなのにタッパーウェアことUNIXの話をしている気がするけどまあええわ、タッパーウェアにおいて一時用ディレクトリである/tmpや/var/tmpは誰でも読み書きが可能な「激アツスリーセブン!ジャンバリ大開放!!」パーミッションとなってる。

$ ls -ld /tmp
drwxrwxrwt+ 1 tnozaki None 0 May 25 23:47 /tmp

$ ls -ld /var/tmp
drwxrwxrwt+ 1 tnozaki None 0 Apr 14 10:26 /var/tmp

今時Cygwinなのでオーナーとグループが変だが気にするな、ここに作業用のファイルやディレクトリを作成する場合、情報漏洩やシンボリックリンク攻撃などのセキュリティ事故を避けるよう注意してコーディングする必要がある。

まぁ詳しい話はここはタッパーウェア通販サイトでもパチスロ情報誌でもないので セキュアコーディングガイドでも読んどけ、 最近のOSは「per user tmp」すなわちユーザー毎に一時ディレクトリが用意されているのでセキュリティについては昔よりマシにはなってるけど、機密性がわずかにマシになっただけでプログラミングには変わらず注意が必要なことに変わりはない。

シェルからならたいていの環境にはmktemp(1)コマンドが用意されているので、これを使って作業用ファイル・ディレクトリを作成すればおk。

作業用ファイルなら

$ f=`mktemp -p /tmp unko.XXXXXX`
$ ls -l $f
-rw------- 1 tnozaki None 0 May 25 23:46 /tmp/unko.CrhtWx
$ rm $f

作業用ディレクトリであれば

$ d=`mktemp -d -p /tmp unko.XXXXXX`
$ ls -dl $d
drwx------+ 1 tnozaki None 0 May 25 23:47 /tmp/unko.ygEwOO
$ rmdir $d

どちらのケースでも

  • XXXXXXの部分が乱数を元に生成された英数字62種に置換えられてるので、62^6通りのファイル名が生成 → 予測不可能なファイル名
  • パーミッションもファイルなら0600、ディレクトリなら0700で他のユーザーからは読み書きできない → 適切なアクセスコントロール
  • 作成したファイル・ディレクトリは既に存在したものを上書きしてないことが保証される → 競合状態の回避

というさっきのセキュアコーディングガイドで触れられている原則が、このコマンドを使うだけで保証できる訳。

ちなみにmktemp(1)の内部ではC APIである

  • mkstemp(3) … 作業用ファイル作成
  • mkdtemp(3) … 作業用ディレクトリ作成

が呼ばれているはずだ、以下はオレオレN6のコードより。

134                 if (dflag) {
135                         if (mkdtemp(name) == NULL) {
...
144                 } else {
145                         fd = mkstemp(name);
...
156                 }

なおコマンドと同名のmktemp(3)は設計ミスで危険な関数なので決して使ってはならない、POSIX:2008でめでたく抹殺されました。

ちなみにSolarisとかAIXなんかの商用UNIXしか経験の無いシェルスクリプトコーダーには、わりと最近までmktemp(1)が存在しなかった関係上「プログラム名 + プロセスID + 日付時刻」みたいなザルなコード書く輩が多いという印象がある。

$ f=`date +"/tmp/$0-$$-%Y-%m-%d_%H:%M:%S"`
$ touch $f
$ ls -l $f
-rw-r--r-- 1 tnozaki None 0 5月  26 00:55 /tmp/unko.sh-1362-2019-05-26_00:55:57
$ rm $f

こーゆーのみかけたら見次第殺。

だからshebang含めて3行以上のシェルスクリプトは書きたくない読みたくないのだよ心臓が止まりそうになる、なぜこれじゃダメなのかはもうお判りですね?

@PowerShellにおけるmktemp(1)の代替品

これはPowerShellも同様の無関心さだったようで、mktemp(1)に該当するコマンドレットであるNew-TemporaryFileは5.0になってようやく実装された始末。

$tmpfile = New-TemporaryFile

それ以前の環境であれば、.Net FrameworkのSystem.IO.Path::GetTempFileNameを使うしかない。

$tmpfile = [System.IO.Path]::GetTempFileName()

ちなみにNew-TemporaryFileの実装( Microsoft.PowerShell.Commands.NewTemporaryFileCommand)もGetTempFileNameを呼出してるだけなのだ。

 16     public class NewTemporaryFileCommand : Cmdlet
 17     {
...
 29                     filePath = Path.GetTempFileName();
...

そして.Net Frameworkの System.IO.Pathのコードを読むと

166        public static string GetTempFileName()
167        {
...
176            uint result = Interop.Kernel32.GetTempFileNameW(
177                ref tempPathBuilder.GetPinnableReference(), "tmp", 0, ref builder.GetPinnableReference());

と、Windows APIのFileAPI.hにある GetTempFileNameWを呼んでるだけなので、詳しい挙動はそっちを参照ってことですな。

しかし困ったことに作業用ファイルの作成はこいつら使えばいいんだけど、作業用ディレクトリを作成する方法については未だにPowerShellにも.Net FrameworkにもWindows APIにもご用意されていないのだ、ファッキン。

@よくあるまちがい

ちなみに作業用ディレクトリを掘れといわれてワンワンワン、セキュリティに理解の浅いプログラマー未満のやらかしがちな失敗は

  • System.IO.Path::GetRandomFileNameを使ってランダムな名前で作業用ディレクトリを掘ればいい
    $tmpdir = [System.IO.Path]::GetRandomFileName()
    New-Item -ItemType Directory $tmpdir
    
  • 作業用ファイルを作成した後に削除し再び同名で作業用ディレクトリを掘ればいい
    $tmpdir = New-TemporaryFile
    Remove-Item $tmpdir
    New-Item -ItemType Directory $tmpdir
    

みたいなコードを書いてしまいがちなんだけど、これどちらもNew-Itemするまでのわずかな時間に「TOCTTOU(Time Of Check To Time Of Use)」と呼ばれる競合状態が発生する可能性があるのでアウトなのだ、TOCTTOUについては 過去回で説明を書いてるので今回は省略。

@本当にそのコードって安全?

んで話脱線するけども、前回Import-Csvがパイプからファイル読めないから作業用ファイルを経由するというサンプル書いたけど

[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$tmpfile = New-TemporaryFile
Invoke-WebRequest -Method Get -Uri 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv' -OutFile $tmpfile
Import-Csv -Encoding OEM $tmpfile | Where-Object { ([DateTime]$_.{国民の祝日・休日月日}).Year -eq 2019 } | ForEach-Object {
	Write-Host $_.{国民の祝日・休日月日}
}
Remove-Item -Force $tmpfile

これだってパラノイアこじらせると怪しいコードに見えてくるのだ、Invoke-WebRequestの-OutFileの挙動がリダイレクトの「>」つまり上書き相当なら問題ないけれど、これが「削除 → 新規作成」という内部動作しとったらやはりTOCTTOUが発生する可能性がある。

なので安心したいので念の為に仕様を確認しておこう、PowerShellのコア部分はMITライセンス下のオープンソースなのでさっさとソースを読むことにする。

  • Microsoft.PowerShell.Commands.InvokeWebRequestCommand
    16     public class InvokeWebRequestCommand : WebRequestPSCmdlet
    17     {
    ...
    32         internal override void ProcessResponse(HttpResponseMessage response)
    33         {
    ...
    52             if (ShouldSaveToOutFile)
    53             {
    54                 StreamHelper.SaveStreamToFile(responseStream, QualifiedOutFile, this);
    55             }
    
  • Microsoft.PowerShell.Commands.WebRequestPSCmdlet
     87     public abstract partial class WebRequestPSCmdlet : PSCmdlet
     88     {
    ...
    369         [Parameter]
    370         public virtual string OutFile { get; set; }
    ...
    
    683         internal bool ShouldSaveToOutFile
    684         {
    685             get { return (!string.IsNullOrEmpty(OutFile)); }
    686         }
    ...
    
  • Microsoft.PowerShell.Commands.StreamHelper
    261     internal static class StreamHelper
    262     {
    ...
    326         internal static void SaveStreamToFile(Stream stream, string filePath, PSCmdlet cmdlet)
    327         {
    ...
    338                 using (FileStream output = File.Create(filePath))
    339                 {
    340                     WriteToStream(stream, output, cmdlet);
    341                 }
    ...
    

このあたりのコードをざっと読むと

  • Invoke-WebRequestコマンドレットの実体はInvokeWebRequestCommandクラスである
  • InvokeWebRequestCommandクラスはWebRequestPSCmdltクラスを継承してる
  • Invoke-WebRequestの-OutFileオプションに指定された引数はWebRequestPSCmdltクラスのOutFileプロパティに格納されている
  • OutFileプロパティが空でない場合、StreamHelperクラスのSaveStreamToFileメソッドが呼ばれる
  • SaveStreamToFileメソッドの中ではSystem.IO.File::Createメソッドが呼ばれる
  • Createメソッドの仕様としてはすでにファイルが存在する場合は上書きモードになる

ということなのでTOCTTOU問題は回避できるので一安心ですな。

だいぶ脱線した、これもストリームに流れるのがテキストでなくオブジェクトなので迂闊にリダイレクトが使えないPowerShellのクソデザインがそもそも悪手なのだ、テキストなら「>」か「>>」使えれば一目瞭然なんだよな、だから 最初に門倉元投手の言葉を借りて「PowerShellだけはやめとけよ」といいたくなるのもお分かりいただけるだろうか。

@次回

作業用ディレクトリを作成する方法なんだけどこれはもう自分で実装するより他にないのだ、セキュリティのためには少なくとも

  • 予測不可能なファイル名
  • 適切なパーミッションを設定する
  • 既に存在するディレクトリとは絶対に被ってはならない

は必須になるのだけれど、これをどう実現したものか説明しようと思う。

2019/5/27(Mon)

[Windows] PowerShellで生活するために - New-TemporaryFileコマンドレット編(その2)

前回はNew-TemporaryFileコマンドレットを紹介し、これで一時ディレクトリに作業用ファイルは作れるけど作業用ディレクトリは掘れないやんけ!ってお話。

無いならNew-TemporaryDirectoryを作ればいいじゃない、でもどうやってセキュリティを担保すればいい?

@予測不能な名前でディレクトリを作成する

なんか難しそうだなーと思うかもしれないけど実はそう大したもんじゃない。

予測可能な名前というのは

  • 連番
    workdir_0001
    workdir_0002
    ...
    
  • 現在時刻
    workdir_2019-05-24-00:00:00:000
    workdir_2019-05-25-23:59:59:999
    …
    

みたいな攻撃者が容易に次に作成されるファイルの名前を、金曜日の海軍の夕食メニューは?レベルで的中させられるような法則性を避けろって程度の話。

なので前回の失敗例でとりあげたSystem.IO.Path::GetRandomFileNameだけども、この条件であれば満たしてはいるのだ。

$workdir = [System.IO.Path]::GetRandomFileName()
Write-Host $workdir

これを実行すると

23zgemoa.mqf

と乱数から生成した8.3形式のファイル名(英数小文字)を返すので、可能性は36^11通りとなる。

あるいはInstallShieldなんかのインストーラーのようにGUIDを使ってもいい、.Net Frameworkの GUID構造体を使えばかんたん。

$workdir =  [Guid]::NewGuid().ToString("B")
Write-Host $workdir

これを実行すると

{efd60a90-6ba7-489d-af7b-39c755cb7f87}

と128ビットのうちバリアントとバージョンのためビットを除いた122ビットを乱数で埋め、さらに16進を[0-9a-f]の文字に置換えた文字列を出力するので、可能性は2^122通りになる。

あとは重篤なパラノイアを患っていると乱数が本当に乱数かどうか心配になって夜も眠れなくなるけどさすがにそこまでは面倒みきれん、ハードウェア乱数生成器でも買ってください。

なお前回ちらっと触れたmktemp(3)というタッパーウェアにあった古い関数は「XXXXX」で表される6桁のテンプレート部分を

  • プロセスID
  • アルファベット小文字から1文字

で埋めるだけなんですな。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int
main(void)
{
	char buf[BUFSIZ];
	snprintf(buf, sizeof(buf), "/tmp/%s.XXXXXX", getprogname());
	printf("current pid: %d\n", getpid());
	printf("mktemp(3) generated: %s\n", mktemp(buf));
}

これを実行すると

$ ./unko
current pid: 11451
mktemp(3) generated: /tmp/unko.11451a

という結果になる、プロセスIDなんぞ余裕でバレるので実質26通りの組合わせしか無いわけで総当り攻撃も余裕。

ちなみに改良されたmkstemp(3)とmkdtemp(3)は乱数を元に62^6通り、さらにテンプレートのXXXXXXを6桁よりも増やして更に可能性を増やすこともできる。

ただ注意が必要なのは、一部のlibcにおけるmkstemp(3)とmkdtemp(3)の実装は、mktemp(3)とまったく同じ生成規則な実質26通りのままで、総当り攻撃が有効なものもある(N6以前とかね)。 とはいえファイル名が予測可能だったとしてもその他の条件で攻撃は防げてるはずなので、ただちに危険というわけではないから無視してもいい。

@適切なアクセス権限でディレクトリを作成する

これPOSIX:2008でmktemp(3)が仕様から削除に至った脆弱性としては予測可能性よりこっちの方が重篤なのだ、こいつは名前を生成するだけの関数なので

char path[PATH_MAX];
snprintf(path, sizeof(buf), "/tmp/%s.XXXXXX", getprogname());
mktemp(path)
mkdir(path, 0777)

のようにプログラマの無知によって不適切なアクセス権限でファイルやディレクトリを作成することをどうやっても防げないのも理由の一つ。 mkstemp(3)やmkdtemp(3)は内部で

  • ファイルなら0600
  • ディレクトリなら0700

で作成したものを返すので、たとえ予測可能な名前であっても権限無ければ攻撃できないのだ。

140                 if (doopen) {
141                         if ((*doopen =
142                             open(path, O_CREAT | O_EXCL | O_RDWR, 0600)) >= 0)
...
146                 } else if (domkdir) {
147                         if (mkdir(path, 0700) >= 0)
...

ではタッパーウェアではなくWindows、それもPowerShellの場合はどうやって適切なアクセス権限でディレクトリを作成すればいいのか。

ディレクトリを作成するコマンドレットは

New-Item -Path 親ディレクトリ -Name ディレクトリ名 -ItemType Directory

とNew-Itemコマンドレットを使用する(mkdirもコマンドでなくこいつのaliasになる)のだけど、こいつのソース Microsoft.PowerShell.Commands.FileSystemProviderを確認すると、実際に実行されるのは.Net FrameworkのSystem.IO.Directory::CreateDirectoryとなっている。

  11 using System.IO;
...
2196         protected override void NewItem(
2197             string path,
2198             string type,
2199             object value)
2200         {
...
2224             itemType = GetItemType(type);
2225
2226             if (itemType == ItemType.Directory)
2227             {
2228                 CreateDirectory(path, true);
2229             }
...
2697         private void CreateDirectory(string path, bool streamOutput)
2698         {
...
2738                     var result = Directory.CreateDirectory(Path.Combine(parentPath, childName));

そんで System.IO.Directoryクラスのドキュメントを確認するとCreateDirectoryには

  • CreateDirectory(String) … Creates all directories and subdirectories in the specified path unless they already exist.
  • CreateDirectory(String, DirectorySecurity) … Creates all the directories in the specified path, unless the already exist, applying the specified Windows security.

という2種類の狂い咲きオーバーロードがある、それぞれの違いは

  • 前者は作成するディレクトリに対してデフォルトのアクセス権を設定する
  • 後者は明示的にアクセス権を設定する

という違いがあるのだけど、New-Item -ItemType Directoryは前者しか呼んでいないので明示的にアクセス権を設定することができないのだ、なんてこったい。

ただこれもまた直ちに影響というわけではない、Windows NT系の場合一時ディレクトリは各ユーザー毎にご用意されるいわゆる「per user tmp」というやつで、環境変数TMP(あるいはTEMP)には

  • %USERPROFILE%\Local Settings\Temp … XP以前
  • %USERPROFILE%\App\Local\Temp … Vista以降

以下が指定されている、そしてこれとは別にSYSTEMユーザーなどが使う環境変数TMP(あるいはTEMP)には

  • %SYSTEMROOT%\Temp

以下が指定されている。

前者の一時ディレクトリを含む%USERPROFILE%(例えばC:\Users\ユーザー名)以下はデフォルトでは

  • SYSTEMユーザ
  • Administratorsグループ
  • 当該ユーザ

以外にはアクセス権限が無いので、CreateDirectory(String)を使って親ディレクトリのアクセス権限を引継いだまま作成してれば、いちいちSet-Acl呼ばなくても攻撃者からは参照できないので安全だと主張もできなくはない。

ただしSYSTEMユーザなどが使う%SYSTEMROOT%\Temp以下は上記に加えて

  • Creator Ownerユーザ(ファイル作成者)
  • Usersグループ

にもファイルやディレクトリ作成と読み書きが許可されていて(若干の制限はある)、セキュリティは緩めなのよね。

なので%SYSTEMROOT%\Tempを使うユーザー権限で動作するスクリプトを書く場合、ファイル名の予測不可能性が破られた時の事を考えてきっちりアクセス権を設定しておかないとアウトなのだ。

それに環境変数なんていくらでも汚染できるので、TMPあるいはTEMPがWindows 9x系までの頃の流儀であるC:\Tempに書き換えられてたり、SSDの寿命を延ばすためD:\Tempとか自分で掘ってそっち使うようにレジストリ含めて変更してる人もいるしな…そういう人アクセス権限とその継承を正しく設定してるとは思えん。

世の中のPowerShell使いたちはどうしてるのか、我々はその謎を解き明かすべくStackOverflowやQiitaといった未開人の住まうジャングルの奥地へと向かった、そこで目にした光景は

$workdir = [System.IO.Path]::GetTempPath() + [System.IO.Path]::GetRandomFileName()
New-Item -Path $workdir -ItemType Directory
$alc = Get-Acl -Path $workdir
$alc.SetAccessRuleProtection($true, $false)
$alc.Access | ForEach-Object {
	$alc.RemoveAccessRule($_)
}
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule (
	[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
	[System.Security.AccessControl.FileSystemRights]::FullControl,
	([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
	[System.Security.AccessControl.PropagationFlags]::NoPropagateInherit,
	[System.Security.AccessControl.AccessControlType]::Allow
)
$alc.AddAccessRule($rule)
# New-Item -> Set-Acl is not Atomic ops, there's TOCTTOU race condition security problem.
Set-Acl -Path $workdir -AclObject $acl
(Get-Acl -Path $workdir).Access
...
Remove-Item -Force -Path $workdir

とディレクトリを作成した後に、Set-Aclコマンドレット(あるいはicalcsコマンド)でアクセス権限を設定し直すというコード例であった。

賢明なプログラマならお気づきだろうけど、New-ItemとSet-Aclに操作が分割されているのでアトミックではない、よってこのわずかな時間を利用してシンボリックリンク攻撃などを成功させてしまう可能性があるのだよね。 まぁアクセス制限を緩める方向性(例えばファイル共有用に誰でも読み書きできるようにするとか)ならええけどさあ…

なのでアトミックに作業用ディレクトリを掘るのであれば内部的にCreateFile(String)を使ってるNew-Itemは禁止、オーバーロードのCreateFile(String, DirectorySecurity)の方を呼ぶことでディレクトリ作成とアクセス権設定を同時にやらんとアカン。

$workdir = [System.IO.Path]::GetTempPath() + [System.IO.Path]::GetRandomFileName()
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule (
	[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
	[System.Security.AccessControl.FileSystemRights]::FullControl,
	([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
	[System.Security.AccessControl.PropagationFlags]::NoPropagateInherit,
	[System.Security.AccessControl.AccessControlType]::Allow
)
$acl.AddAccessRule($rule)
# Atomic ops, there's no TOCTTOU race condition.
[System.IO.Directory]::CreateDirectory($workdir, $acl)
(Get-Acl -Path $workdir).Access
...
Remove-Item -Force -Path $workdir

うーんこの、WindowsのACL複雑すぎんよー…というかCreateDirectory(String, DirectorySecurity)がアトミック操作なのか心配になってきたゾ。

@次回

残りの「競合状態の回避」をどうやって実装するか、そしてNew-TemporaryFileに対してNew-TemporaryDirectoryとでも名付ければいいのか、コマンドレットっぽく使える関数を作るところまで書けたらいいですね…(かなり飽きた)。

2019/5/28(Tue)

[Windows] PowerShellで生活するために - New-TemporaryFileコマンドレット編(その3)

前回はNew-TemporaryFileでは作成できない作業用ディレクトリの作成のため、New-TemporaryDirectory(仮)をどうやって実装したらいいのかというお話の途中まで。

今回は残作業を片づける

なお

@競合状態を回避する(既に存在するディレクトリとは絶対に被らない)

ファイル名の予測不可能性にも限界はあるので、ファイル名の可能性が少ない場合総当り攻撃が有効となる。 例えばN6以前のmkstemp(3)やmkdtemp(3)は実質26通りの組合わせしかないので、総当り攻撃はピースオブケーキだ。

いやケーキ一切れって言うほど簡単か?(甘いもの苦手マン)

これを防ぐには、同名のファイルかディレクトリが存在していたら、改めて別の名前で再試行するかエラーを返さないとまずい。

現在までのコード

function New-TemporaryDirectory() {
	$workdir = [System.IO.Path]::GetTempPath() + [System.IO.Path]::GetRandomFileName()
	$acl = New-Object System.Security.AccessControl.DirectorySecurity
	$rule = New-Object System.Security.AccessControl.FileSystemAccessRule (
		[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
		[System.Security.AccessControl.FileSystemRights]::FullControl,
		([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
		[System.Security.AccessControl.PropagationFlags]::NoPropagateInherit,
		[System.Security.AccessControl.AccessControlType]::Allow
	)
	$acl.AddAccessRule($rule)
	[System.IO.Directory]::CreateDirectory($workdir, $acl)
}

このままだと既に同名のディレクトリが存在するケースは一切想定していないので、競合状態が発生しでセキュリティ的によろしくないのだ。

じゃあどうすりゃいいかというと、同名のディレクトリが存在したらCreateDirectoryは失敗するのでそれをハンドリングし、成功するまで繰り返せばよいだけ。

function New-TemporaryDirectory() {
	$acl = New-Object System.Security.AccessControl.DirectorySecurity
	$rule = New-Object System.Security.AccessControl.FileSystemAccessRule (
		[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
		[System.Security.AccessControl.FileSystemRights]::FullControl,
		([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
		[System.Security.AccessControl.PropagationFlags]::NoPropagateInherit,
		[System.Security.AccessControl.AccessControlType]::Allow
	)
	$acl.AddAccessRule($rule)
	$tmpdir = [System.IO.Path]::GetTempPath()
	for (;;) {
		$path = $tmpdir + [System.IO.Path]::GetRandomFileName()
		try {
			[System.IO.Directory]::CreateDirectory($path, $acl)
		} catch {
			continue;
		}
		return $path
	}
}

あとはそもそも書込み権限が無いなどのケースで無限ループに入らないように、catchステートメントで補足する例外を絞ればいいはず。

ガハハ、勝ったな風呂入ってくる。

@ち~ん(笑)

残念でした(試合結果33-4)、System.IO.Directory::CreateDirectoryはすでにディレクトリが存在する場合であっても例外は飛ばないのだ。なんだこのクソ言語。

ちなみに他の言語のバヤイ

  • C … mkdir(2)はEEXISTを返す
    $ cat >unko.c
    #include <sys/stat.h>
    #include <errno.h>
    #include <limits.h>
    #include <paths.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    
    int
    main(void)
    {
            char path[PATH_MAX];
    
            strlcpy(path, _PATH_TMP, sizeof(path));
            strlcat(path, "{97D12011-4903-44E0-8D3D-FE299CBFDE5F}", sizeof(path));
            if (mkdir(path, 0700))
                    puts(strerror(errno));
    }
    ^D
    $ make unko
    cc     unko.c   -o unko
    $ ./unko
    File exists
    
  • Java … java.nio.file.Files::createDirectory … java.nio.file.FileAlreadyExistsExceptionを投げる
    $ cat >Unko.java
    import java.io.IOException;
    import java.nio.file.FileAlreadyExistsException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    
    public class Unko {
    	public static void main(String[] argv) throws IOException {
    		try {
    			Path path = Paths.get(System.getProperty("java.io.tmpdir"), "{97D12011-4903-44E0-8D3D-FE299CBFDE5F}");
    			Files.createDirectory(path);
    		} catch (FileAlreadyExistsException e) {
    			e.printStackTrace();
    		}
    	}
    }
    ^D
    $ javac Unko.java
    $ java Unko
    java.nio.file.FileAlreadyExistsException: C:\Users\tnozaki\AppData\Local\Temp\{97D12011-4903-44E0-8D3D-FE299CBFDE5F}
    	at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:87)
    	at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
    	at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
    	at java.base/sun.nio.fs.WindowsFileSystemProvider.createDirectory(WindowsFileSystemProvider.java:505)
    	at java.base/java.nio.file.Files.createDirectory(Files.java:689)
    	at unko.Unko.main(Unko.java:13)
    

なんだけどね、Java6以前…?もう時代はJava12ですよ…?

なので他の人はどうしてるかの調査に、再びStackOverflowやQiitaといったジャングルの奥地へ向かうと

	if ([System.IO.Directory]::Exists($path)) {
		[System.IO.Directory]::CreateDirectory($path, $acl)
	}

とPowerShellもC#も事前にディレクトリの存在チェックをすればいいなどというTOCTTOUな競合状態なにそれのクソコードしかありゃしねえ、こいつら消滅しねえかなぁ…

さすがにうっそだろお前とWindows APIのFileAPI.hにあるCreateDirectoryを確認したんだけど、こっちは既にディレクトリが存在する場合にはERROR_ALREADY_EXISTSを返すとなってる、ということで.Net Frameworkの実装がクソなんやなこれ…

不思議なことに、System.IO.Directory::CreateDirectoryでなくNew-Item -ItemType Directoryの場合、-ErrorAction Stopを指定していれば

$path = [System.IO.Path]::GetTempPath() + '{97D12011-4903-44E0-8D3D-FE299CBFDE5F}'
New-Item -ItemType Directory $path
try {
	New-Item -ItemType Directory $path -ErrorAction Stop
} catch [System.IO.IOException] {
	if ($_.Exception.Message -match 'already exists.$') {
		Write-Host $_.Exception.Message
	}
}

これを実行すると

New-Item : An item with the specified name C:\Users\tnozaki\AppData\Local\Temp\{97D12011-4903-44E0-8D3D-FE299CBFDE5F} already exists.
At line:4 char:5
+     New-Item -ItemType Directory $worldir -ErrorAction Stop
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ResourceExists: (C:\Users\tnozak...D-FE299CBFDE5F}:String) [New-Item], IOException
    + FullyQualifiedErrorId : DirectoryExist,Microsoft.PowerShell.Commands.NewItemCommand

ちゃんと例外飛ぶんよね、他の原因のエラーと判別できねえSystem.IO.IOExceptionってのがかなり気に食わないけど、移植性や国際化を一切考慮しないクソコード上等で、例外メッセージ中の「already exists」でもひっかけりゃいい。

ということで一縷の望みを持って該当のエラーメッセージを元にPowerShellのコードを検索してみるとだな…

...
2697         private void CreateDirectory(string path, bool streamOutput)
2698         {
...
2710             if (!Force && ItemExists(path, out error))
2711             {
2712                 string errorMessage = StringUtil.Format(FileSystemProviderStrings.DirectoryExist, path);
2713                 Exception e = new IOException(errorMessage);
2714
2715                 WriteError(new ErrorRecord(
2716                     e,
2717                     "DirectoryExist",
2718                     ErrorCategory.ResourceExists,
2719                     path));
2720
2721                 return;
2722             }
...
2736                 if (ShouldProcess(resource, action))
2737                 {
2738                     var result = Directory.CreateDirectory(Path.Combine(parentPath, childName));

あっ…(察し)、やってることはさっきのStackOverflowなんかに転がってる違反コードと一緒なんやな…だから毎回言ってるだろPowerShellだけはやめとけよと!

んああああああ、Life with PowerShell: A Guide for EveryoneどころかNo Life with PowerShell: Destroy, Kill All Hippiesなんやな。

ということで競合状態を回避してディレクトリを掘るには、Windows APIのFileAPI.hにあるCreateDirectoryを直接呼出す以外に無いわけ。もう PowerShell Coreとか CoreFX(.Net Core)のLinux移植の事まで考える気力は残ってねぇぞ…

@次回

もうめちゃくちゃだよ(怒)、もうC++かC#でモジュール書いたろかって気分だけど、それだと面白くないのでただ今よりPowerShellでWindows APIを使う訓練を開始する、もっと面白くねえわ○すぞ(ガチギレ)。

2019/5/30(Thu)

PowerShellで生活するために - Add-Typeコマンドレット編(その1)

前回の続き、どんどん話が脱線していくけどしょせんは自分用のチラシの裏なのでどうでもいい、なんせPowerShellからWindows APIを呼ばないとセキュリティの基本も満たせないようなクソザコナメクジ言語が悪いよー。

@ネイティブコードを呼びだす(Java JNI編)

なんでPowerShellとは何の関係も無いJavaの話なんですか!まぁシェフの気まぐれサラダ並みに何も考えてない備忘録がてら。なんせチラシの裏だからな。

ネイティブコード呼ぶにはJNI(Java Native Interface)という規則に従って醜悪なC/C++によるブリッジコードを書く羽目になるのだけど、Java屋にC/C++書かせたらそりゃそのラッパー部分ですらメモリリーク起こすしスレッドアンセーフなコード書いてJVMも毎日クラッシュしますがな *1

まぁJavaは前述の通りJava6以降ならjava.nio.file.Files::createDirectoryがjava.nio.file.FileAlreadyExistsException投げるので、今回やりたいこと的にはネイティブコード書く必要も無いんだけど、いまだにJava1.4以降の機能はすべて禁止の縛りプレイやっとる地獄ありそうだしな(しろめ)。

まずJava側ではnative修飾子を使ってメソッド定義だけ書く、NewTemporaryDirectoryとでもしておこう。

package unko;

import java.io.IOException;

public class Unko {
	static {
		System.loadLibrary("unko");
	}
	public native static String NewTemporaryDirectory() throws IOException;
	public static void main(String[] argv) throws IOException {
		System.out.println(NewTemporaryDirectory());
	}
}

System.loadLibraryの引数がDLLなんかの共有ライブラリ名、Windowsならunko.dllが検索されてロードされる。

そんでお次はJava9でjavahコマンドはdeprecatedとなり削除されとったので、javacコマンドで

$ javac -h ヘッダ出力先 Unko.java

としてヘッダファイルを出力する、するとこんなのが出力される。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class unko_Unko */

#ifndef _Included_unko_Unko
#define _Included_unko_Unko
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     unko_Unko
 * Method:    NewTemporaryDirectory
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_unko_Unko_NewTemporaryDirectory
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

あとはこいつの実体を実装し、unko.dllをビルドしてやればおk。

#include <Windows.h>
#include <combaseapi.h>
#pragma comment(lib, "ole32.lib")
#include <strsafe.h>

#include "unko_Unko.h"

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

JNIEXPORT jstring JNICALL
Java_unko_Unko_NewTemporaryDirectory(JNIEnv* env, jclass class)
{
	SECURITY_ATTRIBUTES sa;
	DWORD tmplen;
	WCHAR tmp[MAX_PATH], path[MAX_PATH];
	GUID guid;
	LPOLESTR guidstr;

	/* XXX: null security descriptor means default security, so this is NOT SAFE. */
	sa.nLength = sizeof(sa);
	sa.lpSecurityDescriptor = NULL;
	sa.bInheritHandle = TRUE;
	tmplen = GetTempPathW(arraycount(tmp), tmp);
	if (StringCchCopyW(path, arraycount(path), tmp) == S_OK) {
		for (;;) {
			if (CoCreateGuid(&guid) != S_OK)
				break;
			if (StringFromCLSID(&guid, &guidstr) != S_OK)
				break;
			if (StringCchCopyW(path + tmplen, arraycount(path) - tmplen, (LPCWSTR)guidstr) != S_OK) {
				CoTaskMemFree(guidstr);
				break;
			}
			CoTaskMemFree(guidstr);
			if (CreateDirectoryW(path, &sa))
				return (*env)->NewString(env, (const jchar*)path, (jsize)lstrlenW(path));
			if (GetLastError() != ERROR_ALREADY_EXISTS)
				break;
		}
	}
	(*env)->ThrowNew(env, (*env)->FindClass(env, "java/io/IOException"), "something wrong.");
	return NULL;
}

アクセス権周りのコードまで例に入れると長いので省略よってデフォルトのアクセス権がつくことに注意、なので作業用ディレクトリとして使うには安全でない。 PowerShellで書いてすらあの長さなのでWindows API使ってC/C++で書いたら地獄となる典型的なコードだからな…

@ネイティブコードを呼びだす(Java JNA編)

さすがにJNIによる被害多数により、ブリッジコード不要なJNA(Java Native Access)という規格が作られ、現在では以下のコードのようにすべてJavaで書くことも可能となっている *2

package unko;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.UUID;

import com.sun.jna.IntegerType;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.win32.W32APIOptions;

public class Unko {
	interface WinError {
		static final int ERROR_ALREADY_EXISTS = 183;
	}
	public static class DWORD extends IntegerType {
		public DWORD() {
			this(0);
		}
		public DWORD(long value) {
			super(4, value, true);
		}
	}
	public static class BOOL extends IntegerType {
		public BOOL() {
			this(false);
		}
		public BOOL(boolean value) {
			super(4, value ? 1 : 0, true);
		}
	}
	@FieldOrder({"nLength", "lpSecurityDescriptor", "bInheritHandle"})
	public static class SECURITY_ATTRIBUTES extends Structure {
		public DWORD nLength;
		public Pointer lpSecurityDescriptor;
		public BOOL bInheritHandle;
	}
	public interface Kernel32 extends Library {
		Kernel32 INSTANCE = (Kernel32)Native.load("kernel32", Kernel32.class, W32APIOptions.DEFAULT_OPTIONS);
		boolean CreateDirectory(String lpPathName, SECURITY_ATTRIBUTES lpSecurityAttributes);
		int GetLastError();
	}
	public static String NewTemporaryDirectory() throws IOException {
		/* XXX: null security descriptor means default security, so this is NOT SAFE. */
		SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
		sa.nLength = new DWORD((long)sa.size());
		sa.lpSecurityDescriptor = null;
		sa.bInheritHandle = new BOOL(true);
		String tmp = System.getProperty("java.io.tmpdir");
		for (;;) {
			String path = Paths.get(tmp, UUID.randomUUID().toString()).toString();
			if (Kernel32.INSTANCE.CreateDirectory(path, sa))
				return path;
			if (Kernel32.INSTANCE.GetLastError() != WinError.ERROR_ALREADY_EXISTS)
				break;
		}
		throw new IOException("something wrong.");
	}
	public static void main(String[] argv) throws IOException {
		System.out.println(NewTemporaryDirectory());
	}
}

長げえよクソが、ただでさえドイヒーな設計なWindows APIに大量に存在する独自な型定義をいちいちJavaでの定義に再翻訳する必要あって二重苦もいいとこですわやっぱりちゃんとC/C++書けるプログラマ連れてきた方が楽なんじゃねえの(テノヒラクルー)。

それにNobody RunsならぬRun Anywareが信条のJavaだし、直接システムのネイティブコードを呼ぶのではなくブリッジコードでOSの違いを吸収しとけってのも正しいんだけどね…

話を戻して、Run Anywhereを捨てて直接システムのネイティブコード叩くなら、Windows APIのようなWell Knownなものは contribにぜんぶ定義済のコードがあるので、わざわざ自分で書かずにjarつっこめば再翻訳の手間だけは省ける(楽になるとはいっていない)。

package unko;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.UUID;

import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinError;

class Unko {
	public static String NewTemporaryDirectory() throws IOException {
		/* XXX: null security descriptor means default security, so this is NOT SAFE. */
		WinBase.SECURITY_ATTRIBUTES sa = new WinBase.SECURITY_ATTRIBUTES();
		sa.lpSecurityDescriptor = null;
		sa.bInheritHandle = true;
		String tmp = System.getProperty("java.io.tmpdir");
		for (;;) {
			String path = Paths.get(tmp, UUID.randomUUID().toString()).toString();
			if (Kernel32.INSTANCE.CreateDirectory(path, sa))
				return path;
			if (Kernel32.INSTANCE.GetLastError() != WinError.ERROR_ALREADY_EXISTS)
				break;
		}
		throw new IOException("something wrong.");
	}
	public static void main(String[] argv) throws IOException {
		System.out.println(NewTemporaryDirectory());
	}
}

@ネイティブコードを呼びだす(C# P/Invoke編)

一方C#などの.Net Frameworkの場合、最初からこのJNAと似た「P/Invoke(Platform Invoke)」という仕組が用意されている。 なお「ピ○ボケ」と読んでしまうと放送禁止用語に抵触するので以下略、電気羊のイジドアかな?

そういえばC#っていちども仕事で使ったこと無いんだよね、なのでどういう言語かは

Borlandのリストラを逃れたHejlsbergとJ++を待っていたのはまた地獄だった
Microsoftを訴えたのはInpriseと名を変えた古巣とJavaのSun Microsystem、OSとRADの百年戦争が生み出した法廷闘争
J++とDelphi、Windows APIとOLE/COM/ActiveXをコンクリートミキサーにかけてぶちまけた
それがVisual Studioの最新リリース

次回「.NET」

来週もHejlsbergと地獄に付き合ってもらう

という認識でよかったんだっけ…まってC#作ったのになんでJ#作ったんだ…?

そいやかつて彷徨ってた見知らぬ街の住人たち、いまだに後生大事にVisual Basic 6.0を使い続けているのだろうか、まぁWindows 10 32bitでRuntimeサポートされ続けるそうだし使い続けるんだろうな…むせる。

んでC#でのコード例はとりあえずこんな感じ、アクセス権周りについてはやっぱり省略。

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace unko {
	class Unko {
		internal const int ERROR_ALREADY_EXISTS = 183;
		[StructLayout(LayoutKind.Sequential)]
		internal struct SECURITY_ATTRIBUTES {
			internal uint nLength;
			internal IntPtr lpSecurityDescriptor;
			internal bool bInheritHandle;
		}
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern bool CreateDirectory(string path, ref SECURITY_ATTRIBUTES lpSecurityAttributes);
		static String NewTemporaryDirectory() {
			/* XXX: null security descriptor means default security, so this is NOT SAFE. */
			SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
			sa.nLength = (uint)Marshal.SizeOf(sa);
			sa.lpSecurityDescriptor = IntPtr.Zero;
			sa.bInheritHandle = true;
			String tmpdir = Path.GetTempPath();
			for (;;) {
				String path = tmpdir + Path.GetRandomFileName();
				if (CreateDirectory(path, ref sa))
					return path;
				if (Marshal.GetLastWin32Error() != ERROR_ALREADY_EXISTS)
					break;
			}
			throw new IOException();
		}
		static void Main(string[] args) {
			Console.WriteLine(NewTemporaryDirectory());
		}
	}
}

いちいち構造体やらを再定義する手間はJNAに似てるけど *3、アトリビュート書くだけでネイティブコードをクラスメソッドにできるのはJNIのnative修飾子まではいかないけど手軽よね、しかもJNIと違ってブリッジコード不要だしJNAのようにpublicにする必要も無い。

Javaもどうでもいいとこにアノテーション乱用して地獄絵図になっとるのにこういうとこで使わないからクソ言語といわれるのだ。まぁちょっとP/Invokeのこのアトリビュート名はWindowsベタ過ぎるきらいはあるけどな…と思ったらMonoのドキュメント読んだら

[DllImport ("libc.so")]
private static extern int getpid();

うーんこの、LinuxでもDllImportなのか…

ところでマーシャルという用語をみるたびに何段にも積まれたギターアンプを想像するんだけど語源一緒なんだな、そもそも人名だったり軍隊とか警察のお偉いさんの意味なのに、なんでコンピューター用語では尻洗いズみたいな意味になっているのか…

@PowerShellからP/Invokeを使う

ようやく本題のPowerShellのお話、だが残念なことにPowerShellそのものにはP/Invokeの機能は備わっていないのだ。 しかしAdd-Typeコマンドレットというものを使うと、PowerShellスクリプト中にC#のコードが書けてるので、その中でP/Invokeすることで代用することができる。

Add-Typeに渡すC#コードは文字列なので、読みやすいようヒアドキュメント風に書くと読みやすい。

Add-Type -Language 'CSharp' -TypeDefinition @'
namespace unko {
	class Unko {
		...
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern bool CreateDirectory(string path, ref SECURITY_ATTRIBUTES lpSecurityAttributes);
		static String NewTemporaryDirectory() {
			...
		}
	}
}
'@
[unko.Unko]::NewTemporaryDirectory()

そういえば20年経ってもヒアドキュメント実装されてなくて 未だに提案止まりのクソ言語がありましたね…

ちなみに名前空間やクラス名はオプションでも指定可能、その場合は-TypeDefinitionでなく-MemberDefinitionを使う

Add-Type -Language 'CSharp' -Namespace 'unko' -Name 'Unko' -MemberDefinition @'
static String NewTemporaryDirectory() {
	...
}
'@
[unko.Unko]::NewTemporaryDirectory()

たいして省力化できないし判りづらいよなこれ。

まぁともかくバックグラウンドで

  • -TypeDefinition/-MemberDefinitionの引数の文字列が一時ファイルに書きだされ
  • -Languageで指定した言語に対応するコンパイラが一時ファイルをコンパイル
  • ビルドされたライブラリ(アセンブリ)をPowerShellのAppDomainに動的ロードされる

という感じ。

タッパーウェアシェルスクリプトでもCソースをヒアドキュメントで書いて、それを標準入力でコンパイラに食わせて実行ファイル作るみたいな苦し紛れ考えることはあるでしょ。

#!/bin/sh
f=/var/tmp/unko
gcc -xc - -o $f >/dev/null << EOF;
#include <paths.h>
#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
	char buf[PATH_MAX];
	char *path;
	strlcpy(buf, _PATH_TMP, sizeof(buf));
	strlcat(buf, "unko.XXXXXX", sizeof(buf));
	path = mkdtemp(buf);
	if (path == NULL)
		exit(EXIT_FAILURE);
	puts(path);
	exit(EXIT_SUCCESS);
}
EOF
tmpdir=`$f`
rm -f $f
...

ところであなたがお使いの開発マシンだけでなく、実運用環境にもコンパイラがあるってちゃんと確認しましたか(小声)。

PowerShellの場合はとくに.Net Framework SDKやVisual Studioなどがインストールされてる必要は無くランタイムだけでいいようなので、今回みたいに使わざるをえない状況なら使ってけという感じ。

ただし注意点はAdd-TypeはできてもRemove-Typeが存在しないので(AppDomainの仕様らしい)ちょこっと書換えて再実行とかすると

Add-Type : Cannot add type. The type name 'unko.Unko' already exists.
At line:1 char:1
+ Add-Type -Language 'CSharp' -Namespace 'unko' -Name 'Unko' -MemberDef ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (unko.Unko:String) [Add-Type], Exception
    + FullyQualifiedErrorId : TYPE_ALREADY_EXISTS,Microsoft.PowerShell.Commands.AddTypeCommand
 

とエラーが出て、古いコードが実行されて事故起こす可能性があるのがなんともアレ。-ErrorAction Stopとかしても例外は飛ばないようだしどうやって回避したもんですかね…

@次回

完全に飽きてるんだけど、残りのコード書いて完成させるよ…

*1:某所でWindowsから某商用UNIX向けに移植されたライブラリがグローバル変数だらけでJNIでクラッシュ以下略、機密漏洩とか大事故になるくらい動作してたら恐ろしいとこだったって通りすがりのイルカがいってた。
*2:とはいえJNAもJNIベースの技術なので最低限のブリッジコードはあるんだけどね、libffiベースで書かれててFreeBSDやOpenBSD用のバイナリもリリースに含まれている、なおNなんとかBSD(限界集落)
*3:こっちももしかするとJNAのcontribみたいに定義済のものあるのかもしれない、内部的にはCoreLibのInteropを使ってるみたいだけど公開されてるんかなこれ。

2019/5/31(Fri)

[Windows] PowerShellで生活するために - New-TemporaryFileコマンドレット編(その4)

@適切なアクセス権限でディレクトリを作成する

これこれの続き。

残課題として残ってた(馬に乗馬)、アクセス権の設定に関する部分だけど

  • C/C++でのCreateDirectory … アクセス権を表すSECURITY_DESCRIPTOR構造体を作ってSECURITY_ATTRIBUTE構造体に詰めて渡す
  • .Net FrameworkでのCreateDirectory … アクセス権を表すFileSystemAccessRuleクラスを作ってDirectorySecurityクラスに詰めて渡す

という違いがある、この違いをどうやって埋めるか。

ちなみにC/C++でSECURITY_DESCRIPTOR構造を作るには以下のチュートリアルを読んで絶望するといい

なんだこのクソ設計(しろめ)、ちなみにほとんどのケースでエラー処理省いてるのでちゃんと書こうと思うと倍以上の長さのコードになる。

特に個人的に泡吹いて倒れそうになるのがPACL構造体ポインタに割り当てるメモリサイズの計算に

  PACL pDacl = NULL;
  DWORD cbDacl = 0;

  // Calculate the amount of memory that must be allocated for the DACL.  
  cbDacl = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE)*3 - sizeof(DWORD)*3;  
  cbDacl += GetLengthSid(pTokenUser->User.Sid);  
  cbDacl += GetLengthSid(pEveryoneSid);  
  cbDacl += GetLengthSid(pTrustedUserSid);  
  
  // Create and initialize an ACL.  
  pDacl = (PACL) new BYTE[cbDacl];  

ってどんなハーブキメて設計するとこんなデザインになるんですかね、いやまあ構造体の後ろに可変長データくらいはCでよく使うけどさぁ。

普段からこういうコードばっかり書いてるとC死ねいいたくなるんだろうとは思う、Always Look On The Bright Side Of Cと歌いながら綺麗なコードだけ読んでる世界の人にはあまりピンと来ないんだけどね…

これをさらにP/Invoke経由でC#で書こうとかショットガンで自分の頭打ち抜くようなもの、幸いなことにそんな自殺行為をせんでもDirectorySecurityからSECURITY_DESCRIPTORに変換するメソッドがご用意されているので、そいつを使わせて貰って全力で回避する。

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;

namespace unko
{
	class Unko
	{
		internal const int ERROR_ALREADY_EXISTS = 183;
		[StructLayout(LayoutKind.Sequential)]
		internal struct SECURITY_ATTRIBUTES
		{
			internal uint nLength;
			internal IntPtr lpSecurityDescriptor;
			internal bool bInheritHandle;
		}
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern bool CreateDirectory(string path, ref SECURITY_ATTRIBUTES lpSecurityAttributes);
		static String NewTemporaryDirectory() 
		{
			DirectorySecurity ds = new DirectorySecurity();
			ds.AddAccessRule(new FileSystemAccessRule(
				WindowsIdentity.GetCurrent().Name,
				FileSystemRights.FullControl,
				InheritanceFlags.ContainerInherit| InheritanceFlags.ObjectInherit,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));
			SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
			sa.nLength = (uint)Marshal.SizeOf(sa);
			sa.bInheritHandle = false;
			byte[] sdb = ds.GetSecurityDescriptorBinaryForm();
			int sdn = sdb.Length;
			sa.lpSecurityDescriptor = Marshal.AllocHGlobal(sdn);
			try {
				Marshal.Copy(sdb, 0, sa.lpSecurityDescriptor, sdn);
				String tmpdir = Path.GetTempPath();
				for (;;) {
					String path = tmpdir + Path.GetRandomFileName();
					if (CreateDirectory(path, ref sa))
						return path;
					if (Marshal.GetLastWin32Error() != ERROR_ALREADY_EXISTS)
						break;
				}
			} finally {
				Marshal.FreeHGlobal(sa.lpSecurityDescriptor);
			}
			throw new IOException();
		}
		static void Main(string[] args)
		{
			Console.WriteLine(NewTemporaryDirectory());
		}
	}
}

このコード中のDirectorySecurity::GetSecurityDescriptorBinaryFormがそれなんだけど、扱いがちょっとややこしくて

  • DirectorySecurityはマネージドなメモリ(byte配列)で返す
  • SECURITY_ATTRIBUTESへはアンマネージドなメモリ(IntPtr)に変換して渡す

必要があるので変換が必要 *1、アンマネージドなメモリはMarshal::AlocHGlobalで取得できる、もちろんガベコレは面倒みてくれないのでメモリリークの無いよう、Marshal::FreeHGlobalで使い終わったら解放する。なんかフリーエッチグローバルって書くとフリー○ックスっぽくてアレやな。

ちなみにUNIXの場合root権限はどうあがいても無敵なので考慮する必要はないんだが、Windowsの場合だと管理者からも読めないディレクトリとなってしまうので *2、AdministratorsグループとかSYSTEMユーザにも許可を与える必要があるかもしれないので必要な方は適宜修正してどうぞ。

			ds.AddAccessRule(new FileSystemAccessRule(
				new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
				FileSystemRights.FullControl,
				InheritanceFlags.ContainerInherit| InheritanceFlags.ObjectInherit,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));
			ds.AddAccessRule(new FileSystemAccessRule(
				new SecurityIdentifier(WellKnownSidType.BuiltinSystemOperatorsSid, null),
				FileSystemRights.FullControl,
				InheritanceFlags.ContainerInherit| InheritanceFlags.ObjectInherit,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));

既知のユーザー一覧は System.Security.Principal.WellKnownSidTypeあたりを参照のこと、というかWindows ACL複雑すぎてほんと正解が判らん…

とりあえずここまで書いてAdd-TypeにくべてやればようやっとPowerShellでも

  • 他の誰からも読めない安全な作業用ディレクトリを
  • TikTokだかTOCTTTTTTTTTOUだかといった競合状態の心配なくアトミックに

掘ることができたわけだ、いやー大変でしたね…

Add-Type -TypeDefinition @'
...
namespace unko
{
	class Unko
	{
...
'@
[unko.Unko]::NewTemporaryDirectory()

@次回

このままでも十分実用できなくもないんだけど、NewTemporaryDirectoryの実行結果をパイプで他のコマンドレットに渡せないのが微粒子レベルでめんどくさい可能性があるので、unko.Unkoクラスをコマンドレットとして使えるようリファクタリングしてみる。

*1:アンセーフなプロジェクトとしてビルドすればfixedステートメントでもいけるらしいがここはセーフ○ックスで。
*2:まぁどっちみち管理者ならTakeownコマンドなんてあるくらいで権限割当て直せるから無駄に複雑になるだけよね…