Add rest of chrome_cleaner engines/common subdir

engine_resources.h and a stub implementation in
dummy_engine_resources.cc already existed. This adds the remaining
files from engine/common, plus a test utility that they require.

R=csharp

Bug: 830892
Change-Id: I9e1e31017331a3a7a75099cf711750c8749fbd90
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1526461
Commit-Queue: Joe Mason <joenotcharles@google.com>
Reviewed-by: Chris Sharp <csharp@chromium.org>
Reviewed-by: Ken Rockot <rockot@google.com>
Cr-Commit-Position: refs/heads/master@{#641622}
diff --git a/chrome/chrome_cleaner/BUILD.gn b/chrome/chrome_cleaner/BUILD.gn
index 9d73909..9fcb606 100644
--- a/chrome/chrome_cleaner/BUILD.gn
+++ b/chrome/chrome_cleaner/BUILD.gn
@@ -47,6 +47,7 @@
     # Tests from sub-directories.
     "//chrome/chrome_cleaner/chrome_utils:unittest_sources",
     "//chrome/chrome_cleaner/components:unittest_sources",
+    "//chrome/chrome_cleaner/engines/common:unittest_sources",
     "//chrome/chrome_cleaner/http:unittest_sources",
     "//chrome/chrome_cleaner/interfaces/typemaps:unittest_sources",
     "//chrome/chrome_cleaner/ipc:unittest_sources",
diff --git a/chrome/chrome_cleaner/engines/common/BUILD.gn b/chrome/chrome_cleaner/engines/common/BUILD.gn
index 02d810c..cefeca9 100644
--- a/chrome/chrome_cleaner/engines/common/BUILD.gn
+++ b/chrome/chrome_cleaner/engines/common/BUILD.gn
@@ -2,6 +2,25 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+source_set("common") {
+  sources = [
+    "engine_digest_verifier.cc",
+    "engine_digest_verifier.h",
+    "engine_result_codes.h",
+    "registry_util.cc",
+    "registry_util.h",
+    "sandbox_error_code.h",
+  ]
+
+  deps = [
+    ":resources",
+    "//base",
+    "//chrome/chrome_cleaner/os:common_os",
+    "//chrome/chrome_cleaner/strings",
+    "//sandbox/win:sandbox",
+  ]
+}
+
 source_set("resources") {
   sources = [
     "engine_resources.h",
@@ -27,3 +46,26 @@
     "//chrome/chrome_cleaner/test:test_uws_catalog",
   ]
 }
+
+source_set("unittest_sources") {
+  testonly = true
+
+  sources = [
+    "registry_util_unittest.cc",
+  ]
+
+  deps = [
+    ":common",
+    "//base:base",
+    "//chrome/chrome_cleaner/interfaces:engine_sandbox_interface",
+    "//chrome/chrome_cleaner/ipc:ipc_test_util",
+    "//chrome/chrome_cleaner/ipc:mojo_task_runner",
+    "//chrome/chrome_cleaner/os:common_os",
+    "//chrome/chrome_cleaner/test:test_util",
+    "//mojo/core/embedder",
+    "//mojo/public/cpp/bindings",
+    "//mojo/public/cpp/system",
+    "//sandbox/win:sandbox",
+    "//testing/gtest",
+  ]
+}
diff --git a/chrome/chrome_cleaner/engines/common/DEPS b/chrome/chrome_cleaner/engines/common/DEPS
new file mode 100644
index 0000000..424f244
--- /dev/null
+++ b/chrome/chrome_cleaner/engines/common/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+  # Allow the unit test to set up a mojo embedder.
+  "+mojo/core/embedder",
+]
diff --git a/chrome/chrome_cleaner/engines/common/engine_digest_verifier.cc b/chrome/chrome_cleaner/engines/common/engine_digest_verifier.cc
new file mode 100644
index 0000000..4156974
--- /dev/null
+++ b/chrome/chrome_cleaner/engines/common/engine_digest_verifier.cc
@@ -0,0 +1,18 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/chrome_cleaner/engines/common/engine_digest_verifier.h"
+
+#include "chrome/chrome_cleaner/engines/common/engine_resources.h"
+#include "chrome/chrome_cleaner/os/digest_verifier.h"
+
+namespace chrome_cleaner {
+
+scoped_refptr<DigestVerifier> GetDigestVerifier() {
+  int resource_id = GetProtectedFilesDigestResourceId();
+  return resource_id ? DigestVerifier::CreateFromResource(resource_id)
+                     : nullptr;
+}
+
+}  // namespace chrome_cleaner
diff --git a/chrome/chrome_cleaner/engines/common/engine_digest_verifier.h b/chrome/chrome_cleaner/engines/common/engine_digest_verifier.h
new file mode 100644
index 0000000..f54cc47
--- /dev/null
+++ b/chrome/chrome_cleaner/engines/common/engine_digest_verifier.h
@@ -0,0 +1,20 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_CHROME_CLEANER_ENGINES_COMMON_ENGINE_DIGEST_VERIFIER_H_
+#define CHROME_CHROME_CLEANER_ENGINES_COMMON_ENGINE_DIGEST_VERIFIER_H_
+
+#include "base/memory/scoped_refptr.h"
+#include "chrome/chrome_cleaner/engines/common/engine_digest_verifier.h"
+#include "chrome/chrome_cleaner/os/digest_verifier.h"
+
+namespace chrome_cleaner {
+
+// Creates a DigestVerifier from the protected files resource id. Returns
+// nullptr if the resource id is not set.
+scoped_refptr<DigestVerifier> GetDigestVerifier();
+
+}  // namespace chrome_cleaner
+
+#endif  // CHROME_CHROME_CLEANER_ENGINES_COMMON_ENGINE_DIGEST_VERIFIER_H_
diff --git a/chrome/chrome_cleaner/engines/common/engine_result_codes.h b/chrome/chrome_cleaner/engines/common/engine_result_codes.h
new file mode 100644
index 0000000..36c0655
--- /dev/null
+++ b/chrome/chrome_cleaner/engines/common/engine_result_codes.h
@@ -0,0 +1,31 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_CHROME_CLEANER_ENGINES_COMMON_ENGINE_RESULT_CODES_H_
+#define CHROME_CHROME_CLEANER_ENGINES_COMMON_ENGINE_RESULT_CODES_H_
+
+#include <stdint.h>
+
+namespace chrome_cleaner {
+
+// Represents result codes from the sandboxed engine.
+enum EngineResultCode : uint32_t {
+  kSuccess = 0x0,
+  kCancelled = 0x1,
+  kInvalidParameter = 0x2,
+  kWrongState = 0x3,
+  kAlreadyShutDown = 0x4,
+  kNotEnoughSpace = 0x5,
+  kSandboxUnavailable = 0x6,
+  kCleaningFailed = 0x7,
+  kCleanupInitializationFailed = 0x8,
+
+  kEngineInternal = 0x100,
+  kModuleLoadFailure = 0x101,
+  kModuleException = 0x102,
+};
+
+}  // namespace chrome_cleaner
+
+#endif  // CHROME_CHROME_CLEANER_ENGINES_COMMON_ENGINE_RESULT_CODES_H_
diff --git a/chrome/chrome_cleaner/engines/common/registry_util.cc b/chrome/chrome_cleaner/engines/common/registry_util.cc
new file mode 100644
index 0000000..327d0d2
--- /dev/null
+++ b/chrome/chrome_cleaner/engines/common/registry_util.cc
@@ -0,0 +1,282 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/chrome_cleaner/engines/common/registry_util.h"
+
+#include "base/numerics/safe_conversions.h"
+#include "base/strings/string_util.h"
+#include "sandbox/win/src/win_utils.h"
+
+using chrome_cleaner::String16EmbeddedNulls;
+
+namespace chrome_cleaner_sandbox {
+
+void InitUnicodeString(UNICODE_STRING* unicode_string,
+                       std::vector<wchar_t>* data) {
+  DCHECK(data && !data->empty() && data->back() == L'\0');
+  // As per
+  // https://msdn.microsoft.com/en-us/library/windows/hardware/ff564879.aspx
+  // Length is the length of the string in BYTES, NOT including the
+  // terminating NULL char, MaximumLength is the length of the string in BYTES
+  // INCLUDING the terminating NULL char.
+  unicode_string->Length =
+      static_cast<USHORT>((data->size() - 1) * sizeof(wchar_t));
+  unicode_string->MaximumLength =
+      static_cast<USHORT>(unicode_string->Length + sizeof(wchar_t));
+  unicode_string->Buffer = const_cast<wchar_t*>(data->data());
+}
+
+bool ValidateRegistryValueChange(const String16EmbeddedNulls& old_value,
+                                 const String16EmbeddedNulls& new_value) {
+  size_t new_cursor = 0;
+  size_t old_cursor = 0;
+  while (new_cursor < new_value.size()) {
+    while (old_cursor < old_value.size() &&
+           new_value.data()[new_cursor] != old_value.data()[old_cursor]) {
+      old_cursor++;
+    }
+
+    if (old_cursor >= old_value.size())
+      return false;
+
+    old_cursor++;
+    new_cursor++;
+  }
+
+  return true;
+}
+
+NtRegistryParamError ValidateNtRegistryValue(
+    const String16EmbeddedNulls& param) {
+  if (param.size() >= kMaxRegistryParamLength)
+    return NtRegistryParamError::TooLong;
+  return NtRegistryParamError::None;
+}
+
+NtRegistryParamError ValidateNtRegistryNullTerminatedParam(
+    const String16EmbeddedNulls& param) {
+  if (param.size() == 0)
+    return NtRegistryParamError::ZeroLength;
+  if (param.size() >= kMaxRegistryParamLength)
+    return NtRegistryParamError::TooLong;
+  if (param.data()[param.size() - 1] != L'\0')
+    return NtRegistryParamError::NotNullTerminated;
+  return NtRegistryParamError::None;
+}
+
+NtRegistryParamError ValidateNtRegistryKey(const String16EmbeddedNulls& key) {
+  NtRegistryParamError error = ValidateNtRegistryNullTerminatedParam(key);
+  if (error != NtRegistryParamError::None)
+    return error;
+
+  if (key.data()[0] == L'\\') {
+    if (!base::StartsWith(key.CastAsStringPiece16(), L"\\Registry\\",
+                          base::CompareCase::INSENSITIVE_ASCII)) {
+      return NtRegistryParamError::PathOutsideRegistry;
+    }
+  }
+
+  // TODO(joenotcharles): Ban .. to prevent path traversal. This is not
+  // critical as all the native registry functions call NtOpenKey, which will
+  // return an error for keys outside \Registry, but it should be checked for
+  // defense in depth.
+  return NtRegistryParamError::None;
+}
+
+base::string16 FormatNtRegistryMemberForLogging(
+    const String16EmbeddedNulls& key) {
+  switch (ValidateNtRegistryKey(key)) {
+    case NtRegistryParamError::NullParam:
+      return L"(null)";
+    case NtRegistryParamError::ZeroLength:
+      return L"(empty key)";
+    case NtRegistryParamError::TooLong:
+      return L"(excessively long key)";
+    default:
+      // Replace null chars with 0s for printing.
+      base::string16 str(key.CastAsStringPiece16());
+      base::ReplaceChars(str, base::StringPiece16(L"\0", 1), L"\\0", &str);
+      return str;
+  }
+}
+
+std::ostream& operator<<(std::ostream& os, NtRegistryParamError param_error) {
+  switch (param_error) {
+    case NtRegistryParamError::None:
+      os << "parameter is OK";
+      break;
+    case NtRegistryParamError::NullParam:
+      os << "parameter is NULL";
+      break;
+    case NtRegistryParamError::ZeroLength:
+      os << "parameter has length 0";
+      break;
+    case NtRegistryParamError::TooLong:
+      os << "parameter has length above the maximum";
+      break;
+    case NtRegistryParamError::NotNullTerminated:
+      os << "parameter is not NULL terminated";
+      break;
+    case NtRegistryParamError::PathOutsideRegistry:
+      os << "key path is not rooted under \\Registry";
+      break;
+    default:
+      NOTREACHED();
+      break;
+  }
+  return os;
+}
+
+NTSTATUS NativeCreateKey(HANDLE parent_key,
+                         std::vector<wchar_t>* key_name,
+                         HANDLE* out_handle,
+                         ULONG* out_disposition) {
+  DCHECK(key_name && !key_name->empty() && key_name->back() == L'\0');
+  DCHECK(out_handle);
+  *out_handle = INVALID_HANDLE_VALUE;
+
+  static NtCreateKeyFunction NtCreateKey = nullptr;
+  if (!NtCreateKey)
+    ResolveNTFunctionPtr("NtCreateKey", &NtCreateKey);
+
+  // Set up a key with an embedded null.
+  UNICODE_STRING uni_name = {0};
+  InitUnicodeString(&uni_name, key_name);
+
+  OBJECT_ATTRIBUTES obj_attr = {0};
+  InitializeObjectAttributes(&obj_attr, &uni_name, OBJ_CASE_INSENSITIVE,
+                             parent_key, /*SecurityDescriptor=*/NULL);
+
+  return NtCreateKey(out_handle, KEY_ALL_ACCESS, &obj_attr, /*TitleIndex=*/0,
+                     /*Class=*/nullptr, REG_OPTION_NON_VOLATILE,
+                     out_disposition);
+}
+
+NTSTATUS NativeOpenKey(HANDLE parent_key,
+                       const String16EmbeddedNulls& key_name,
+                       uint32_t dw_access,
+                       HANDLE* out_handle) {
+  static NtOpenKeyFunction NtOpenKey = nullptr;
+  if (!NtOpenKey)
+    ResolveNTFunctionPtr("NtOpenKey", &NtOpenKey);
+
+  // Set up a key with an embedded null.
+  std::vector<wchar_t> key_name_buffer(key_name.data());
+  UNICODE_STRING uni_name = {0};
+  InitUnicodeString(&uni_name, &key_name_buffer);
+
+  OBJECT_ATTRIBUTES obj_attr = {0};
+  InitializeObjectAttributes(&obj_attr, &uni_name, OBJ_CASE_INSENSITIVE,
+                             parent_key, /*SecurityDescriptor=*/NULL);
+
+  return NtOpenKey(out_handle, dw_access, &obj_attr);
+}
+
+NTSTATUS NativeSetValueKey(HANDLE key,
+                           const String16EmbeddedNulls& value_name,
+                           ULONG type,
+                           const String16EmbeddedNulls& value) {
+  static NtSetValueKeyFunction NtSetValueKey = nullptr;
+  if (!NtSetValueKey)
+    ResolveNTFunctionPtr("NtSetValueKey", &NtSetValueKey);
+
+  UNICODE_STRING uni_name = {};
+  std::vector<wchar_t> value_name_buffer(value_name.data());
+  if (value_name.size()) {
+    InitUnicodeString(&uni_name, &value_name_buffer);
+  }
+
+  std::vector<wchar_t> value_buffer(value.data());
+
+  // The astute reader will notice here that we pass a zero'ed out
+  // UNICODE_STRING instead of a NULL pointer in the second parameter to set the
+  // default value, which is not consistent with the MSDN documentation for
+  // ZwSetValueKey. Afaict, the documentation is incorrect, calling this with a
+  // NULL pointer returns access denied.
+  return NtSetValueKey(
+      key, &uni_name, /*TitleIndex=*/0, type,
+      reinterpret_cast<void*>(value_buffer.data()),
+      base::checked_cast<ULONG>(value_buffer.size() * sizeof(wchar_t)));
+}
+
+NTSTATUS NativeQueryValueKey(HANDLE key,
+                             const String16EmbeddedNulls& value_name,
+                             ULONG* out_type,
+                             String16EmbeddedNulls* out_value) {
+  // Figure out a) if the value exists and b) what the type of the value is.
+  static NtQueryValueKeyFunction NtQueryValueKey = nullptr;
+  if (!NtQueryValueKey)
+    ResolveNTFunctionPtr("NtQueryValueKey", &NtQueryValueKey);
+
+  UNICODE_STRING uni_name = {};
+  std::vector<wchar_t> value_name_buffer(value_name.data());
+  if (value_name.size()) {
+    InitUnicodeString(&uni_name, &value_name_buffer);
+  }
+
+  ULONG size_needed = 0;
+
+  // The astute reader will notice here that we pass a zero'ed out
+  // UNICODE_STRING instead of a NULL pointer in the second parameter to set the
+  // default value, which is not consistent with the MSDN documentation for
+  // ZwQueryValueKey. Afaict, the documentation is incorrect, calling this with
+  // a NULL pointer returns access denied.
+  NTSTATUS status = NtQueryValueKey(key, &uni_name, KeyValueFullInformation,
+                                    nullptr, 0, &size_needed);
+  if (status != STATUS_BUFFER_TOO_SMALL || size_needed == 0) {
+    LOG(ERROR) << "Call to NtQueryValueKey to query size failed. Returned: "
+               << std::hex << status;
+    return false;
+  }
+
+  std::vector<BYTE> buffer(size_needed);
+  ULONG bytes_read = 0;
+  status = NtQueryValueKey(key, &uni_name, KeyValueFullInformation,
+                           reinterpret_cast<void*>(buffer.data()), size_needed,
+                           &bytes_read);
+  if (status != STATUS_SUCCESS || bytes_read != size_needed) {
+    LOG(ERROR) << "Call to NtQueryValueKey failed.  Returned: " << std::hex
+               << status;
+    return false;
+  }
+
+  KEY_VALUE_FULL_INFORMATION* full_information =
+      reinterpret_cast<KEY_VALUE_FULL_INFORMATION*>(buffer.data());
+
+  if (out_type)
+    *out_type = full_information->Type;
+  if (out_value) {
+    // Make sure that the length is a multiple of the width of a wchar_t to
+    // avoid alignment errors.
+    if (full_information->DataLength % sizeof(wchar_t) != 0) {
+      LOG(ERROR) << "Mis-aligned size reading registry value.";
+      return false;
+    }
+
+    // Explanation for fiery mess of casts: the value data from NtQueryValueKey
+    // is in a memory block allocated right at the end of the
+    // KEY_VALUE_FULL_INFORMATION structure. The DataOffset member is the offset
+    // in BYTES of the start of the value data.
+    // The inner reinterpret_cast ensures that the pointer arithmetic uses
+    // BYTE widths.
+    // The outer reinterpret_cast then treats that address as a wchar_t*,
+    // which is safe due to the alignment check above.
+    const wchar_t* data_start =
+        reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(full_information) +
+                                   full_information->DataOffset);
+    *out_value = String16EmbeddedNulls(
+        data_start, full_information->DataLength / sizeof(wchar_t));
+  }
+
+  return true;
+}
+
+NTSTATUS NativeDeleteKey(HANDLE handle) {
+  static NtDeleteKeyFunction NtDeleteKey = nullptr;
+  if (!NtDeleteKey)
+    ResolveNTFunctionPtr("NtDeleteKey", &NtDeleteKey);
+  return NtDeleteKey(handle);
+}
+
+}  // namespace chrome_cleaner_sandbox
diff --git a/chrome/chrome_cleaner/engines/common/registry_util.h b/chrome/chrome_cleaner/engines/common/registry_util.h
new file mode 100644
index 0000000..2d1788a
--- /dev/null
+++ b/chrome/chrome_cleaner/engines/common/registry_util.h
@@ -0,0 +1,112 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_CHROME_CLEANER_ENGINES_COMMON_REGISTRY_UTIL_H_
+#define CHROME_CHROME_CLEANER_ENGINES_COMMON_REGISTRY_UTIL_H_
+
+#include <limits>
+#include <ostream>
+#include <vector>
+
+#include "chrome/chrome_cleaner/strings/string16_embedded_nulls.h"
+#include "sandbox/win/src/nt_internals.h"
+
+namespace chrome_cleaner_sandbox {
+
+// Possible errors in native registry parameter strings (keys or values). Note
+// that these strings allow embedded nulls, but must also be null terminated.
+enum class NtRegistryParamError {
+  None,
+  NullParam,
+  ZeroLength,
+  TooLong,
+  NotNullTerminated,
+  PathOutsideRegistry,  // Only valid for keys.
+};
+
+// The maximum length the sandboxed scanner/cleaner APIs will accept for a
+// registry key or value.
+constexpr uint32_t kMaxRegistryParamLength =
+    std::numeric_limits<uint16_t>::max();
+
+// Initializes the given |unicode_string| with the character data stored in
+// |data|.
+// It is assumed that |data| is a null-terminated list of wchar_ts. |data| may
+// also contain embedded NULL chars, making this a convenient alternative to
+// RtlInitUnicodeString if you wish to handle data with embedded NULLs.
+// It is important that whatever |data| points to outlives any usage
+// of |unicode_string| since |unicode_string| will point into |data|'s buffer.
+// It is also important that |data| not be modified after a call to this
+// function.
+void InitUnicodeString(UNICODE_STRING* unicode_string,
+                       std::vector<wchar_t>* data);
+
+// Returns true if |new_value| can be arrived at solely by deleting 0 or more
+// characters from |old_value|.
+bool ValidateRegistryValueChange(
+    const chrome_cleaner::String16EmbeddedNulls& old_value,
+    const chrome_cleaner::String16EmbeddedNulls& new_value);
+
+// Checks for errors in a value parameter for a native registry function.
+NtRegistryParamError ValidateNtRegistryValue(
+    const chrome_cleaner::String16EmbeddedNulls& param);
+
+// Checks for errors in a native registry function parameter that is expected
+// to be NULL-terminated (keys and value names).
+NtRegistryParamError ValidateNtRegistryNullTerminatedParam(
+    const chrome_cleaner::String16EmbeddedNulls& param);
+
+// Checks for errors in a native registry key path: all the errors detected by
+// ValidateNtRegistryParam, plus if it's an absolute path it must be under
+// \Registry.
+NtRegistryParamError ValidateNtRegistryKey(
+    const chrome_cleaner::String16EmbeddedNulls& key);
+
+// Format a native registry key, value or value name (which may contain
+// embedded NULLs) for logging.
+base::string16 FormatNtRegistryMemberForLogging(
+    const chrome_cleaner::String16EmbeddedNulls& key);
+
+// Format NtRegistryParamError and write it to a stream for logging.
+std::ostream& operator<<(std::ostream& os, NtRegistryParamError param_error);
+
+// |key_name| must be a null-terminated list of wchar_ts, that may also include
+// embedded nulls. |key_name| is not a const& since under the hood the native
+// functions take non-const pointers.
+NTSTATUS NativeCreateKey(HANDLE parent_key,
+                         std::vector<wchar_t>* key_name,
+                         HANDLE* out_handle,
+                         ULONG* out_disposition);
+
+// |key_name| must be a null-terminated string.
+NTSTATUS NativeOpenKey(HANDLE parent_key,
+                       const chrome_cleaner::String16EmbeddedNulls& key_name,
+                       uint32_t dw_access,
+                       HANDLE* out_handle);
+
+// |value_name| and |value| must be null-terminated strings. |value_name| may be
+// empty to signify setting |key|'s default value. |type| is one of the registry
+// value types described here:
+// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724884.aspx
+NTSTATUS NativeSetValueKey(
+    HANDLE key,
+    const chrome_cleaner::String16EmbeddedNulls& value_name,
+    ULONG type,
+    const chrome_cleaner::String16EmbeddedNulls& value);
+
+// Retrieves the type and data of the value under |registry_handle| specified by
+// |value_name| and places it them in |out_type| and |out_value|. Either or both
+// of |out_type| and |out_value| may be null in which case they won't be
+// returned. Returns true on success, false otherwise.
+NTSTATUS NativeQueryValueKey(
+    HANDLE key,
+    const chrome_cleaner::String16EmbeddedNulls& value_name,
+    ULONG* out_type,
+    chrome_cleaner::String16EmbeddedNulls* out_value);
+
+NTSTATUS NativeDeleteKey(HANDLE handle);
+
+}  // namespace chrome_cleaner_sandbox
+
+#endif  // CHROME_CHROME_CLEANER_ENGINES_COMMON_REGISTRY_UTIL_H_
diff --git a/chrome/chrome_cleaner/engines/common/registry_util_unittest.cc b/chrome/chrome_cleaner/engines/common/registry_util_unittest.cc
new file mode 100644
index 0000000..af22a7a
--- /dev/null
+++ b/chrome/chrome_cleaner/engines/common/registry_util_unittest.cc
@@ -0,0 +1,420 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/chrome_cleaner/engines/common/registry_util.h"
+
+#include <windows.h>
+#include <algorithm>
+#include <memory>
+#include <sstream>
+#include <utility>
+
+#include "base/base_paths.h"
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "base/numerics/safe_conversions.h"
+#include "base/path_service.h"
+#include "base/strings/string_util.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/win/win_util.h"
+#include "chrome/chrome_cleaner/interfaces/windows_handle.mojom.h"
+#include "chrome/chrome_cleaner/ipc/ipc_test_util.h"
+#include "chrome/chrome_cleaner/ipc/mojo_task_runner.h"
+#include "chrome/chrome_cleaner/os/pre_fetched_paths.h"
+#include "chrome/chrome_cleaner/test/test_native_reg_util.h"
+#include "mojo/core/embedder/embedder.h"
+#include "mojo/public/cpp/bindings/binding.h"
+#include "mojo/public/cpp/system/platform_handle.h"
+#include "sandbox/win/src/win_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/multiprocess_func_list.h"
+
+using base::WaitableEvent;
+using chrome_cleaner::MojoTaskRunner;
+using chrome_cleaner::String16EmbeddedNulls;
+using chrome_cleaner::mojom::TestWindowsHandle;
+using chrome_cleaner::mojom::TestWindowsHandlePtr;
+
+namespace chrome_cleaner_sandbox {
+
+namespace {
+
+class TestFile : public base::File {
+ public:
+  TestFile()
+      : base::File(
+            chrome_cleaner::PreFetchedPaths::GetInstance()->GetExecutablePath(),
+            FLAG_OPEN) {}
+};
+
+class TestWindowsHandleImpl : public TestWindowsHandle {
+ public:
+  explicit TestWindowsHandleImpl(
+      chrome_cleaner::mojom::TestWindowsHandleRequest request)
+      : binding_(this, std::move(request)) {}
+
+  // TestWindowsHandle
+
+  void EchoHandle(HANDLE handle, EchoHandleCallback callback) override {
+    std::move(callback).Run(handle);
+  }
+
+  void EchoRawHandle(mojo::ScopedHandle handle,
+                     EchoRawHandleCallback callback) override {
+    std::move(callback).Run(std::move(handle));
+  }
+
+ private:
+  mojo::Binding<TestWindowsHandle> binding_;
+};
+
+class SandboxParentProcess : public chrome_cleaner::ParentProcess {
+ public:
+  explicit SandboxParentProcess(scoped_refptr<MojoTaskRunner> mojo_task_runner)
+      : ParentProcess(std::move(mojo_task_runner)) {}
+
+ protected:
+  void CreateImpl(mojo::ScopedMessagePipeHandle mojo_pipe) override {
+    chrome_cleaner::mojom::TestWindowsHandleRequest request(
+        std::move(mojo_pipe));
+    test_windows_handle_impl_ =
+        std::make_unique<TestWindowsHandleImpl>(std::move(request));
+  }
+
+  void DestroyImpl() override { test_windows_handle_impl_.reset(); }
+
+ private:
+  ~SandboxParentProcess() override = default;
+
+  std::unique_ptr<TestWindowsHandleImpl> test_windows_handle_impl_;
+};
+
+class SandboxChildProcess : public chrome_cleaner::ChildProcess {
+ public:
+  explicit SandboxChildProcess(scoped_refptr<MojoTaskRunner> mojo_task_runner)
+      : ChildProcess(mojo_task_runner),
+        test_windows_handle_ptr_(std::make_unique<TestWindowsHandlePtr>()) {}
+
+  void BindToPipe(mojo::ScopedMessagePipeHandle mojo_pipe,
+                  WaitableEvent* event) {
+    test_windows_handle_ptr_->Bind(
+        chrome_cleaner::mojom::TestWindowsHandlePtrInfo(std::move(mojo_pipe),
+                                                        0));
+    event->Signal();
+  }
+
+  HANDLE EchoHandle(HANDLE input_handle) {
+    HANDLE output_handle;
+    WaitableEvent event(WaitableEvent::ResetPolicy::MANUAL,
+                        WaitableEvent::InitialState::NOT_SIGNALED);
+    auto callback = base::BindOnce(
+        [](HANDLE* handle_holder, WaitableEvent* event, HANDLE handle) {
+          *handle_holder = handle;
+          event->Signal();
+        },
+        &output_handle, &event);
+    mojo_task_runner_->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            [](TestWindowsHandlePtr* ptr, HANDLE handle,
+               TestWindowsHandle::EchoHandleCallback callback) {
+              (*ptr)->EchoHandle(std::move(handle), std::move(callback));
+            },
+            base::Unretained(test_windows_handle_ptr_.get()), input_handle,
+            std::move(callback)));
+    event.Wait();
+    return output_handle;
+  }
+
+  HANDLE EchoRawHandle(HANDLE input_handle) {
+    mojo::ScopedHandle scoped_handle = mojo::WrapPlatformFile(input_handle);
+    mojo::ScopedHandle output_handle;
+    WaitableEvent event(WaitableEvent::ResetPolicy::MANUAL,
+                        WaitableEvent::InitialState::NOT_SIGNALED);
+    auto callback = base::BindOnce(
+        [](mojo::ScopedHandle* handle_holder, WaitableEvent* event,
+           mojo::ScopedHandle handle) {
+          *handle_holder = std::move(handle);
+          event->Signal();
+        },
+        &output_handle, &event);
+
+    mojo_task_runner_->PostTask(
+        FROM_HERE, base::BindOnce(
+                       [](TestWindowsHandlePtr* ptr, mojo::ScopedHandle handle,
+                          TestWindowsHandle::EchoRawHandleCallback callback) {
+                         (*ptr)->EchoRawHandle(std::move(handle),
+                                               std::move(callback));
+                       },
+                       base::Unretained(test_windows_handle_ptr_.get()),
+                       base::Passed(&scoped_handle), std::move(callback)));
+    event.Wait();
+
+    HANDLE raw_output_handle;
+    CHECK_EQ(
+        mojo::UnwrapPlatformFile(std::move(output_handle), &raw_output_handle),
+        MOJO_RESULT_OK);
+    return raw_output_handle;
+  }
+
+ private:
+  ~SandboxChildProcess() override {
+    mojo_task_runner_->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            [](std::unique_ptr<TestWindowsHandlePtr> ptr) { ptr.reset(); },
+            base::Passed(&test_windows_handle_ptr_)));
+  }
+
+  std::unique_ptr<TestWindowsHandlePtr> test_windows_handle_ptr_;
+};
+
+base::string16 HandlePath(HANDLE handle) {
+  base::string16 full_path;
+  // The size parameter of GetFinalPathNameByHandle does NOT include the null
+  // terminator.
+  DWORD result = ::GetFinalPathNameByHandleW(
+      handle, base::WriteInto(&full_path, MAX_PATH), MAX_PATH - 1, 0);
+  if (result > MAX_PATH) {
+    result = ::GetFinalPathNameByHandle(
+        handle, base::WriteInto(&full_path, result), result - 1, 0);
+  }
+  if (!result) {
+    PLOG(ERROR) << "Could not get full path for handle " << handle;
+    return base::string16();
+  }
+  return full_path;
+}
+
+::testing::AssertionResult HandlesAreEqual(HANDLE handle1, HANDLE handle2) {
+  // The best way to check this is CompareObjectHandles, but it isn't available
+  // until Windows 10. So just check that both refer to the same path.
+  base::string16 path1 = HandlePath(handle1);
+  base::string16 path2 = HandlePath(handle2);
+
+  if (path1.empty() || path2.empty() || path1 != path2) {
+    auto format_message = [](HANDLE handle, const base::string16& path) {
+      std::ostringstream s;
+      s << handle;
+      if (path.empty())
+        s << " has no valid path";
+      else
+        s << " has path " << path;
+      return s.str();
+    };
+    return ::testing::AssertionFailure()
+           << format_message(handle1, path1) << ", "
+           << format_message(handle2, path2);
+  }
+  return ::testing::AssertionSuccess();
+}
+
+}  // namespace
+
+MULTIPROCESS_TEST_MAIN(HandleWrappingIPCMain) {
+  auto mojo_task_runner = MojoTaskRunner::Create();
+  auto child_process =
+      base::MakeRefCounted<SandboxChildProcess>(mojo_task_runner);
+  auto message_pipe_handle = child_process->CreateMessagePipeFromCommandLine();
+
+  WaitableEvent event(WaitableEvent::ResetPolicy::MANUAL,
+                      WaitableEvent::InitialState::NOT_SIGNALED);
+  mojo_task_runner->PostTask(
+      FROM_HERE, base::BindOnce(&SandboxChildProcess::BindToPipe, child_process,
+                                base::Passed(&message_pipe_handle), &event));
+  event.Wait();
+
+  // Check that this test is actually testing what it thinks it is: when
+  // passing a mojo::ScopedHandle to another process, Mojo attempts to
+  // duplicate and close the original handle. This fails if the ScopedHandle is
+  // wrapping a Windows pseudo-handle, and nullptr gets passed instead. The
+  // WindowsHandle wrapper avoids this.
+  //
+  // However, if the mojo::ScopedHandle is passed over a Mojo connection that
+  // isn't going to another process, pseudo-handles are passed correctly. So
+  // make sure the test connection actually causes the error with
+  // mojo::ScopedHandle. If not, the tests of WindowsHandle will trivially
+  // succeed without demonstrating that WindowsHandle avoids the error.
+  CHECK_EQ(nullptr, child_process->EchoRawHandle(HKEY_CLASSES_ROOT));
+
+  CHECK_EQ(INVALID_HANDLE_VALUE,
+           child_process->EchoHandle(INVALID_HANDLE_VALUE));
+  CHECK_EQ(nullptr, child_process->EchoHandle(nullptr));
+  CHECK_EQ(HKEY_CLASSES_ROOT, child_process->EchoHandle(HKEY_CLASSES_ROOT));
+  CHECK_EQ(HKEY_CURRENT_CONFIG, child_process->EchoHandle(HKEY_CURRENT_CONFIG));
+  CHECK_EQ(HKEY_CURRENT_USER, child_process->EchoHandle(HKEY_CURRENT_USER));
+  CHECK_EQ(HKEY_LOCAL_MACHINE, child_process->EchoHandle(HKEY_LOCAL_MACHINE));
+  CHECK_EQ(HKEY_USERS, child_process->EchoHandle(HKEY_USERS));
+
+  // mojo::ScopedHandle CHECKS if given an invalid handle, so pass this handle
+  // raw and ensure it is marked as invalid.
+  HANDLE fake_handle = base::win::Uint32ToHandle(0x12345678);
+  CHECK_EQ(nullptr, child_process->EchoRawHandle(fake_handle));
+
+  TestFile test_file;
+  HANDLE test_handle = test_file.GetPlatformFile();
+  CHECK(HandlesAreEqual(test_handle, child_process->EchoHandle(test_handle)));
+
+  return 0;
+}
+
+TEST(SandboxUtil, HandleWrappingIPC) {
+  auto mojo_task_runner = MojoTaskRunner::Create();
+  auto parent_process =
+      base::MakeRefCounted<SandboxParentProcess>(mojo_task_runner);
+
+  int32_t exit_code = -1;
+  EXPECT_TRUE(parent_process->LaunchConnectedChildProcess(
+      "HandleWrappingIPCMain", &exit_code));
+  EXPECT_EQ(0, exit_code);
+}
+
+TEST(SandboxUtil, NativeQueryValueKey) {
+  std::vector<wchar_t> key_name{L'a', L'b', L'c', L'\0'};
+  String16EmbeddedNulls value_name1{L'f', L'o', L'o', L'1', L'\0'};
+  String16EmbeddedNulls value_name2{L'f', L'o', L'o', L'2', L'\0'};
+  String16EmbeddedNulls value_name3{L'f', L'o', L'o', L'3', L'\0'};
+  String16EmbeddedNulls value_name4{L'f', L'o', L'o', L'4', L'\0'};
+  String16EmbeddedNulls value{L'b', L'a', L'r', L'\0'};
+
+  struct TestCases {
+    const String16EmbeddedNulls& value_name;
+    ULONG reg_type;
+  } test_cases[] = {
+      {value_name1, REG_SZ},
+      {value_name2, REG_EXPAND_SZ},
+      {value_name3, REG_DWORD},
+      {value_name4, REG_BINARY},
+  };
+
+  ScopedTempRegistryKey temp_key;
+
+  ULONG disposition = 0;
+  HANDLE subkey_handle = INVALID_HANDLE_VALUE;
+  EXPECT_EQ(STATUS_SUCCESS, NativeCreateKey(temp_key.Get(), &key_name,
+                                            &subkey_handle, &disposition));
+  EXPECT_EQ(static_cast<ULONG>(REG_CREATED_NEW_KEY), disposition);
+
+  for (auto test_case : test_cases) {
+    EXPECT_EQ(STATUS_SUCCESS,
+              NativeSetValueKey(subkey_handle, test_case.value_name,
+                                test_case.reg_type, value));
+    DWORD actual_reg_type = 0;
+    String16EmbeddedNulls actual_value;
+    EXPECT_TRUE(NativeQueryValueKey(subkey_handle, test_case.value_name,
+                                    &actual_reg_type, &actual_value));
+    EXPECT_EQ(test_case.reg_type, actual_reg_type);
+    EXPECT_EQ(value, actual_value);
+  }
+
+  EXPECT_EQ(STATUS_SUCCESS, NativeDeleteKey(subkey_handle));
+  EXPECT_TRUE(::CloseHandle(subkey_handle));
+}
+
+TEST(SandboxUtil, ValidateRegistryValueChange) {
+  String16EmbeddedNulls eq{L'a', L'b', L'\0', L'c'};
+  EXPECT_TRUE(ValidateRegistryValueChange(eq, eq));
+
+  String16EmbeddedNulls subset1{L'a', L'b', L'\0', L'c'};
+  String16EmbeddedNulls subset2{L'a', L'\0', L'c'};
+  EXPECT_TRUE(ValidateRegistryValueChange(subset1, subset2));
+
+  String16EmbeddedNulls prefix1{L'a', L'b', L'\0', L'c'};
+  String16EmbeddedNulls prefix2{L'b', L'\0', L'c'};
+  EXPECT_TRUE(ValidateRegistryValueChange(prefix1, prefix2));
+
+  String16EmbeddedNulls suffix1{L'a', L'b', L'\0', L'c'};
+  String16EmbeddedNulls suffix2{L'a', L'b', L'\0'};
+  EXPECT_TRUE(ValidateRegistryValueChange(suffix1, suffix2));
+
+  String16EmbeddedNulls empty1{L'a', L'b', L'\0', L'c'};
+  String16EmbeddedNulls empty2;
+  EXPECT_TRUE(ValidateRegistryValueChange(empty1, empty2));
+
+  String16EmbeddedNulls super_empty1;
+  String16EmbeddedNulls super_empty2{L'a', L'b', L'\0', L'c'};
+  EXPECT_FALSE(ValidateRegistryValueChange(super_empty1, super_empty2));
+
+  String16EmbeddedNulls superset1{L'a', L'\0', L'c'};
+  String16EmbeddedNulls superset2{L'a', L'b', L'\0', L'c'};
+  EXPECT_FALSE(ValidateRegistryValueChange(superset1, superset2));
+
+  String16EmbeddedNulls bad_prefix1{L'b', L'\0', L'c'};
+  String16EmbeddedNulls bad_prefix2{L'a', L'b', L'\0', L'c'};
+  EXPECT_FALSE(ValidateRegistryValueChange(bad_prefix1, bad_prefix2));
+
+  String16EmbeddedNulls bad_suffix1{L'a', L'b', L'\0'};
+  String16EmbeddedNulls bad_suffix2{L'a', L'b', L'\0', L'c'};
+  EXPECT_FALSE(ValidateRegistryValueChange(bad_suffix1, bad_suffix2));
+
+  String16EmbeddedNulls different1{L'a', L'b', L'\0', L'c'};
+  String16EmbeddedNulls different2{L'd', L'e', L'f'};
+  EXPECT_FALSE(ValidateRegistryValueChange(different1, different2));
+}
+
+TEST(SandboxUtil, ValidateFailureOfNtSetValueKeyOnNull) {
+  // The documentation for NtSetValueKey is incorrect, calling it with a
+  // NULL pointer returns access denied. This test ensures that the observed
+  // behaviour remains consistent.
+  std::vector<wchar_t> key_name{L'a', L'b', L'c', L'\0'};
+  std::vector<wchar_t> value{L'b', L'a', L'r', L'\0'};
+
+  ScopedTempRegistryKey temp_key;
+
+  ULONG disposition = 0;
+  HANDLE subkey_handle = INVALID_HANDLE_VALUE;
+  EXPECT_EQ(STATUS_SUCCESS, NativeCreateKey(temp_key.Get(), &key_name,
+                                            &subkey_handle, &disposition));
+  EXPECT_EQ(static_cast<ULONG>(REG_CREATED_NEW_KEY), disposition);
+
+  static NtSetValueKeyFunction NtSetValueKey = nullptr;
+  if (!NtSetValueKey)
+    ResolveNTFunctionPtr("NtSetValueKey", &NtSetValueKey);
+
+  NTSTATUS status =
+      NtSetValueKey(subkey_handle, nullptr, /*TitleIndex=*/0, REG_SZ,
+                    reinterpret_cast<void*>(value.data()),
+                    base::checked_cast<ULONG>(value.size() * sizeof(wchar_t)));
+  EXPECT_EQ(static_cast<NTSTATUS>(STATUS_ACCESS_VIOLATION), status);
+
+  EXPECT_EQ(STATUS_SUCCESS, NativeDeleteKey(subkey_handle));
+  EXPECT_TRUE(::CloseHandle(subkey_handle));
+}
+
+TEST(SandboxUtil, ValidateFailureOfNtQueryValueKeyOnNull) {
+  // The documentation for NtQueryValueKey is incorrect, calling it with a
+  // NULL pointer returns access denied. This test ensures that the observed
+  // behaviour remains consistent.
+  std::vector<wchar_t> key_name{L'a', L'b', L'c', L'\0'};
+
+  String16EmbeddedNulls value{L'b', L'a', L'r', L'\0'};
+
+  ScopedTempRegistryKey temp_key;
+
+  ULONG disposition = 0;
+  HANDLE subkey_handle = INVALID_HANDLE_VALUE;
+  EXPECT_EQ(STATUS_SUCCESS, NativeCreateKey(temp_key.Get(), &key_name,
+                                            &subkey_handle, &disposition));
+  EXPECT_EQ(static_cast<ULONG>(REG_CREATED_NEW_KEY), disposition);
+
+  // Create a default value.
+  EXPECT_EQ(STATUS_SUCCESS,
+            NativeSetValueKey(subkey_handle, String16EmbeddedNulls(nullptr),
+                              REG_SZ, value));
+
+  static NtQueryValueKeyFunction NtQueryValueKey = nullptr;
+  if (!NtQueryValueKey)
+    ResolveNTFunctionPtr("NtQueryValueKey", &NtQueryValueKey);
+
+  DWORD size_needed = 0;
+  NTSTATUS status =
+      NtQueryValueKey(subkey_handle, nullptr, KeyValueFullInformation, nullptr,
+                      0, &size_needed);
+  EXPECT_EQ(static_cast<NTSTATUS>(STATUS_ACCESS_VIOLATION), status);
+
+  EXPECT_EQ(STATUS_SUCCESS, NativeDeleteKey(subkey_handle));
+  EXPECT_TRUE(::CloseHandle(subkey_handle));
+}
+
+}  // namespace chrome_cleaner_sandbox
diff --git a/chrome/chrome_cleaner/engines/common/sandbox_error_code.h b/chrome/chrome_cleaner/engines/common/sandbox_error_code.h
new file mode 100644
index 0000000..e37e307
--- /dev/null
+++ b/chrome/chrome_cleaner/engines/common/sandbox_error_code.h
@@ -0,0 +1,38 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_CHROME_CLEANER_ENGINES_COMMON_SANDBOX_ERROR_CODE_H_
+#define CHROME_CHROME_CLEANER_ENGINES_COMMON_SANDBOX_ERROR_CODE_H_
+
+#include <stdint.h>
+
+namespace chrome_cleaner {
+
+// A list of custom error codes that can be returned by sandbox callbacks.
+// These enums must be defined such that their values don't conflict with
+// Windows error codes (so they must set bit 29 to 1).
+enum SandboxErrorCode : uint32_t {
+  INVALID_DW_ACCESS = 0x10000000,
+  NULL_ROOT_KEY = 0x10000001,
+  INVALID_SUBKEY_STRING = 0x10000002,
+  NULL_ROOT_AND_RELATIVE_SUB_KEY = 0x10000003,
+  NULL_OUTPUT_HANDLE = 0x10000004,
+  NULL_SUB_KEY = 0x10000005,
+  FILE_NAME_TOO_LONG = 0x10000006,
+  NULL_FILE_NAME = 0x10000007,
+  NULL_NAME = 0x10000008,
+  NAME_TOO_LONG = 0x10000009,
+  INVALID_KEY = 0x1000000A,
+  INVALID_VALUE_NAME = 0x1000000B,
+  INVALID_VALUE = 0x1000000C,
+  BAD_SID = 0x1000000D,
+  NULL_FIND_HANDLE = 0x1000000E,
+  NULL_DATA_HANDLE = 0x1000000F,
+  RELATIVE_PATH_NOT_ALLOWED = 0x10000010,
+  INTERNAL_ERROR = 0x1FFFFFFF,
+};
+
+}  // namespace chrome_cleaner
+
+#endif  // CHROME_CHROME_CLEANER_ENGINES_COMMON_SANDBOX_ERROR_CODE_H_
diff --git a/chrome/chrome_cleaner/test/BUILD.gn b/chrome/chrome_cleaner/test/BUILD.gn
index 8426dd6..8543ed3 100644
--- a/chrome/chrome_cleaner/test/BUILD.gn
+++ b/chrome/chrome_cleaner/test/BUILD.gn
@@ -156,6 +156,8 @@
     "test_file_util.h",
     "test_layered_service_provider.cc",
     "test_layered_service_provider.h",
+    "test_native_reg_util.cc",
+    "test_native_reg_util.h",
     "test_registry_util.cc",
     "test_registry_util.h",
     "test_settings_util.cc",
diff --git a/chrome/chrome_cleaner/test/test_native_reg_util.cc b/chrome/chrome_cleaner/test/test_native_reg_util.cc
new file mode 100644
index 0000000..3873c71
--- /dev/null
+++ b/chrome/chrome_cleaner/test/test_native_reg_util.cc
@@ -0,0 +1,55 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/chrome_cleaner/test/test_native_reg_util.h"
+
+#include "base/guid.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string16.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/test_reg_util_win.h"
+#include "base/time/time.h"
+#include "base/win/win_util.h"
+#include "chrome/chrome_cleaner/os/registry.h"
+
+namespace chrome_cleaner_sandbox {
+
+namespace {
+
+const base::char16 kTimestampDelimiter[] = STRING16_LITERAL("$");
+const base::char16 kTempTestKeyPath[] =
+    STRING16_LITERAL("Software\\ChromeCleaner\\NativeTempTestKeys");
+
+}  // namespace
+
+ScopedTempRegistryKey::ScopedTempRegistryKey() {
+  const base::TimeDelta timestamp =
+      base::Time::Now().ToDeltaSinceWindowsEpoch();
+  key_path_ = base::StrCat(
+      {kTempTestKeyPath, base::Int64ToString16(timestamp.InMicroseconds()),
+       kTimestampDelimiter, base::ASCIIToUTF16(base::GenerateGUID())});
+
+  CHECK(ERROR_SUCCESS ==
+        reg_key_.Create(HKEY_LOCAL_MACHINE, key_path_.c_str(), KEY_ALL_ACCESS));
+  chrome_cleaner::GetNativeKeyPath(reg_key_, &fully_qualified_key_path_);
+}
+
+ScopedTempRegistryKey::~ScopedTempRegistryKey() {
+  reg_key_.DeleteKey(L"");
+}
+
+HANDLE ScopedTempRegistryKey::Get() {
+  return reg_key_.Handle();
+}
+
+const base::string16& ScopedTempRegistryKey::Path() const {
+  return key_path_;
+}
+
+const base::string16& ScopedTempRegistryKey::FullyQualifiedPath() const {
+  return fully_qualified_key_path_;
+}
+
+}  // namespace chrome_cleaner_sandbox
diff --git a/chrome/chrome_cleaner/test/test_native_reg_util.h b/chrome/chrome_cleaner/test/test_native_reg_util.h
new file mode 100644
index 0000000..02732d09
--- /dev/null
+++ b/chrome/chrome_cleaner/test/test_native_reg_util.h
@@ -0,0 +1,38 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_CHROME_CLEANER_TEST_TEST_NATIVE_REG_UTIL_H_
+#define CHROME_CHROME_CLEANER_TEST_TEST_NATIVE_REG_UTIL_H_
+
+#include <vector>
+
+#include "base/strings/string16.h"
+#include "base/win/registry.h"
+
+namespace chrome_cleaner_sandbox {
+
+// Creates a temporary registry key for tests to play under. Useful for tests
+// using native APIs that will bypass advapi's registry redirection.
+class ScopedTempRegistryKey {
+ public:
+  ScopedTempRegistryKey();
+  ~ScopedTempRegistryKey();
+
+  HANDLE Get();
+
+  // Returne the relative registry path.
+  const base::string16& Path() const;
+
+  // Returns a fully qualified native registry path.
+  const base::string16& FullyQualifiedPath() const;
+
+ private:
+  base::string16 key_path_;
+  base::string16 fully_qualified_key_path_;
+  base::win::RegKey reg_key_;
+};
+
+}  // namespace chrome_cleaner_sandbox
+
+#endif  // CHROME_CHROME_CLEANER_TEST_TEST_NATIVE_REG_UTIL_H_