The Man Who Fell From The Wrong Side Of The Sky:2019年6月1日分

2019/6/1(Sat)

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

前回の続きを書こうと思ったんだが、気づいたことがあったので予定変更。

@New-TemporaryFileそしてCreateTempFileNameの作るファイルのアクセス権もガバガバだった件

タイトルなのに初回以降まったくその存在にすら触れていなかったNew-TemporaryFileコマンドレットだけど、こいつで作成する一時ファイルも%USERPROFILE%\AppData\Local\Temp\なんかの親ディレクトリの権限を継承してることが前提で、本質的に安全でないようやね…Windows APIのGetTempFileNameからしてアウトなんやなこいつ。

なのでNew-TemporaryFileコマンドレットの安全な実装も作りますかね…アーメンド。

@CreateFileをP/Invokeする

前回のCreateDirectoryと同様にCreateFileもP/Invokeするだけで基本的な考えは一緒なんだけど、CreateFileの戻り値がインド人なことに要注意。

using System.Runtime.InteropServices;

	[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
	internal static extern IntPtr CreateFile(
		String lpFileName,
		uint dwDesiredAccess,
		uint dwShareMode,
		ref SECURITY_ATTRIBUTES lpSecurityAttributess,
		uint dwCreationDisposition,
		uint dwFlagsAndAttributes,
		IntPtr hTemplateFile
	);
	...
	for (;;) {
		IntPtr fh = CreateFile(path,
			0x80000000|0x40000000, /* GENERIC_READ|GENERIC_WRITE */
			0,
			ref sa,
			1, /* CREATE_NEW */
			/* FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_TEMPORARY|FILE_ATTRIBUTE_NOT_CONTENT_INDEXED */
			0x00000080|0x00000100|0x00002000,
			IntPtr.Zero
		)) {
		if (fh.ToInt64() != -1 /*INVALID_HANDLE_VALUE*/)
			return path;
		if (Marshal.GetLastWin32Error() != 80/*ERROR_FILE_EXISTS*/)
			break;
	}

密林の奥に住むStackOverflow民やQiita民は上記のようにHANDLEをIntPtrと定義してるケースが多いみたい、ところでCreateFileした後にちゃんとCloseHandleしてますか…(小声)?

ハンドルは開いたら閉じなければって見慣れたHANDLE型なら覚えてるだろうけど、IntPtrで定義しちゃうと元がHANDLEってこと忘れちゃう罠。 それにCloseHandleの定義も必要になるのでちょっとめんどくさい。

そんで楽をしたい人に耳寄りな情報、HANDLE型をラップする SafeHandleクラス、そしてファイルハンドルであればその継承クラスの SafeFileHandleクラスというのが標準で用意されているのだ。

こいつは IDisposableインタフェースを実装しているので usingステートメント内のブロックを抜けたらただちにDispose()を呼んで自動で解放をしてくれるし、ガベコレ対象になった時にも以下同文。そんで内部ではCloseHandleを呼んでるのでこれは安心ですね。

ちなみにSECURITY_ATTRIBUTES構造体も同様にメンバのSECURITY_DESCRIPTORのアンマネージドなメモリの解放漏れ防ぎてーなーと思ってIDisposable実装も考えたんだが、C#の構造体はインタフェースを実装できないのだ。ヘルパー書くのもかっこ悪いのでtry~finallyでお茶を濁したが、なんかいい方法ないですかね。

ということでざっと書き直し。

using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

	[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
	internal static extern SafeFileHandle CreateFile(
		String lpFileName,
		uint dwDesiredAccess,
		uint dwShareMode,
		ref SECURITY_ATTRIBUTES lpSecurityAttributess,
		uint dwCreationDisposition,
		uint dwFlagsAndAttributes,
		SafeFileHandle hTemplateFile
	);
	...
	SafeFileHandle HANDLE_NULL = new SafeFileHandle(IntPtr.Zero, false);
	for (;;) {
		using (SafeFileHandle fh = CreateFile(path,
			0x80000000|0x40000000, /* GENERIC_READ|GENERIC_WRITE *.
			0,
			ref sa,
			1, /* CREATE_NEW */
			/* FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_TEMPORARY|FILE_ATTRIBUTE_NOT_CONTENT_INDEXED */
			0x00000080|0x00000100|0x00002000,
			HANDLE_NULL
		)) {
			if (!fh.IsInvalid)
				return path;
			if (Marshal.GetLastWin32Error() != 80/*ERROR_FILE_EXISTS*/)
				break;
		}
	}

さらに

現状のコードの全体はこんな感じ。

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.Win32.SafeHandles;

namespace unko
{
	class Unko
	{
		internal const int RETRY_MAX = 100;
		internal enum Errors : int
		{
			ERROR_FILE_EXISTS = 80,
			ERROR_ALREADY_EXISTS = 183
		}
		internal enum DesiredAccess : uint
		{
			GENERIC_ALL     = 0x10000000,
			GENERIC_EXECUTE = 0x20000000,
			GENERIC_WRITE   = 0x40000000,
			GENERIC_READ    = 0x80000000
		}
		internal enum ShareMode : uint
		{
			FILE_SHARE_READ   = 0x00000001,
			FILE_SHARE_WRITE  = 0x00000002,
			FILE_SHARE_DELETE = 0x00000004
		}
		internal enum CreationDisposition : uint
		{
			CREATE_NEW        = 1,
			CREATE_ALWAYS     = 2,
			OPEN_EXISTING     = 3,
			OPEN_ALWAYS       = 4,
			TRUNCATE_EXISTING = 5
		}
		internal enum FlagsAndAttributes : uint
		{
			FILE_ATTRIBUTE_READONLY            = 0x00000001,
			FILE_ATTRIBUTE_HIDDEN              = 0x00000002,
			FILE_ATTRIBUTE_SYSTEM              = 0x00000004,
			FILE_ATTRIBUTE_DIRECTORY           = 0x00000010,
			FILE_ATTRIBUTE_ARCHIVE             = 0x00000020,
			FILE_ATTRIBUTE_DEVICE              = 0x00000040,
			FILE_ATTRIBUTE_NORMAL              = 0x00000080,
			FILE_ATTRIBUTE_TEMPORARY           = 0x00000100,
			FILE_ATTRIBUTE_SPARSE_FILE         = 0x00000200,
			FILE_ATTRIBUTE_REPARSE_POINT       = 0x00000400,
			FILE_ATTRIBUTE_COMPRESSED          = 0x00000800,
			FILE_ATTRIBUTE_OFFLINE             = 0x00001000,
			FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000,
			FILE_ATTRIBUTE_ENCRYPTED           = 0x00004000,
			FILE_ATTRIBUTE_VIRTUAL             = 0x00010000,
			FILE_ATTRIBUTE_VALID_FLAGS         = 0x00007fb7,
			FILE_ATTRIBUTE_VALID_SET_FLAGS     = 0x000031a7
		}
		[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 SafeFileHandle CreateFile(
			String lpFileName,
			DesiredAccess dwDesiredAccess,
			ShareMode dwShareMode,
			ref SECURITY_ATTRIBUTES lpSecurityAttributess,
			CreationDisposition dwCreationDisposition,
			FlagsAndAttributes dwFlagsAndAttributes,
			SafeFileHandle hTemplateFile
		);
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern bool CreateDirectory(
			String lpPathName,
			ref SECURITY_ATTRIBUTES lpSecurityAttributes
		);
		private static readonly SafeFileHandle HANDLE_NULL = new SafeFileHandle(IntPtr.Zero, false);
		private static readonly FileSecurity fs = new FileSecurity();
		private static readonly DirectorySecurity ds = new DirectorySecurity();
		static Unko()
		{
			fs.AddAccessRule(new FileSystemAccessRule(
				WindowsIdentity.GetCurrent().Name,
				FileSystemRights.FullControl,
				InheritanceFlags.None,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));
			ds.AddAccessRule(new FileSystemAccessRule(
				WindowsIdentity.GetCurrent().Name,
				FileSystemRights.FullControl,
				InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
				PropagationFlags.NoPropagateInherit,
				AccessControlType.Allow
			));
		}
		static String NewSafeTemporaryFile()
		{
			SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
			sa.nLength = (uint)Marshal.SizeOf(sa);
			sa.bInheritHandle = false;
			byte[] sdb = fs.GetSecurityDescriptorBinaryForm();
			int sdn = sdb.Length;
			sa.lpSecurityDescriptor = Marshal.AllocHGlobal(sdn);
			try {
				Marshal.Copy(sdb, 0, sa.lpSecurityDescriptor, sdn);
				String tmpdir = Path.GetTempPath();
				for (int i = 0; i < RETRY_MAX; ++i) {
					String path = Path.Combine(tmpdir, Path.GetRandomFileName());
					using (SafeFileHandle handle = CreateFile(path,
						DesiredAccess.GENERIC_READ|DesiredAccess.GENERIC_WRITE,
						0,
						ref sa,
						CreationDisposition.CREATE_NEW,
						FlagsAndAttributes.FILE_ATTRIBUTE_NORMAL|
						FlagsAndAttributes.FILE_ATTRIBUTE_TEMPORARY|
						FlagsAndAttributes.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
						HANDLE_NULL
					)) {
						if (!handle.IsInvalid)
							return path;
						if (Marshal.GetLastWin32Error() != (int)Errors.ERROR_FILE_EXISTS)
							break;
					}
				}
			} finally {
				Marshal.FreeHGlobal(sa.lpSecurityDescriptor);
			}
			throw new IOException("something wrong.");
		}
		static String NewSafeTemporaryDirectory() 
		{
			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 (int i = 0; i < RETRY_MAX; ++i) {
					String path = Path.Combine(tmpdir, Guid.NewGuid().ToString("B"));
					if (CreateDirectory(path, ref sa))
						return path;
					if (Marshal.GetLastWin32Error() != (int)Errors.ERROR_ALREADY_EXISTS)
						break;
				}
			} finally {
				Marshal.FreeHGlobal(sa.lpSecurityDescriptor);
			}
			throw new IOException("something wrong.");
		}
		static void Main(String[] args)
		{
			Console.WriteLine(NewSafeTemporaryFile());
			Console.WriteLine(NewSafeTemporaryDirectory());
		}
	}
}

@次回

前回の予告どおりにコマンドレット化を試みる。