File: process.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (364 lines) | stat: -rw-r--r-- 12,589 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
require_relative '../../../puppet/util/windows/monkey_patches/process'
require_relative '../../../puppet/ffi/windows'

module Puppet::Util::Windows::Process
  extend Puppet::FFI::Windows::Functions
  include Puppet::FFI::Windows::Structs
  extend Puppet::Util::Windows::String

  WAIT_TIMEOUT = 0x102
  WAIT_INTERVAL = 200
  # https://docs.microsoft.com/en-us/windows/desktop/ProcThread/process-creation-flags
  CREATE_NO_WINDOW = 0x08000000
  # https://docs.microsoft.com/en-us/windows/desktop/ProcThread/process-security-and-access-rights
  PROCESS_QUERY_INFORMATION = 0x0400
  # https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
  MAX_PATH_LENGTH = 32767

  def execute(command, arguments, stdin, stdout, stderr)
    create_args = {
      :command_line => command,
      :startup_info => {
        :stdin => stdin,
        :stdout => stdout,
        :stderr => stderr
      },
      :close_handles => false,
    }
    if arguments[:suppress_window]
      create_args[:creation_flags] = CREATE_NO_WINDOW
    end
    if arguments[:cwd]
      create_args[:cwd] = arguments[:cwd]
    end
    Process.create(create_args)
  end
  module_function :execute

  def wait_process(handle)
    while WaitForSingleObject(handle, WAIT_INTERVAL) == WAIT_TIMEOUT
      sleep(0)
    end

    exit_status = -1
    FFI::MemoryPointer.new(:dword, 1) do |exit_status_ptr|
      if GetExitCodeProcess(handle, exit_status_ptr) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error.new(_("Failed to get child process exit code"))
      end
      exit_status = exit_status_ptr.read_dword

      # $CHILD_STATUS is not set when calling win32/process Process.create
      # and since it's read-only, we can't set it. But we can execute a
      # a shell that simply returns the desired exit status, which has the
      # desired effect.
      %x{#{ENV['COMSPEC']} /c exit #{exit_status}}
    end

    exit_status
  end
  module_function :wait_process

  def get_current_process
    # this pseudo-handle does not require closing per MSDN docs
    GetCurrentProcess()
  end
  module_function :get_current_process

  def open_process(desired_access, inherit_handle, process_id, &block)
    phandle = nil
    inherit = inherit_handle ? FFI::WIN32_TRUE : FFI::WIN32_FALSE
    begin
      phandle = OpenProcess(desired_access, inherit, process_id)
      if phandle == FFI::Pointer::NULL_HANDLE
        raise Puppet::Util::Windows::Error.new(
          "OpenProcess(#{desired_access.to_s(8)}, #{inherit}, #{process_id})")
      end

      yield phandle
    ensure
      FFI::WIN32.CloseHandle(phandle) if phandle
    end

    # phandle has had CloseHandle called against it, so nothing to return
    nil
  end
  module_function :open_process

  def open_process_token(handle, desired_access, &block)
    token_handle = nil
    begin
      FFI::MemoryPointer.new(:handle, 1) do |token_handle_ptr|
        result = OpenProcessToken(handle, desired_access, token_handle_ptr)
        if result == FFI::WIN32_FALSE
          raise Puppet::Util::Windows::Error.new(
            "OpenProcessToken(#{handle}, #{desired_access.to_s(8)}, #{token_handle_ptr})")
        end

        yield token_handle = token_handle_ptr.read_handle
      end

      token_handle
    ensure
      FFI::WIN32.CloseHandle(token_handle) if token_handle
    end

    # token_handle has had CloseHandle called against it, so nothing to return
    nil
  end
  module_function :open_process_token

  # Execute a block with the current process token
  def with_process_token(access, &block)
    handle = get_current_process
    open_process_token(handle, access) do |token_handle|
      yield token_handle
    end

    # all handles have been closed, so nothing to safely return
    nil
  end
  module_function :with_process_token

  def get_process_image_name_by_pid(pid)
    image_name = ""

    Puppet::Util::Windows::Security.with_privilege(Puppet::Util::Windows::Security::SE_DEBUG_NAME) do
      open_process(PROCESS_QUERY_INFORMATION, false, pid) do |phandle|
        FFI::MemoryPointer.new(:dword, 1) do |exe_name_length_ptr|
          # UTF is 2 bytes/char:
          max_chars = MAX_PATH_LENGTH + 1
          exe_name_length_ptr.write_dword(max_chars)
          FFI::MemoryPointer.new(:wchar, max_chars) do |exe_name_ptr|
            use_win32_path_format = 0
            result = QueryFullProcessImageNameW(phandle, use_win32_path_format, exe_name_ptr, exe_name_length_ptr)
            if result == FFI::WIN32_FALSE
              raise Puppet::Util::Windows::Error.new(
                      "QueryFullProcessImageNameW(phandle, #{use_win32_path_format}, " +
                      "exe_name_ptr, #{max_chars}")
            end
            image_name = exe_name_ptr.read_wide_string(exe_name_length_ptr.read_dword)
          end
        end
      end
    end

    image_name
  end
  module_function :get_process_image_name_by_pid

  def lookup_privilege_value(name, system_name = '', &block)
    FFI::MemoryPointer.new(LUID.size) do |luid_ptr|
      result = LookupPrivilegeValueW(
        wide_string(system_name),
        wide_string(name.to_s),
        luid_ptr
        )

      if result == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error.new(
          "LookupPrivilegeValue(#{system_name}, #{name}, #{luid_ptr})")
      end

      yield LUID.new(luid_ptr)
    end

    # the underlying MemoryPointer for LUID is cleaned up by this point
    nil
  end
  module_function :lookup_privilege_value

  def get_token_information(token_handle, token_information, &block)
    # to determine buffer size
    FFI::MemoryPointer.new(:dword, 1) do |return_length_ptr|
      result = GetTokenInformation(token_handle, token_information, nil, 0, return_length_ptr)
      return_length = return_length_ptr.read_dword

      if return_length <= 0
        raise Puppet::Util::Windows::Error.new(
          "GetTokenInformation(#{token_handle}, #{token_information}, nil, 0, #{return_length_ptr})")
      end

      # re-call API with properly sized buffer for all results
      FFI::MemoryPointer.new(return_length) do |token_information_buf|
        result = GetTokenInformation(token_handle, token_information,
          token_information_buf, return_length, return_length_ptr)

        if result == FFI::WIN32_FALSE
          raise Puppet::Util::Windows::Error.new(
            "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " +
              "#{return_length}, #{return_length_ptr})")
        end

        yield token_information_buf
      end
    end

    # GetTokenInformation buffer has been cleaned up by this point, nothing to return
    nil
  end
  module_function :get_token_information

  def parse_token_information_as_token_privileges(token_information_buf)
    raw_privileges = TOKEN_PRIVILEGES.new(token_information_buf)
    privileges = { :count => raw_privileges[:PrivilegeCount], :privileges => [] }

    offset = token_information_buf + TOKEN_PRIVILEGES.offset_of(:Privileges)
    privilege_ptr = FFI::Pointer.new(LUID_AND_ATTRIBUTES, offset)

    # extract each instance of LUID_AND_ATTRIBUTES
    0.upto(privileges[:count] - 1) do |i|
      privileges[:privileges] <<  LUID_AND_ATTRIBUTES.new(privilege_ptr[i])
    end

    privileges
  end
  module_function :parse_token_information_as_token_privileges

  def parse_token_information_as_token_elevation(token_information_buf)
    TOKEN_ELEVATION.new(token_information_buf)
  end
  module_function :parse_token_information_as_token_elevation

  TOKEN_ALL_ACCESS = 0xF01FF
  ERROR_NO_SUCH_PRIVILEGE = 1313
  def process_privilege_symlink?
    privilege_symlink = false
    handle = get_current_process
    open_process_token(handle, TOKEN_ALL_ACCESS) do |token_handle|
      lookup_privilege_value('SeCreateSymbolicLinkPrivilege') do |luid|
        get_token_information(token_handle, :TokenPrivileges) do |token_info|
          token_privileges = parse_token_information_as_token_privileges(token_info)
          privilege_symlink = token_privileges[:privileges].any? { |p| p[:Luid].values == luid.values }
        end
      end
    end

    privilege_symlink
  rescue Puppet::Util::Windows::Error => e
    if e.code == ERROR_NO_SUCH_PRIVILEGE
      false # pre-Vista
    else
      raise e
    end
  end
  module_function :process_privilege_symlink?

  TOKEN_QUERY = 0x0008
  # Returns whether or not the owner of the current process is running
  # with elevated security privileges.
  #
  # Only supported on Windows Vista or later.
  #
  def elevated_security?
    # default / pre-Vista
    elevated = false
    handle = nil

    begin
      handle = get_current_process
      open_process_token(handle, TOKEN_QUERY) do |token_handle|
        get_token_information(token_handle, :TokenElevation) do |token_info|
          token_elevation = parse_token_information_as_token_elevation(token_info)
          # TokenIsElevated member of the TOKEN_ELEVATION struct
          elevated = token_elevation[:TokenIsElevated] != 0
        end
      end

      elevated
    rescue Puppet::Util::Windows::Error => e
      raise e if e.code != ERROR_NO_SUCH_PRIVILEGE
    ensure
      FFI::WIN32.CloseHandle(handle) if handle
    end
  end
  module_function :elevated_security?

  def windows_major_version
    ver = 0

    FFI::MemoryPointer.new(OSVERSIONINFO.size) do |os_version_ptr|
      os_version = OSVERSIONINFO.new(os_version_ptr)
      os_version[:dwOSVersionInfoSize] = OSVERSIONINFO.size

      result = GetVersionExW(os_version_ptr)

      if result == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error.new(_("GetVersionEx failed"))
      end

      ver = os_version[:dwMajorVersion]
    end

    ver
  end
  module_function :windows_major_version

  # Returns a hash of the current environment variables encoded as UTF-8
  # The memory block returned from GetEnvironmentStringsW is double-null terminated and the vars are paired as below;
  # Var1=Value1\0
  # Var2=Value2\0
  # VarX=ValueX\0\0
  # Note - Some env variable names start with '=' and are excluded from the return value
  # Note - The env_ptr MUST be freed using the FreeEnvironmentStringsW function
  # Note - There is no technical limitation to the size of the environment block returned.
  #   However a practical limit of 64K is used as no single environment variable can exceed 32KB
  def get_environment_strings
    env_ptr = GetEnvironmentStringsW()

    # pass :invalid => :replace to the Ruby String#encode to use replacement characters
    pairs = env_ptr.read_arbitrary_wide_string_up_to(65534, :double_null, { :invalid => :replace })
      .split(?\x00)
      .reject { |env_str| env_str.nil? || env_str.empty? || env_str[0] == '=' }
      .reject do |env_str|
        # reject any string containing the Unicode replacement character
        if env_str.include?("\uFFFD")
          Puppet.warning(_("Discarding environment variable %{string} which contains invalid bytes") % { string: env_str })
          true
        end
      end
      .map { |env_pair| env_pair.split('=', 2) }
    Hash[ pairs ]
  ensure
    if env_ptr && ! env_ptr.null?
      if FreeEnvironmentStringsW(env_ptr) == FFI::WIN32_FALSE
        Puppet.debug "FreeEnvironmentStringsW memory leak"
      end
    end
  end
  module_function :get_environment_strings

  def set_environment_variable(name, val)
    raise Puppet::Util::Windows::Error(_('environment variable name must not be nil or empty')) if ! name || name.empty?

    FFI::MemoryPointer.from_string_to_wide_string(name) do |name_ptr|
      if (val.nil?)
        if SetEnvironmentVariableW(name_ptr, FFI::MemoryPointer::NULL) == FFI::WIN32_FALSE
          raise Puppet::Util::Windows::Error.new(_("Failed to remove environment variable: %{name}") % { name: name })
        end
      else
        FFI::MemoryPointer.from_string_to_wide_string(val) do |val_ptr|
          if SetEnvironmentVariableW(name_ptr, val_ptr) == FFI::WIN32_FALSE
            raise Puppet::Util::Windows::Error.new(_("Failed to set environment variable: %{name}") % { name: name })
          end
        end
      end
    end
  end
  module_function :set_environment_variable

  def get_system_default_ui_language
    GetSystemDefaultUILanguage()
  end
  module_function :get_system_default_ui_language

  # Returns whether or not the OS has the ability to set elevated
  # token information.
  #
  # Returns true on Windows Vista or later, otherwise false
  #
  def supports_elevated_security?
    windows_major_version >= 6
  end
  module_function :supports_elevated_security?

end