[Libguestfs] [PATCH v10 09/10] daemon: Implement inspection of Windows.

Richard W.M. Jones rjones at redhat.com
Fri Jul 21 21:50:21 UTC 2017


Mostly a line-for-line translation of the C inspection code.
---
 daemon/Makefile.am            |   2 +
 daemon/inspect_fs.ml          |   6 +
 daemon/inspect_fs_windows.ml  | 491 ++++++++++++++++++++++++++++++++++++++++++
 daemon/inspect_fs_windows.mli |  24 +++
 4 files changed, 523 insertions(+)

diff --git a/daemon/Makefile.am b/daemon/Makefile.am
index e19cb0ed8..4fd619c7e 100644
--- a/daemon/Makefile.am
+++ b/daemon/Makefile.am
@@ -254,6 +254,7 @@ SOURCES_MLI = \
 	inspect_fs.mli \
 	inspect_fs_unix.mli \
 	inspect_fs_unix_fstab.mli \
+	inspect_fs_windows.mli \
 	inspect_types.mli \
 	inspect_utils.mli \
 	is.mli \
@@ -295,6 +296,7 @@ SOURCES_ML = \
 	inspect_utils.ml \
 	inspect_fs_unix_fstab.ml \
 	inspect_fs_unix.ml \
+	inspect_fs_windows.ml \
 	inspect_fs.ml \
 	inspect.ml \
 	callbacks.ml \
diff --git a/daemon/inspect_fs.ml b/daemon/inspect_fs.ml
index 9153e68a5..10a15827b 100644
--- a/daemon/inspect_fs.ml
+++ b/daemon/inspect_fs.ml
@@ -192,6 +192,12 @@ and check_filesystem mountable =
     debug_matching "Linux /var";
     ()
   )
+  (* Windows root? *)
+  else if Inspect_fs_windows.is_windows_systemroot () then (
+    debug_matching "Windows root";
+    role := `Root;
+    Inspect_fs_windows.check_windows_root data;
+  )
   (* Windows volume with installed applications (but not root)? *)
   else if is_dir_nocase "/System Volume Information" &&
           is_dir_nocase "/Program Files" then (
diff --git a/daemon/inspect_fs_windows.ml b/daemon/inspect_fs_windows.ml
new file mode 100644
index 000000000..8cece319f
--- /dev/null
+++ b/daemon/inspect_fs_windows.ml
@@ -0,0 +1,491 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+open Printf
+
+open Std_utils
+
+open Utils
+open Inspect_types
+open Inspect_utils
+
+(* Check a predefined list of common windows system root locations. *)
+let systemroot_paths =
+  [ "/windows"; "/winnt"; "/win32"; "/win"; "/reactos" ]
+
+let re_boot_ini_os =
+  Str.regexp "^\\(multi\\|scsi\\)(\\([0-9]+\\))disk(\\([0-9]+\\))rdisk(\\([0-9]+\\))partition(\\([0-9]+\\))\\([^=]+\\)="
+
+let rec check_windows_root data =
+  let systemroot =
+    match get_windows_systemroot () with
+    | None -> assert false (* Should never happen - see caller. *)
+    | Some systemroot -> systemroot in
+
+  data.os_type <- Some OS_TYPE_WINDOWS;
+  data.distro <- Some DISTRO_WINDOWS;
+  data.windows_systemroot <- Some systemroot;
+  data.arch <- Some (check_windows_arch systemroot);
+
+  (* Load further fields from the Windows registry. *)
+  check_windows_registry systemroot data
+
+and is_windows_systemroot () =
+  get_windows_systemroot () <> None
+
+and get_windows_systemroot () =
+  let rec loop = function
+    | [] -> None
+    | path :: paths ->
+       let path = case_sensitive_path_silently path in
+       match path with
+       | None -> loop paths
+       | Some path ->
+          if is_systemroot path then Some path
+          else loop paths
+  in
+  let systemroot = loop systemroot_paths in
+
+  let systemroot =
+    match systemroot with
+    | Some systemroot -> Some systemroot
+    | None ->
+       (* If the fs contains boot.ini, check it for non-standard
+        * systemroot locations.
+        *)
+       let boot_ini_path = case_sensitive_path_silently "/boot.ini" in
+       match boot_ini_path with
+       | None -> None
+       | Some boot_ini_path ->
+          get_windows_systemroot_from_boot_ini boot_ini_path in
+
+  match systemroot with
+  | None -> None
+  | Some systemroot ->
+     if verbose () then
+       eprintf "get_windows_systemroot: windows %%SYSTEMROOT%% = %s\n%!"
+               systemroot;
+     Some systemroot
+
+and get_windows_systemroot_from_boot_ini boot_ini_path =
+  let chroot = Chroot.create ~name:"get_windows_systemroot_from_boot_ini" () in
+  let lines =
+    Chroot.f chroot (
+      fun () ->
+        if not (is_small_file boot_ini_path) then (
+          eprintf "%s: not a regular file or too large\n" boot_ini_path;
+          None
+        )
+        else
+          Some (read_whole_file boot_ini_path)
+    ) () in
+  match lines with
+  | None -> None
+  | Some lines ->
+     let lines = String.nsplit "\n" lines in
+
+     (* Find:
+      *   [operating systems]
+      * followed by multiple lines starting with "multi" or "scsi".
+      *)
+     let rec loop = function
+       | [] -> None
+       | str :: rest when String.is_prefix str "[operating systems]" ->
+          let rec loop2 = function
+            | [] -> []
+            | str :: rest when String.is_prefix str "multi(" ||
+                               String.is_prefix str "scsi(" ->
+               str :: loop2 rest
+            | _ -> []
+          in
+          Some (loop2 rest)
+       | _ :: rest -> loop rest
+     in
+     match loop lines with
+     | None -> None
+     | Some oses ->
+        (* Rewrite multi|scsi lines, removing any which we cannot parse. *)
+        let oses =
+          filter_map (
+            fun line ->
+              if Str.string_match re_boot_ini_os line 0 then (
+                let ctrlr_type = Str.matched_group 1 line
+                and ctrlr = int_of_string (Str.matched_group 2 line)
+                and disk = int_of_string (Str.matched_group 3 line)
+                and rdisk = int_of_string (Str.matched_group 4 line)
+                and part = int_of_string (Str.matched_group 5 line)
+                and path = Str.matched_group 6 line in
+
+                (* Swap backslashes for forward slashes in the
+                 * system root path.
+                 *)
+                let path = String.replace_char path '\\' '/' in
+
+                Some (ctrlr_type, ctrlr, disk, rdisk, part, path)
+              )
+              else None
+          ) oses in
+
+        (* The Windows system root may be on any disk. However, there
+         * are currently (at least) 2 practical problems preventing us
+         * from locating it on another disk:
+         *
+         * 1. We don't have enough metadata about the disks we were
+         * given to know if what controller they were on and what
+         * index they had.
+         *
+         * 2. The way inspection of filesystems currently works, we
+         * can't mark another filesystem, which we may have already
+         * inspected, to be inspected for a specific Windows system
+         * root.
+         *
+         * Solving 1 properly would require a new API at a minimum. We
+         * might be able to fudge something practical without this,
+         * though, e.g. by looking at the <partition>th partition of
+         * every disk for the specific windows root.
+         *
+         * Solving 2 would probably require a significant refactoring
+         * of the way filesystems are inspected. We should probably do
+         * this some time.
+         *
+         * For the moment, we ignore all partition information and
+         * assume the system root is on the current partition. In
+         * practice, this will normally be correct.
+         *)
+
+        let rec loop = function
+          | [] -> None
+          | (_, _, _, _, _, path) :: rest ->
+             if is_systemroot path then Some path
+             else loop rest
+        in
+        loop oses
+
+(* Try to find Windows systemroot using some common locations.
+ *
+ * Notes:
+ *
+ * (1) We check for some directories inside to see if it is a real
+ * systemroot, and not just a directory that happens to have the same
+ * name.
+ *
+ * (2) If a Windows guest has multiple disks and applications are
+ * installed on those other disks, then those other disks will contain
+ * "/Program Files" and "/System Volume Information".  Those would
+ * *not* be Windows root disks.  (RHBZ#674130)
+ *)
+and is_systemroot systemroot =
+  is_dir_nocase (systemroot ^ "/system32") &&
+  is_dir_nocase (systemroot ^ "/system32/config") &&
+  is_file_nocase (systemroot ^ "/system32/cmd.exe")
+
+(* Return the architecture of the guest from cmd.exe. *)
+and check_windows_arch systemroot =
+  let cmd_exe = sprintf "%s/system32/cmd.exe" systemroot in
+
+  (* Should exist because of previous check above in is_systemroot. *)
+  let cmd_exe = Realpath.case_sensitive_path cmd_exe in
+
+  Filearch.file_architecture cmd_exe
+
+(* Read further fields from the Windows registry. *)
+and check_windows_registry systemroot data =
+  (* We know (from is_systemroot) that the config directory exists. *)
+  let software_hive = sprintf "%s/system32/config/software" systemroot in
+  let software_hive = Realpath.case_sensitive_path software_hive in
+  let software_hive =
+    if Is.is_file software_hive then Some software_hive else None in
+  data.windows_software_hive <- software_hive;
+
+  let system_hive = sprintf "%s/system32/config/system" systemroot in
+  let system_hive = Realpath.case_sensitive_path system_hive in
+  let system_hive =
+    if Is.is_file system_hive then Some system_hive else None in
+  data.windows_system_hive <- system_hive;
+
+  match software_hive, system_hive with
+  | None, _ | Some _, None -> ()
+  | Some software_hive, Some system_hive ->
+     (* Check software hive. *)
+     check_windows_software_registry software_hive data;
+
+     (* Check system hive. *)
+     check_windows_system_registry system_hive data
+
+(* At the moment, pull just the ProductName and version numbers from
+ * the registry.  In future there is a case for making many more
+ * registry fields available to callers.
+ *)
+and check_windows_software_registry software_hive data =
+  with_hive (Sysroot.sysroot () // software_hive) (
+    fun h root ->
+      try
+        let path = [ "Microsoft"; "Windows NT"; "CurrentVersion" ] in
+        let node = get_node h root path in
+        let values = Hivex.node_values h node in
+        let values = Array.to_list values in
+        (* Convert to a list of (key, value) to make the following easier. *)
+        let values = List.map (fun v -> Hivex.value_key h v, v) values in
+
+        (* Look for ProductName key. *)
+        (try
+           let v = List.assoc "ProductName" values in
+           data.product_name <- Some (hivex_value_as_utf8 h v)
+         with
+           Not_found -> ()
+        );
+
+        (* Version is complicated.  Use CurrentMajorVersionNumber and
+         * CurrentMinorVersionNumber if present.  If they are not
+         * found, fall back on CurrentVersion.
+         *)
+        (try
+           let major_v = List.assoc "CurrentMajorVersionNumber" values
+           and minor_v = List.assoc "CurrentMinorVersionNumber" values in
+           let major = Int32.to_int (Hivex.value_dword h major_v)
+           and minor = Int32.to_int (Hivex.value_dword h minor_v) in
+           data.version <- Some (major, minor)
+         with
+           Not_found ->
+           let v = List.assoc "CurrentVersion" values in
+           let v = hivex_value_as_utf8 h v in
+           parse_version_from_major_minor v data
+        );
+
+        (* InstallationType (product_variant). *)
+        (try
+           let v = List.assoc "InstallationType" values in
+           data.product_variant <- Some (hivex_value_as_utf8 h v)
+         with
+           Not_found -> ()
+        );
+      with
+      | Not_found ->
+         if verbose () then
+           eprintf "check_windows_software_registry: cannot locate HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\n%!"
+  ) (* with_hive *)
+
+and check_windows_system_registry system_hive data =
+  with_hive (Sysroot.sysroot () // system_hive) (
+    fun h root ->
+      get_drive_mappings h root data;
+
+      let current_control_set = get_current_control_set h root in
+      data.windows_current_control_set <- current_control_set;
+
+      match current_control_set with
+      | None -> ()
+      | Some current_control_set ->
+         let hostname = get_hostname h root current_control_set in
+         data.hostname <- hostname
+  ) (* with_hive *)
+
+(* Get the CurrentControlSet. *)
+and get_current_control_set h root =
+  try
+    let path = [ "Select" ] in
+    let node = get_node h root path in
+    let current_v = Hivex.node_get_value h node "Current" in
+    let current_control_set =
+      sprintf "ControlSet%03ld" (Hivex.value_dword h current_v) in
+    Some current_control_set
+  with
+  | Not_found ->
+     if verbose () then
+       eprintf "check_windows_system_registry: cannot locate HKLM\\SYSTEM\\Select\n%!";
+     None
+
+(* Get the drive mappings.
+ * This page explains the contents of HKLM\System\MountedDevices:
+ * http://www.goodells.net/multiboot/partsigs.shtml
+ *)
+and get_drive_mappings h root data =
+  let devices = lazy (Devsparts.list_devices ()) in
+  let partitions = lazy (Devsparts.list_partitions ()) in
+  try
+    let path = [ "MountedDevices" ] in
+    let node = get_node h root path in
+    let values = Hivex.node_values h node in
+    let values = Array.to_list values in
+    let values =
+      filter_map (
+        fun value ->
+          let key = Hivex.value_key h value in
+          let keylen = String.length key in
+          if keylen >= 14 &&
+             String.lowercase_ascii (String.sub key 0 12) = "\\dosdevices\\" &&
+             Char.isalpha key.[12] && key.[13] = ':' then (
+            let drive_letter = String.sub key 12 1 in
+
+            (* Get the binary value.  Is it a fixed disk? *)
+            let (typ, blob) = Hivex.value_value h value in
+            let device =
+              if typ = Hivex.REG_BINARY then (
+                if String.length blob >= 24 &&
+                   String.is_prefix blob "DMIO:ID:" (* GPT *) then
+                  map_registry_disk_blob_gpt (Lazy.force partitions) blob
+                else if String.length blob = 12 then
+                  map_registry_disk_blob (Lazy.force devices) blob
+                else
+                  None
+              )
+              else None in
+
+            match device with
+            | None -> None
+            | Some device -> Some (drive_letter, device)
+          )
+          else
+            None
+      ) values in
+
+    data.drive_mappings <- values
+
+  with
+  | Not_found ->
+     if verbose () then
+       eprintf "check_windows_system_registry: cannot find drive mappings\n%!"
+
+(* Windows Registry HKLM\SYSTEM\MountedDevices uses a blob of data
+ * to store partitions.  This blob is described here:
+ * http://www.goodells.net/multiboot/partsigs.shtml
+ * The following function maps this blob to a libguestfs partition
+ * name, if possible.
+ *)
+and map_registry_disk_blob devices blob =
+  try
+    (* First 4 bytes are the disk ID.  Search all devices to find the
+     * disk with this disk ID.
+     *)
+    let diskid = String.sub blob 0 4 in
+    let device = List.find (fun dev -> pread dev 4 0x01b8 = diskid) devices in
+
+    (* Next 8 bytes are the offset of the partition in bytes(!) given as
+     * a 64 bit little endian number.  Luckily it's easy to get the
+     * partition byte offset from Parted.part_list.
+     *)
+    let offset = String.sub blob 4 8 in
+    let offset = int_of_le64 offset in
+    let partitions = Parted.part_list device in
+    let partition =
+      List.find (fun { Parted.part_start = s } -> s = offset) partitions in
+
+    (* Construct the full device name. *)
+    Some (sprintf "%s%ld" device partition.Parted.part_num)
+  with
+  | Not_found -> None
+
+(* Matches Windows registry HKLM\SYSYTEM\MountedDevices\DosDevices blob to
+ * to libguestfs GPT partition device. For GPT disks, the blob is made of
+ * "DMIO:ID:" prefix followed by the GPT partition GUID.
+ *)
+and map_registry_disk_blob_gpt partitions blob =
+  (* The blob_guid is returned as a lowercase hex string. *)
+  let blob_guid = extract_guid_from_registry_blob blob in
+
+  if verbose () then
+    eprintf "map_registry_disk_blob_gpt: searching for GUID %s\n%!"
+            blob_guid;
+
+  try
+    let partition =
+      List.find (
+        fun part ->
+          let partnum = Devsparts.part_to_partnum part in
+          let device = Devsparts.part_to_dev part in
+          let typ = Parted.part_get_parttype device in
+          if typ <> "gpt" then false
+          else (
+            let guid = Parted.part_get_gpt_guid device partnum in
+            String.lowercase_ascii guid = blob_guid
+          )
+      ) partitions in
+    Some partition
+  with
+  | Not_found -> None
+
+(* Extracts the binary GUID stored in blob from Windows registry
+ * HKLM\SYSTYEM\MountedDevices\DosDevices value and converts it to a
+ * GUID string so that it can be matched against libguestfs partition
+ * device GPT GUID.
+ *)
+and extract_guid_from_registry_blob blob =
+  (* Copy relevant sections from blob to respective ints.
+   * Note we have to skip 8 byte "DMIO:ID:" prefix.
+   *)
+  let data1 = int_of_le32 (String.sub blob 8 4)
+  and data2 = int_of_le16 (String.sub blob 12 2)
+  and data3 = int_of_le16 (String.sub blob 14 2)
+  and data4 = int_of_be64 (String.sub blob 16 8) (* really big endian! *) in
+
+  (* Must be lowercase hex. *)
+  sprintf "%08Lx-%04Lx-%04Lx-%04Lx-%012Lx"
+          data1 data2 data3
+          (Int64.shift_right_logical data4 48)
+          (data4 &^ 0xffffffffffff_L)
+
+and pread device size offset =
+  let fd = Unix.openfile device [Unix.O_RDONLY; Unix.O_CLOEXEC] 0 in
+  let ret =
+    protect ~f:(
+      fun () ->
+        ignore (Unix.lseek fd offset Unix.SEEK_SET);
+        let ret = Bytes.create size in
+        if Unix.read fd ret 0 size < size then
+          failwithf "pread: %s: short read" device;
+        ret
+    ) ~finally:(fun () -> Unix.close fd) in
+  Bytes.to_string ret
+
+(* Get the hostname. *)
+and get_hostname h root current_control_set =
+  try
+    let path = [ current_control_set; "Services"; "Tcpip"; "Parameters" ] in
+    let node = get_node h root path in
+    let values = Hivex.node_values h node in
+    let values = Array.to_list values in
+    (* Convert to a list of (key, value) to make the following easier. *)
+    let values = List.map (fun v -> Hivex.value_key h v, v) values in
+    let hostname_v = List.assoc "Hostname" values in
+    Some (hivex_value_as_utf8 h hostname_v)
+  with
+  | Not_found ->
+     if verbose () then
+       eprintf "check_windows_system_registry: cannot locate HKLM\\SYSTEM\\%s\\Services\\Tcpip\\Parameters and/or Hostname key\n%!" current_control_set;
+     None
+
+(* Raises [Not_found] if the node is not found. *)
+and get_node h node = function
+  | [] -> node
+  | x :: xs ->
+     let node = Hivex.node_get_child h node x in
+     get_node h node xs
+
+(* NB: This function DOES NOT test for the existence of the file.  It
+ * will return non-NULL even if the file/directory does not exist.
+ * You have to call guestfs_is_file{,_opts} etc.
+ *)
+and case_sensitive_path_silently path =
+  try
+    Some (Realpath.case_sensitive_path path)
+  with
+  | exn ->
+     if verbose () then
+       eprintf "case_sensitive_path_silently: %s: %s\n%!" path
+               (Printexc.to_string exn);
+     None
diff --git a/daemon/inspect_fs_windows.mli b/daemon/inspect_fs_windows.mli
new file mode 100644
index 000000000..a7dcd76ba
--- /dev/null
+++ b/daemon/inspect_fs_windows.mli
@@ -0,0 +1,24 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+val check_windows_root : Inspect_types.inspection_data -> unit
+(** Inspect the Windows [C:] filesystem mounted on sysroot. *)
+
+val is_windows_systemroot : unit -> bool
+(** Decide if the filesystem mounted on sysroot looks like a
+    Windows [C:] filesystem. *)
-- 
2.13.2




More information about the Libguestfs mailing list