Powershell, PInvoke and GetLastError

4.2k views Asked by At

I'm trying to use GetFileSecurity from Powershell via PInvoke. The call itself works, but both System.Runtime.InteropServices.Marshal.GetLastWin32Error and GetLastError (which I imported for testing) return the wrong error code. It seems that SetLastError = true in the PInvoke signature doesn't have any effect at all, since the first time the script is run after starting a new Powershell instance, both return 203 (ERROR_ENVVAR_NOT_FOUND), but if I call the same script again, both return 0.

As a quick test, I whipped up an equivalent C program, and GetLastError returns the expected value (122 == ERROR_INSUFFICIENT_BUFFER) there.

Note that I'm new to Powershell and .NET in general, so please excuse any obvious faux pas.

$signature = @'
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetFileSecurity(
  string lpFileName,
  int RequestedInformation,
  byte [] pSecurityDescriptor,
  uint nLength,
  out int lpnLengthNeeded
);

[DllImport("Kernel32.dll")]
public static extern uint GetLastError();
'@

$enum = @'
namespace Win32 {
  public enum SECURITY_INFORMATION : uint
  {
    OWNER_SECURITY_INFORMATION        = 0x00000001,
    GROUP_SECURITY_INFORMATION        = 0x00000002,
    DACL_SECURITY_INFORMATION         = 0x00000004,
    SACL_SECURITY_INFORMATION         = 0x00000008,
    UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000,
    UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000,
    PROTECTED_SACL_SECURITY_INFORMATION   = 0x40000000,
    PROTECTED_DACL_SECURITY_INFORMATION   = 0x80000000
  }
}
'@


Add-Type -TypeDefinition $enum
Add-Type -MemberDefinition $signature -Name API -Namespace Win32

$len = 0
$sec = New-Object byte[] 0
$info = new-object Win32.SECURITY_INFORMATION
$info = [Win32.SECURITY_INFORMATION]::OWNER_SECURITY_INFORMATION -bor
        [Win32.SECURITY_INFORMATION]::GROUP_SECURITY_INFORMATION -bor
        [Win32.SECURITY_INFORMATION]::DACL_SECURITY_INFORMATION
[Win32.API]::GetFileSecurity("c:\temp", $info, $sec, $sec.Length, [ref] $len)
[System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
[Win32.API]::GetLastError()
2

There are 2 answers

1
Keith Hill On BEST ANSWER

Rather that directly pinvoking, I would create a wrapper C# class that makes the GetFileSecurity call and then gets the error. It is possible that any calls PowerShell makes to Win32 API between your GetFileSecurity() pinvoke call and the GetLastWin32Error() pinvoke call is resetting the Win32 error number.

0
Phil On

You can keep this in PowerShell, as long as you prevent PowerShell from writing the result of GetFileSecurity() directly to the console. The call to GetLastWin32Error() must come immediately after the call to GetFileSecurity(), with no console writes or other Win32 API calls in between, as Keith Hill said.

You can silence the call by casting it to void:

[void]([Win32.API]::GetFileSecurity("c:\temp", $info, $sec, $sec.Length, [ref] $len))
[System.Runtime.InteropServices.Marshal]::GetLastWin32Error()

Or you can save its result to a variable and print it later:

$result = [Win32.API]::GetFileSecurity("c:\temp", $info, $sec, $sec.Length, [ref] $len)
[System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-Output $result