Getting the Owner of a File

The simple task of getting the owner of a file is not supported by the "native" .Net Framework v1.1, and is poorly supported in .Net Framework v2.0. That means that instead of .Net "managed code", we are forced to use the Windows API.

Note: The new GetOwner() method of the FileSecurity and DirectorySecurity classes (in v2.0 Framework only) throws an exception when it attempts to resolve an unknown Security Identifier (SID) and can not be used to resolve an owner of a remote file

Note: There are other options available... You can use WMI's Win32_LogicalFileSecuritySetting methods, see Introduction to Windows Management Instrumentation for a discussion of WMI.

The ownership of a file is held inside a "Security Descriptor" for each file (on an NTFS partition). So, we use a bunch of Windows API calls to get the owner of the file from the Security Descriptor. All user accounts in Windows are stored behind the scenes as Security Identifiers (SIDs). When we use the API to get the file's owner, it returns the SID. We then have to look up the name of the user account associated with that SID.

API Declarations

Let's start with the API "boiler plate" stuff:

Imports System.Runtime.InteropServices
Imports System.ComponentModel
Imports System.IO

Private Enum SE_OBJECT_TYPE As Integer
    SE_UNKNOWN_OBJECT_TYPE = 0
    SE_FILE_OBJECT
    SE_SERVICE
    SE_PRINTER
    SE_REGISTRY_KEY
    SE_LMSHARE
    SE_KERNEL_OBJECT
    SE_WINDOW_OBJECT
    SE_DS_OBJECT
    SE_DS_OBJECT_ALL
    SE_PROVIDER_DEFINED_OBJECT
    SE_WMIGUID_OBJECT
    SE_REGISTRY_WOW64_32
End Enum

Private Enum SECURITY_INFORMATION As Integer
    OWNER_SECURITY_INFORMATION = 1
    GROUP_SECURITY_INFORMATION = 2
    DACL_SECURITY_INFORMATION = 4
    SACL_SECURITY_INFORMATION = 8
    PROTECTED_SACL_SECURITY_INFORMATION = 16
    PROTECTED_DACL_SECURITY_INFORMATION = 32
    UNPROTECTED_SACL_SECURITY_INFORMATION = 64
    UNPROTECTED_DACL_SECURITY_INFORMATION = 128
End Enum

Private Declare Auto Function GetNamedSecurityInfo Lib "advapi32.dll" ( _
    ByVal pObjectName As String, _
    ByVal ObjectType As SE_OBJECT_TYPE, _
    ByVal SecurityInfo As SECURITY_INFORMATION, _
    ByRef ppsidOwner As IntPtr, _
    ByRef ppsidGroup As IntPtr, _
    ByRef ppDacl As IntPtr, _
    ByRef ppSacl As IntPtr, _
    ByRef ppSecurityDescriptor As IntPtr _
) As Integer

Private Declare Auto Function LookupAccountSid Lib "advapi32.dll" ( _
    ByVal lpSystemName As String, _
    ByVal lpSid As IntPtr, _
    ByVal lpName As String, _
    ByRef cchName As Integer, _
    ByVal lpReferenceDomainName As String, _
    ByRef cchReferencedDomainName As Integer, _
    ByRef peUse As Integer _
) As Boolean

Private Declare Auto Function ConvertSidToStringSid Lib "advapi32.dll" ( _
    ByVal Sid As IntPtr, _
    ByRef StringSid As IntPtr _
) As Boolean

Private Const NAME_SIZE As Integer = 64

API Documentation Links

The Example Code

The first thing we do is use the GetNamedSecurityInfo API to get the SID of the owner. Notice the use of FreeHGlobal to clean up memory after we're done.

Dim UserName, MachineName, SidString, name, domain_name As String
Dim OwnerSid, SD, StringPtr As IntPtr
Dim ret, LastError, name_len, domain_len, peUse As Integer
Dim Win32Error As Win32Exception
Dim ObjectType As SE_OBJECT_TYPE

ObjectType = SE_OBJECT_TYPE.SE_FILE_OBJECT

' Get the Owner's SID.
ret = GetNamedSecurityInfo(Path, ObjectType, _
 SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION, OwnerSid, Nothing, _
 Nothing, Nothing, SD)
If ret <> 0 Then
     Return "Error"
End If

Marshal.FreeHGlobal(SD)

To resolve the SID into a human-readable name, we use the LookupAccountSid API. It's a strange function in that you must reserve room for the strings that it uses. Alternately, you could call the API with NULLs for the strings and it will do a "dry run" that only returns the correct sizes for the strings. This would require two calls... one to get the sizes, and a second one to get the SID. Look at Checking File Permissions for an example of using this technique (using LookupAccountName).

The error code 1332 means that we could not resolve the SID... this is not an error, per se... it's an indication that you've got a "dead SID". A dead SID would occur if you deleted an account, yet that old account still is the owner of some files.

' are we doing this remotely? If so, we need to resolve the SID remotely
If Path.StartsWith("\\") Then
    MachineName = Path.Split("\")(2)
Else
    MachineName = ""
End If

name_len = NAME_SIZE
domain_len = NAME_SIZE
name = Space(name_len)
domain_name = Space(domain_len)

' look up the Account associated with that SID
If LookupAccountSid(MachineName, OwnerSid, name, name_len, domain_name, _
 domain_len, peUse) = False Then
    LastError = Marshal.GetLastWin32Error()
    ' this means we've got a "dead" SID, so just return the SID as a string
    If LastError = 1332 Then
        ' convert the SID to a string
        If ConvertSidToStringSid(OwnerSid, StringPtr) = False Then
            Return "Error"
        End If
        SidString = Marshal.PtrToStringAuto(StringPtr)
        Marshal.FreeHGlobal(StringPtr)

        domain_len = 0
        name = SidString
        name_len = Len(name)
    Else
        Return "Error"
    End If
End If

' put the name parts together
If domain_len > 0 Then
    UserName = Left(domain_name, domain_len) & "\" & Left(name, name_len)
Else
    UserName = Left(name, name_len)
End If

Notice the use Marshal.PtrToStringAuto to copy the string pointer returned by the API into a normal everyday .Net Framework String.

Download/Links

Download the complete VB.Net Source code example used in this article: GetOwner.zip