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

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を使う訓練を開始する、もっと面白くねえわ○すぞ(ガチギレ)。