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

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コマンドなんてあるくらいで権限割当て直せるから無駄に複雑になるだけよね…