diff --git a/daemon/Makefile.am b/daemon/Makefile.am index f035add2b..f9de758dc 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -256,6 +256,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 \ @@ -297,6 +298,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..78af7f048 --- /dev/null +++ b/daemon/inspect_fs_windows.ml @@ -0,0 +1,480 @@ +(* 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 = + PCRE.compile "^(multi|scsi)\\((\\d+)\\)disk\\((\\d+)\\)rdisk\\((\\d+)\\)partition\\((\\d+)\\)([^=]+)=" + +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 () -> read_small_file boot_ini_path) () in + match lines with + | None -> None + | Some lines -> + (* 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 PCRE.matches re_boot_ini_os line then ( + let ctrlr_type = PCRE.sub 1 + and ctrlr = int_of_string (PCRE.sub 2) + and disk = int_of_string (PCRE.sub 3) + and rdisk = int_of_string (PCRE.sub 4) + and part = int_of_string (PCRE.sub 5) + and path = PCRE.sub 6 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 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_path 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_string 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_string 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_string 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_path 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_string 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. *)