diff --git a/lib/puppet/util/windows/api_types.rb b/lib/puppet/util/windows/api_types.rb index 8f236060f..8766921f9 100644 --- a/lib/puppet/util/windows/api_types.rb +++ b/lib/puppet/util/windows/api_types.rb @@ -1,121 +1,151 @@ require 'ffi' require 'puppet/util/windows/string' module Puppet::Util::Windows::APITypes module ::FFI WIN32_FALSE = 0 end module ::FFI::Library # Wrapper method for attach_function + private def attach_function_private(*args) attach_function(*args) private args[0] end end class ::FFI::Pointer NULL_HANDLE = 0 NULL_TERMINATOR_WCHAR = 0 def self.from_string_to_wide_string(str) str = Puppet::Util::Windows::String.wide_string(str) ptr = FFI::MemoryPointer.new(:byte, str.bytesize) # uchar here is synonymous with byte ptr.put_array_of_uchar(0, str.bytes.to_a) ptr end def read_win32_bool # BOOL is always a 32-bit integer in Win32 # some Win32 APIs return 1 for true, while others are non-0 read_int32 != FFI::WIN32_FALSE end alias_method :read_dword, :read_uint32 def read_handle type_size == 4 ? read_uint32 : read_uint64 end alias_method :read_wchar, :read_uint16 def read_wide_string(char_length) # char_length is number of wide chars (typically excluding NULLs), *not* bytes str = get_bytes(0, char_length * 2).force_encoding('UTF-16LE') str.encode(Encoding.default_external) end def read_arbitrary_wide_string_up_to(max_char_length = 512) # max_char_length is number of wide chars (typically excluding NULLs), *not* bytes # use a pointer to read one UTF-16LE char (2 bytes) at a time wchar_ptr = FFI::Pointer.new(:wchar, address) # now iterate 2 bytes at a time until an offset lower than max_char_length is found 0.upto(max_char_length - 1) do |i| if wchar_ptr[i].read_wchar == NULL_TERMINATOR_WCHAR return read_wide_string(i) end end read_wide_string(max_char_length) end + def read_win32_local_pointer(&block) + ptr = nil + begin + ptr = read_pointer + yield ptr + ensure + if ptr && ! ptr.null? + if FFI::WIN32::LocalFree(ptr.address) != FFI::Pointer::NULL_HANDLE + Puppet.debug "LocalFree memory leak" + end + end + end + + # ptr has already had LocalFree called, so nothing to return + nil + end + alias_method :write_dword, :write_uint32 end # FFI Types # https://github.com/ffi/ffi/wiki/Types # Windows - Common Data Types # http://msdn.microsoft.com/en-us/library/cc230309.aspx # Windows Data Types # http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx FFI.typedef :uint16, :word FFI.typedef :uint32, :dword # uintptr_t is defined in an FFI conf as platform specific, either # ulong_long on x64 or just ulong on x86 FFI.typedef :uintptr_t, :handle # buffer_inout is similar to pointer (platform specific), but optimized for buffers FFI.typedef :buffer_inout, :lpwstr # buffer_in is similar to pointer (platform specific), but optimized for CONST read only buffers FFI.typedef :buffer_in, :lpcwstr # string is also similar to pointer, but should be used for const char * # NOTE that this is not wide, useful only for A suffixed functions FFI.typedef :string, :lpcstr # pointer in FFI is platform specific # NOTE: for API calls with reserved lpvoid parameters, pass a FFI::Pointer::NULL FFI.typedef :pointer, :lpcvoid FFI.typedef :pointer, :lpvoid FFI.typedef :pointer, :lpword FFI.typedef :pointer, :lpdword FFI.typedef :pointer, :pdword FFI.typedef :pointer, :phandle FFI.typedef :pointer, :ulong_ptr FFI.typedef :pointer, :pbool # any time LONG / ULONG is in a win32 API definition DO NOT USE platform specific width # which is what FFI uses by default # instead create new aliases for these very special cases # NOTE: not a good idea to redefine FFI :ulong since other typedefs may rely on it FFI.typedef :uint32, :win32_ulong FFI.typedef :int32, :win32_long # FFI bool can be only 1 byte at times, # Win32 BOOL is a signed int, and is always 4 bytes, even on x64 # http://blogs.msdn.com/b/oldnewthing/archive/2011/03/28/10146459.aspx FFI.typedef :int32, :win32_bool # NOTE: FFI already defines (u)short as a 16-bit (un)signed like this: # FFI.typedef :uint16, :ushort # FFI.typedef :int16, :short # 8 bits per byte FFI.typedef :uchar, :byte FFI.typedef :uint16, :wchar + + module ::FFI::WIN32 + extend ::FFI::Library + + ffi_convention :stdcall + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx + # HLOCAL WINAPI LocalFree( + # _In_ HLOCAL hMem + # ); + ffi_lib :kernel32 + attach_function :LocalFree, [:handle], :handle + end end diff --git a/lib/puppet/util/windows/error.rb b/lib/puppet/util/windows/error.rb index 7d14307e4..2a5634951 100644 --- a/lib/puppet/util/windows/error.rb +++ b/lib/puppet/util/windows/error.rb @@ -1,98 +1,86 @@ require 'puppet/util/windows' # represents an error resulting from a Win32 error code class Puppet::Util::Windows::Error < Puppet::Error require 'ffi' extend FFI::Library attr_reader :code def initialize(message, code = @@GetLastError.call(), original = nil) super(message + ": #{self.class.format_error_code(code)}", original) @code = code end # Helper method that wraps FormatMessage that returns a human readable string. def self.format_error_code(code) # specifying 0 will look for LANGID in the following order # 1.Language neutral # 2.Thread LANGID, based on the thread's locale value # 3.User default LANGID, based on the user's default locale value # 4.System default LANGID, based on the system default locale value # 5.US English dwLanguageId = 0 flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK + error_string = '' + # this pointer actually points to a :lpwstr (pointer) since we're letting Windows allocate for us buffer_ptr = FFI::MemoryPointer.new(:pointer, 1) + length = FormatMessageW(flags, FFI::Pointer::NULL, code, dwLanguageId, + buffer_ptr, 0, FFI::Pointer::NULL) - begin - length = FormatMessageW(flags, FFI::Pointer::NULL, code, dwLanguageId, - buffer_ptr, 0, FFI::Pointer::NULL) - - if length == FFI::WIN32_FALSE - # can't raise same error type here or potentially recurse infinitely - raise Puppet::Error.new("FormatMessageW could not format code #{code}") - end - - # returns an FFI::Pointer with autorelease set to false, which is what we want - wide_string_ptr = buffer_ptr.read_pointer + if length == FFI::WIN32_FALSE + # can't raise same error type here or potentially recurse infinitely + raise Puppet::Error.new("FormatMessageW could not format code #{code}") + end + # returns an FFI::Pointer with autorelease set to false, which is what we want + buffer_ptr.read_win32_local_pointer do |wide_string_ptr| if wide_string_ptr.null? raise Puppet::Error.new("FormatMessageW failed to allocate buffer for code #{code}") end - return wide_string_ptr.read_wide_string(length) - ensure - if ! wide_string_ptr.nil? && ! wide_string_ptr.null? - if LocalFree(wide_string_ptr.address) != FFI::Pointer::NULL_HANDLE - Puppet.debug "LocalFree memory leak" - end - end + error_string = wide_string_ptr.read_wide_string(length) end + + error_string end FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000 FORMAT_MESSAGE_MAX_WIDTH_MASK = 0x000000FF ffi_convention :stdcall # NOTE: It seems like FFI.errno is already implemented as GetLastError... or is it? # http://msdn.microsoft.com/en-us/library/windows/desktop/ms679360(v=vs.85).aspx # DWORD WINAPI GetLastError(void); # HACK: unfortunately using FFI.errno or attach_function to hook GetLastError in # FFI like the following will not work. Something internal to FFI appears to # be stomping out the value of GetLastError when calling via FFI. # attach_function_private :GetLastError, [], :dword require 'Win32API' @@GetLastError = Win32API.new('kernel32', 'GetLastError', [], 'L') # http://msdn.microsoft.com/en-us/library/windows/desktop/ms679351(v=vs.85).aspx # DWORD WINAPI FormatMessage( # _In_ DWORD dwFlags, # _In_opt_ LPCVOID lpSource, # _In_ DWORD dwMessageId, # _In_ DWORD dwLanguageId, # _Out_ LPTSTR lpBuffer, # _In_ DWORD nSize, # _In_opt_ va_list *Arguments # ); # NOTE: since we're not preallocating the buffer, use a :pointer for lpBuffer ffi_lib :kernel32 attach_function_private :FormatMessageW, [:dword, :lpcvoid, :dword, :dword, :pointer, :dword, :pointer], :dword - - # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx - # HLOCAL WINAPI LocalFree( - # _In_ HLOCAL hMem - # ); - ffi_lib :kernel32 - attach_function_private :LocalFree, [:handle], :handle end diff --git a/lib/puppet/util/windows/sid.rb b/lib/puppet/util/windows/sid.rb index c51d9bebe..ab5a9b638 100644 --- a/lib/puppet/util/windows/sid.rb +++ b/lib/puppet/util/windows/sid.rb @@ -1,164 +1,152 @@ require 'puppet/util/windows' module Puppet::Util::Windows module SID require 'ffi' extend FFI::Library # missing from Windows::Error ERROR_NONE_MAPPED = 1332 ERROR_INVALID_SID_STRUCTURE = 1337 # Convert an account name, e.g. 'Administrators' into a SID string, # e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators', # 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the # SID. Returns nil if the account doesn't exist. def name_to_sid(name) sid = name_to_sid_object(name) sid ? sid.to_s : nil end # Convert an account name, e.g. 'Administrators' into a SID object, # e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators', # 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the # SID object. Returns nil if the account doesn't exist. def name_to_sid_object(name) # Apparently, we accept a symbol.. name = name.to_s.strip if name # if it's in SID string form, convert to user parsed_sid = Win32::Security::SID.string_to_sid(name) rescue nil parsed_sid ? Win32::Security::SID.new(parsed_sid) : Win32::Security::SID.new(name) rescue nil end # Converts an octet string array of bytes to a SID object, # e.g. [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0] is the representation for # S-1-5-18, the local 'SYSTEM' account. # Raises an Error for nil or non-array input. def octet_string_to_sid_object(bytes) if !bytes || !bytes.respond_to?('pack') || bytes.empty? raise Puppet::Util::Windows::Error.new("Octet string must be an array of bytes") end Win32::Security::SID.new(bytes.pack('C*')) end # Convert a SID string, e.g. "S-1-5-32-544" to a name, # e.g. 'BUILTIN\Administrators'. Returns nil if an account # for that SID does not exist. def sid_to_name(value) sid = Win32::Security::SID.new(Win32::Security::SID.string_to_sid(value)) if sid.domain and sid.domain.length > 0 "#{sid.domain}\\#{sid.account}" else sid.account end rescue nil end # http://stackoverflow.com/a/1792930 - 68 bytes, 184 characters in a string MAXIMUM_SID_STRING_LENGTH = 184 # Convert a SID pointer to a SID string, e.g. "S-1-5-32-544". def sid_ptr_to_string(psid) if ! psid.instance_of?(FFI::Pointer) || IsValidSid(psid) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Invalid SID") end buffer_ptr = FFI::MemoryPointer.new(:pointer, 1) + if ConvertSidToStringSidW(psid, buffer_ptr) == FFI::WIN32_FALSE + raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") + end - begin - if ConvertSidToStringSidW(psid, buffer_ptr) == FFI::WIN32_FALSE - raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") - end - - wide_string_ptr = buffer_ptr.read_pointer - + sid_string = nil + buffer_ptr.read_win32_local_pointer do |wide_string_ptr| if wide_string_ptr.null? raise Puppet::Error.new("ConvertSidToStringSidW failed to allocate buffer for sid") end - return wide_string_ptr.read_arbitrary_wide_string_up_to(MAXIMUM_SID_STRING_LENGTH) - ensure - if ! wide_string_ptr.nil? && ! wide_string_ptr.null? - if LocalFree(wide_string_ptr.address) != FFI::Pointer::NULL_HANDLE - Puppet.debug "LocalFree memory leak" - end - end + sid_string = wide_string_ptr.read_arbitrary_wide_string_up_to(MAXIMUM_SID_STRING_LENGTH) end + + sid_string end # Convert a SID string, e.g. "S-1-5-32-544" to a pointer (containing the # address of the binary SID structure). The returned value can be used in # Win32 APIs that expect a PSID, e.g. IsValidSid. The account for this # SID may or may not exist. def string_to_sid_ptr(string_sid, &block) lpcwstr = FFI::MemoryPointer.from_string_to_wide_string(string_sid) sid_ptr_ptr = FFI::MemoryPointer.new(:pointer, 1) if ConvertStringSidToSidW(lpcwstr, sid_ptr_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to convert string SID: #{string_sid}") end - begin - yield sid_ptr = sid_ptr_ptr.read_pointer - ensure - if LocalFree(sid_ptr.address) != FFI::Pointer::NULL_HANDLE - Puppet.debug "LocalFree memory leak" - end + sid_ptr_ptr.read_win32_local_pointer do |sid_ptr| + yield sid_ptr end + + # yielded sid_ptr has already had LocalFree called, nothing to return + nil end # Return true if the string is a valid SID, e.g. "S-1-5-32-544", false otherwise. def valid_sid?(string_sid) - string_to_sid_ptr(string_sid) { |ptr| true } - rescue Puppet::Util::Windows::Error => e - if e.code == ERROR_INVALID_SID_STRUCTURE - false - else - raise + valid = false + + begin + string_to_sid_ptr(string_sid) { |ptr| valid = ! ptr.nil? && ! ptr.null? } + rescue Puppet::Util::Windows::Error => e + raise if e.code != ERROR_INVALID_SID_STRUCTURE end + + valid end ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379151(v=vs.85).aspx # BOOL WINAPI IsValidSid( # _In_ PSID pSid # ); ffi_lib :advapi32 attach_function_private :IsValidSid, [:pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376399(v=vs.85).aspx # BOOL ConvertSidToStringSid( # _In_ PSID Sid, # _Out_ LPTSTR *StringSid # ); ffi_lib :advapi32 attach_function_private :ConvertSidToStringSidW, [:pointer, :pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376402(v=vs.85).aspx # BOOL WINAPI ConvertStringSidToSid( # _In_ LPCTSTR StringSid, # _Out_ PSID *Sid # ); ffi_lib :advapi32 attach_function_private :ConvertStringSidToSidW, [:lpcwstr, :pointer], :win32_bool - - # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx - # HLOCAL WINAPI LocalFree( - # _In_ HLOCAL hMem - # ); - ffi_lib :kernel32 - attach_function_private :LocalFree, [:handle], :handle end end