Commit 67667a8e authored by Greg Thompson's avatar Greg Thompson Committed by Commit Bot

Stop leaking files extracted by mini_installer.

With this CL, mini_installer now holds the files it creates open with
FLAG_DELETE_ON_CLOSE so that they are automatically deleted by the
filesystem when they are no longer needed.

The most risky operation is the multi-step process to drop write
permission on the files so that they can be opened for consumption by
parties that do not allow writers. New process exits codes have been
introduced to track whether or not this operation fails in practice.

BUG=516207

Change-Id: Ic5e25692cf3dca0fcc7cd01faf5759648f5c6890
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2307250Reviewed-by: default avatarWill Harris <wfh@chromium.org>
Commit-Queue: Greg Thompson <grt@chromium.org>
Cr-Commit-Position: refs/heads/master@{#790735}
parent 03ae2f46
...@@ -76,6 +76,7 @@ source_set("unit_tests") { ...@@ -76,6 +76,7 @@ source_set("unit_tests") {
"mini_file_test.cc", "mini_file_test.cc",
"mini_installer_unittest.cc", "mini_installer_unittest.cc",
"mini_string_test.cc", "mini_string_test.cc",
"pe_resource_test.cc",
] ]
public_deps = [ ":lib" ] public_deps = [ ":lib" ]
......
...@@ -22,7 +22,7 @@ struct ExpandContext { ...@@ -22,7 +22,7 @@ struct ExpandContext {
const wchar_t* const dest_path; const wchar_t* const dest_path;
// The destination file; valid once the destination is created. // The destination file; valid once the destination is created.
mini_installer::MiniFile dest_file; mini_installer::MiniFile& dest_file;
// Set to true if the file was extracted to |dest_path|. Note that |dest_file| // Set to true if the file was extracted to |dest_path|. Note that |dest_file|
// may be valid even in case of failure. // may be valid even in case of failure.
...@@ -247,7 +247,7 @@ bool InitializeFdi() { ...@@ -247,7 +247,7 @@ bool InitializeFdi() {
namespace mini_installer { namespace mini_installer {
bool Expand(const wchar_t* source, const wchar_t* destination) { bool Expand(const wchar_t* source, const wchar_t* destination, MiniFile& file) {
if (!InitializeFdi()) if (!InitializeFdi())
return false; return false;
...@@ -274,7 +274,7 @@ bool Expand(const wchar_t* source, const wchar_t* destination) { ...@@ -274,7 +274,7 @@ bool Expand(const wchar_t* source, const wchar_t* destination) {
if (!fdi) if (!fdi)
return false; return false;
ExpandContext context = {destination, {}, /*succeeded=*/false}; ExpandContext context = {destination, file, /*succeeded=*/false};
g_FDICopy(fdi, source_name_utf8, source_path_utf8, 0, &Notify, nullptr, g_FDICopy(fdi, source_name_utf8, source_path_utf8, 0, &Notify, nullptr,
&context); &context);
g_FDIDestroy(fdi); g_FDIDestroy(fdi);
...@@ -282,8 +282,7 @@ bool Expand(const wchar_t* source, const wchar_t* destination) { ...@@ -282,8 +282,7 @@ bool Expand(const wchar_t* source, const wchar_t* destination) {
return true; return true;
// Delete the output file if it was created. // Delete the output file if it was created.
if (context.dest_file.IsValid()) file.Close();
context.dest_file.DeleteOnClose();
return false; return false;
} }
......
...@@ -7,11 +7,14 @@ ...@@ -7,11 +7,14 @@
namespace mini_installer { namespace mini_installer {
// Same as the tool, expand.exe. Decompresses a file that was compressed class MiniFile;
// using Microsoft's MSCF compression algorithm.
// |source| is the full path of the file to decompress and |destination| // Expands the first file in |source| to the file |destination| using
// is the full path of the target file. // Microsoft's MSCF compression algorithm (a la expand.exe). Returns true on
bool Expand(const wchar_t* source, const wchar_t* destination); // success, in which case |file| holds an open handle to the destination file.
// |file| will be opened with exclusive write access and shared read and delete
// access, and will be marked as delete-on-close.
bool Expand(const wchar_t* source, const wchar_t* destination, MiniFile& file);
} // namespace mini_installer } // namespace mini_installer
......
...@@ -5,9 +5,11 @@ ...@@ -5,9 +5,11 @@
#include <windows.h> #include <windows.h>
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h" #include "base/files/scoped_temp_dir.h"
#include "base/path_service.h" #include "base/path_service.h"
#include "chrome/installer/mini_installer/decompress.h" #include "chrome/installer/mini_installer/decompress.h"
#include "chrome/installer/mini_installer/mini_file.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
TEST(MiniDecompressTest, ExpandTest) { TEST(MiniDecompressTest, ExpandTest) {
...@@ -27,11 +29,21 @@ TEST(MiniDecompressTest, ExpandTest) { ...@@ -27,11 +29,21 @@ TEST(MiniDecompressTest, ExpandTest) {
temp_dir.GetPath().Append(FILE_PATH_LITERAL("setup.exe"))); temp_dir.GetPath().Append(FILE_PATH_LITERAL("setup.exe")));
// Decompress our test file. // Decompress our test file.
EXPECT_TRUE(mini_installer::Expand(source_path.value().c_str(), mini_installer::MiniFile file(mini_installer::MiniFile::DeleteOnClose::kYes);
dest_path.value().c_str())); ASSERT_TRUE(mini_installer::Expand(source_path.value().c_str(),
dest_path.value().c_str(), file));
ASSERT_TRUE(file.IsValid());
ASSERT_PRED1(base::PathExists, dest_path);
// Drop write permission so that the call below can open the file.
ASSERT_TRUE(file.DropWritePermission());
// Check if the expanded file is a valid executable. // Check if the expanded file is a valid executable.
DWORD type = static_cast<DWORD>(-1); DWORD type = static_cast<DWORD>(-1);
EXPECT_TRUE(GetBinaryType(dest_path.value().c_str(), &type)); EXPECT_TRUE(GetBinaryType(dest_path.value().c_str(), &type));
EXPECT_EQ(static_cast<DWORD>(SCS_32BIT_BINARY), type); EXPECT_EQ(static_cast<DWORD>(SCS_32BIT_BINARY), type);
// Closing the handle should delete the file.
file.Close();
EXPECT_FALSE(base::PathExists(dest_path));
} }
...@@ -42,6 +42,11 @@ enum ExitCode { ...@@ -42,6 +42,11 @@ enum ExitCode {
RUN_SETUP_FAILED_FILE_NOT_FOUND = 122, // ERROR_FILE_NOT_FOUND. RUN_SETUP_FAILED_FILE_NOT_FOUND = 122, // ERROR_FILE_NOT_FOUND.
RUN_SETUP_FAILED_PATH_NOT_FOUND = 123, // ERROR_PATH_NOT_FOUND. RUN_SETUP_FAILED_PATH_NOT_FOUND = 123, // ERROR_PATH_NOT_FOUND.
RUN_SETUP_FAILED_COULD_NOT_CREATE_PROCESS = 124, // All other errors. RUN_SETUP_FAILED_COULD_NOT_CREATE_PROCESS = 124, // All other errors.
UNABLE_TO_OPEN_PATCHED_SETUP = 125,
DROP_WRITE_ON_EXTRACTED_ARCHIVE_FAILED = 126,
DROP_WRITE_ON_EXTRACTED_SETUP_PATCH_FAILED = 127,
DROP_WRITE_ON_EXTRACTED_SETUP_FAILED = 128,
DROP_WRITE_ON_EXPANDED_SETUP_FAILED = 129,
}; };
} // namespace mini_installer } // namespace mini_installer
......
...@@ -8,7 +8,10 @@ ...@@ -8,7 +8,10 @@
namespace mini_installer { namespace mini_installer {
MiniFile::MiniFile() = default; MiniFile::MiniFile(DeleteOnClose delete_on_close)
: delete_on_close_flag_(delete_on_close != DeleteOnClose::kNo
? FILE_FLAG_DELETE_ON_CLOSE
: 0) {}
MiniFile::~MiniFile() { MiniFile::~MiniFile() {
Close(); Close();
...@@ -26,9 +29,11 @@ bool MiniFile::Create(const wchar_t* path) { ...@@ -26,9 +29,11 @@ bool MiniFile::Create(const wchar_t* path) {
Close(); Close();
if (!path_.assign(path)) if (!path_.assign(path))
return false; return false;
handle_ = handle_ = ::CreateFileW(path_.get(), GENERIC_WRITE,
::CreateFileW(path_.get(), DELETE | GENERIC_WRITE, FILE_SHARE_DELETE, FILE_SHARE_DELETE | FILE_SHARE_READ,
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); /*lpSecurityAttributes=*/nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | delete_on_close_flag_,
/*hTemplateFile=*/nullptr);
if (handle_ != INVALID_HANDLE_VALUE) if (handle_ != INVALID_HANDLE_VALUE)
return true; return true;
path_.clear(); path_.clear();
...@@ -39,11 +44,76 @@ bool MiniFile::IsValid() const { ...@@ -39,11 +44,76 @@ bool MiniFile::IsValid() const {
return handle_ != INVALID_HANDLE_VALUE; return handle_ != INVALID_HANDLE_VALUE;
} }
bool MiniFile::DeleteOnClose() { bool MiniFile::DropWritePermission() {
FILE_DISPOSITION_INFO disposition = {/*DeleteFile=*/TRUE}; // The original file was opened with write access (of course), so it will take
return IsValid() && // a little hoop jumping to get a handle without it. First, get a new handle
::SetFileInformationByHandle(handle_, FileDispositionInfo, // that doesn't have write access. This one must allow others to write on
&disposition, sizeof(disposition)); // account of the fact that the original handle has write access.
HANDLE without_write =
::ReOpenFile(handle_, GENERIC_READ,
FILE_SHARE_DELETE | FILE_SHARE_WRITE | FILE_SHARE_READ,
delete_on_close_flag_);
if (without_write == INVALID_HANDLE_VALUE) {
const auto error = ::GetLastError();
Close();
::SetLastError(error);
return false;
}
// Next, close the original handle so that there are no longer any writers.
// This will mark the file for deletion if the original handle was opened with
// FILE_FLAG_DELETE_ON_CLOSE.
::CloseHandle(std::exchange(handle_, INVALID_HANDLE_VALUE));
// Now unmark the file for deletion if needed.
if (delete_on_close_flag_) {
FILE_DISPOSITION_INFO disposition = {/*DeleteFile=*/FALSE};
if (!::SetFileInformationByHandle(without_write, FileDispositionInfo,
&disposition, sizeof(disposition))) {
const auto error = ::GetLastError();
::CloseHandle(std::exchange(without_write, INVALID_HANDLE_VALUE));
Close();
::SetLastError(error);
return false;
}
}
// Now open a read-only handle (with FILE_FLAG_DELETE_ON_CLOSE as needed) that
// doesn't allow others to write. Note that there is a potential race here:
// another party could open the file for shared write access at this precise
// moment, causing this ReOpenFile to fail. This would likely be an issue
// anyway, as one common thing to do with the file is to execute it, which
// will fail if there are writers.
handle_ =
::ReOpenFile(without_write, GENERIC_READ,
FILE_SHARE_DELETE | FILE_SHARE_READ, delete_on_close_flag_);
if (handle_ == INVALID_HANDLE_VALUE) {
const auto error = ::GetLastError();
::CloseHandle(std::exchange(without_write, INVALID_HANDLE_VALUE));
Close();
::SetLastError(error);
return false;
}
// Closing the handle that allowed shared writes may once again mark the file
// for deletion.
::CloseHandle(std::exchange(without_write, INVALID_HANDLE_VALUE));
// Everything went according to plan; |handle_| is now lacking write access
// and does not allow other writers. The last step is to unmark the file for
// deletion once again, as the closure of |without_write| has re-marked it.
if (delete_on_close_flag_) {
FILE_DISPOSITION_INFO disposition = {/*DeleteFile=*/FALSE};
if (!::SetFileInformationByHandle(handle_, FileDispositionInfo,
&disposition, sizeof(disposition))) {
const auto error = ::GetLastError();
Close();
::SetLastError(error);
return false;
}
}
return true;
} }
void MiniFile::Close() { void MiniFile::Close() {
...@@ -64,6 +134,18 @@ HANDLE MiniFile::DuplicateHandle() const { ...@@ -64,6 +134,18 @@ HANDLE MiniFile::DuplicateHandle() const {
: INVALID_HANDLE_VALUE; : INVALID_HANDLE_VALUE;
} }
bool MiniFile::Open(const PathString& path) {
Close();
handle_ = ::CreateFileW(path.get(), GENERIC_READ,
FILE_SHARE_DELETE | FILE_SHARE_READ,
/*lpSecurityAttributes=*/nullptr, OPEN_EXISTING,
delete_on_close_flag_, /*hTemplateFile=*/nullptr);
if (handle_ == INVALID_HANDLE_VALUE)
return false;
path_.assign(path);
return true;
}
HANDLE MiniFile::GetHandleUnsafe() const { HANDLE MiniFile::GetHandleUnsafe() const {
return handle_; return handle_;
} }
......
...@@ -14,10 +14,11 @@ namespace mini_installer { ...@@ -14,10 +14,11 @@ namespace mini_installer {
// A simple abstraction over a path to a file and a Windows file handle to it. // A simple abstraction over a path to a file and a Windows file handle to it.
class MiniFile { class MiniFile {
public: public:
MiniFile(); enum class DeleteOnClose : bool { kNo = false, kYes = true };
explicit MiniFile(DeleteOnClose delete_on_close);
// Closes the file if the instance holds a valid handle. The file will be // Closes the file if the instance holds a valid handle. The file will be
// deleted if directed by a call to DeleteOnClose(). // deleted if the instance was constructed with |delete_on_close|.
~MiniFile(); ~MiniFile();
MiniFile(const MiniFile&) = delete; MiniFile(const MiniFile&) = delete;
...@@ -35,17 +36,28 @@ class MiniFile { ...@@ -35,17 +36,28 @@ class MiniFile {
// Returns true if this object has a path and a handle to an open file. // Returns true if this object has a path and a handle to an open file.
bool IsValid() const; bool IsValid() const;
// Marks the file for deletion when the handle is closed via Close() or the // Drops write permission on the file handle so that other parties that
// instance's destructor. This state follows the handle when moved. // require no writers may open the file. In particular, the Windows loader
bool DeleteOnClose(); // opens files for execution with shared read/delete access, as do the
// extraction operations in Chrome's mini_installer.exe and setup.exe. These
// Closes the handle and clears the path. Following this, IsValid() will // would fail with sharing violations if mini_installer were to hold files
// return false. // open with write permissions. Returns false on failure, in which case the
// instance is no longer valid. The file will have been deleted if the
// instance was created with DeleteOnClose.
bool DropWritePermission();
// Closes the handle and clears the path. The file will be deleted if the
// instance was constructed with |delete_on_close|. Following this, IsValid()
// will return false.
void Close(); void Close();
// Returns a new handle to the file, or INVALID_HANDLE_VALUE on error. // Returns a new handle to the file, or INVALID_HANDLE_VALUE on error.
HANDLE DuplicateHandle() const; HANDLE DuplicateHandle() const;
// Opens the file for read access, disallowing writers (as if Create followed
// by DropWritePermission).
bool Open(const PathString& path);
// Returns the path to the open file, or a pointer to an empty string if // Returns the path to the open file, or a pointer to an empty string if
// IsValid() is false. // IsValid() is false.
const wchar_t* path() const { return path_.get(); } const wchar_t* path() const { return path_.get(); }
...@@ -61,6 +73,10 @@ class MiniFile { ...@@ -61,6 +73,10 @@ class MiniFile {
// A handle to the open file, or INVALID_HANDLE_VALUE. // A handle to the open file, or INVALID_HANDLE_VALUE.
HANDLE handle_ = INVALID_HANDLE_VALUE; HANDLE handle_ = INVALID_HANDLE_VALUE;
// Zero or FILE_FLAG_DELETE_ON_CLOSE, according to how the instance was
// constructed.
const DWORD delete_on_close_flag_;
}; };
} // namespace mini_installer } // namespace mini_installer
......
...@@ -8,16 +8,25 @@ ...@@ -8,16 +8,25 @@
#include "base/files/file_util.h" #include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h" #include "base/files/scoped_temp_dir.h"
#include "chrome/installer/mini_installer/path_string.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
namespace mini_installer { namespace mini_installer {
class MiniFileTest : public ::testing::Test { // A test harness for MiniFile. The MiniFile::DeleteOnClose parameter is passed
// to the test instance's constructor.
class MiniFileTest : public ::testing::TestWithParam<MiniFile::DeleteOnClose> {
protected: protected:
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
void TearDown() override { EXPECT_TRUE(temp_dir_.Delete()); } void TearDown() override { EXPECT_TRUE(temp_dir_.Delete()); }
static MiniFile::DeleteOnClose delete_on_close() { return GetParam(); }
static bool should_delete_on_close() {
return delete_on_close() != MiniFile::DeleteOnClose::kNo;
}
const base::FilePath& temp_dir() const { return temp_dir_.GetPath(); } const base::FilePath& temp_dir() const { return temp_dir_.GetPath(); }
private: private:
...@@ -25,68 +34,73 @@ class MiniFileTest : public ::testing::Test { ...@@ -25,68 +34,73 @@ class MiniFileTest : public ::testing::Test {
}; };
// Create should create a file. // Create should create a file.
TEST_F(MiniFileTest, Create) { TEST_P(MiniFileTest, Create) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM")); const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
MiniFile file; MiniFile file(delete_on_close());
ASSERT_TRUE(file.Create(file_path.value().c_str())); ASSERT_TRUE(file.Create(file_path.value().c_str()));
ASSERT_TRUE(base::PathExists(file_path)); EXPECT_TRUE(base::PathExists(file_path));
} }
// Created files should be deletable by others and should vanish when closed. // Created files should be deletable by others and should vanish when closed.
TEST_F(MiniFileTest, CreateDeleteIsShared) { TEST_P(MiniFileTest, CreateDeleteIsShared) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM")); const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
MiniFile file; MiniFile file(delete_on_close());
ASSERT_TRUE(file.Create(file_path.value().c_str())); ASSERT_TRUE(file.Create(file_path.value().c_str()));
// DeleteFile uses POSIX semantics, so the file appears to vanish immediately. // DeleteFile uses POSIX semantics, so the file appears to vanish immediately.
ASSERT_TRUE(base::DeleteFile(file_path)); ASSERT_TRUE(base::DeleteFile(file_path));
file.Close(); file.Close();
ASSERT_FALSE(base::PathExists(file_path)); EXPECT_FALSE(base::PathExists(file_path));
} }
// DeleteOnClose should work as advertised. // Tests that a file can be opened without shared write access after write
TEST_F(MiniFileTest, DeleteOnClose) { // permissions are dropped.
TEST_P(MiniFileTest, DropWritePermission) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM")); const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
MiniFile file; MiniFile file(delete_on_close());
ASSERT_TRUE(file.Create(file_path.value().c_str())); ASSERT_TRUE(file.Create(file_path.value().c_str()));
ASSERT_TRUE(file.DeleteOnClose()); ASSERT_FALSE(base::File(file_path, base::File::FLAG_OPEN |
base::File::FLAG_READ |
// The file can no longer be opened now that it has been marked for deletion. base::File::FLAG_EXCLUSIVE_WRITE |
// Attempts to do so will fail with ERROR_ACCESS_DENIED. Under the covers, the base::File::FLAG_SHARE_DELETE)
// NT status code is STATUS_DELETE_PENDING. Since base::PathExists will return .IsValid());
// false in this case, confirm the file's existence by trying to open it. ASSERT_TRUE(file.DropWritePermission());
base::File the_file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ | EXPECT_TRUE(base::File(file_path, base::File::FLAG_OPEN |
base::File::FLAG_SHARE_DELETE); base::File::FLAG_READ |
ASSERT_FALSE(the_file.IsValid()); base::File::FLAG_EXCLUSIVE_WRITE |
ASSERT_EQ(the_file.error_details(), base::File::FILE_ERROR_ACCESS_DENIED); base::File::FLAG_SHARE_DELETE)
.IsValid());
file.Close();
ASSERT_FALSE(base::PathExists(file_path));
} }
// Close should really close. // Close should really close.
TEST_F(MiniFileTest, Close) { TEST_P(MiniFileTest, CreateThenClose) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM")); const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
MiniFile file; MiniFile file(delete_on_close());
ASSERT_TRUE(file.Create(file_path.value().c_str())); ASSERT_TRUE(file.Create(file_path.value().c_str()));
file.Close(); file.Close();
EXPECT_FALSE(file.IsValid()); EXPECT_FALSE(file.IsValid());
EXPECT_EQ(*file.path(), 0); EXPECT_EQ(*file.path(), 0);
ASSERT_TRUE(base::PathExists(file_path));
base::File f(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ | ASSERT_NE(base::PathExists(file_path), should_delete_on_close());
base::File::FLAG_EXCLUSIVE_READ | if (!should_delete_on_close()) {
base::File::FLAG_EXCLUSIVE_WRITE); // If closing should not have deleted the file, it should now be possible to
ASSERT_TRUE(f.IsValid()); // open it with exclusive access.
EXPECT_TRUE(base::File(file_path, base::File::FLAG_OPEN |
base::File::FLAG_READ |
base::File::FLAG_EXCLUSIVE_READ |
base::File::FLAG_EXCLUSIVE_WRITE)
.IsValid());
}
} }
// DuplicateHandle should work as advertized. // DuplicateHandle should work as advertized.
TEST_F(MiniFileTest, DuplicateHandle) { TEST_P(MiniFileTest, DuplicateHandle) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM")); const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
MiniFile file; MiniFile file(delete_on_close());
ASSERT_TRUE(file.Create(file_path.value().c_str())); ASSERT_TRUE(file.Create(file_path.value().c_str()));
HANDLE dup = file.DuplicateHandle(); HANDLE dup = file.DuplicateHandle();
ASSERT_NE(dup, INVALID_HANDLE_VALUE); ASSERT_NE(dup, INVALID_HANDLE_VALUE);
...@@ -104,10 +118,55 @@ TEST_F(MiniFileTest, DuplicateHandle) { ...@@ -104,10 +118,55 @@ TEST_F(MiniFileTest, DuplicateHandle) {
::CloseHandle(std::exchange(dup, INVALID_HANDLE_VALUE)); ::CloseHandle(std::exchange(dup, INVALID_HANDLE_VALUE));
} }
TEST_F(MiniFileTest, Path) { // Open should provide access to a file just like Create, but for a file that
// already exists.
TEST_P(MiniFileTest, Open) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
ASSERT_TRUE(
base::File(file_path, base::File::FLAG_CREATE | base::File::FLAG_WRITE)
.IsValid());
ASSERT_TRUE(base::PathExists(file_path));
PathString path_string;
ASSERT_TRUE(path_string.assign(file_path.value().c_str()));
MiniFile file(delete_on_close());
ASSERT_TRUE(file.Open(path_string));
}
// Close should really close.
TEST_P(MiniFileTest, OpenThenClose) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM")); const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
MiniFile file; ASSERT_TRUE(
base::File(file_path, base::File::FLAG_CREATE | base::File::FLAG_WRITE)
.IsValid());
ASSERT_TRUE(base::PathExists(file_path));
MiniFile file(delete_on_close());
PathString path_string;
ASSERT_TRUE(path_string.assign(file_path.value().c_str()));
ASSERT_TRUE(file.Open(path_string));
file.Close();
EXPECT_FALSE(file.IsValid());
EXPECT_EQ(*file.path(), 0);
ASSERT_NE(base::PathExists(file_path), should_delete_on_close());
if (!should_delete_on_close()) {
// If closing should not have deleted the file, it should now be possible to
// open it with exclusive access.
EXPECT_TRUE(base::File(file_path, base::File::FLAG_OPEN |
base::File::FLAG_READ |
base::File::FLAG_EXCLUSIVE_READ |
base::File::FLAG_EXCLUSIVE_WRITE)
.IsValid());
}
}
TEST_P(MiniFileTest, Path) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
MiniFile file(delete_on_close());
EXPECT_EQ(*file.path(), 0); EXPECT_EQ(*file.path(), 0);
ASSERT_TRUE(file.Create(file_path.value().c_str())); ASSERT_TRUE(file.Create(file_path.value().c_str()));
...@@ -117,10 +176,10 @@ TEST_F(MiniFileTest, Path) { ...@@ -117,10 +176,10 @@ TEST_F(MiniFileTest, Path) {
EXPECT_EQ(*file.path(), 0); EXPECT_EQ(*file.path(), 0);
} }
TEST_F(MiniFileTest, GetHandleUnsafe) { TEST_P(MiniFileTest, GetHandleUnsafe) {
const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM")); const base::FilePath file_path = temp_dir().Append(FILE_PATH_LITERAL("HUM"));
MiniFile file; MiniFile file(delete_on_close());
EXPECT_EQ(file.GetHandleUnsafe(), INVALID_HANDLE_VALUE); EXPECT_EQ(file.GetHandleUnsafe(), INVALID_HANDLE_VALUE);
ASSERT_TRUE(file.Create(file_path.value().c_str())); ASSERT_TRUE(file.Create(file_path.value().c_str()));
...@@ -130,4 +189,11 @@ TEST_F(MiniFileTest, GetHandleUnsafe) { ...@@ -130,4 +189,11 @@ TEST_F(MiniFileTest, GetHandleUnsafe) {
EXPECT_EQ(file.GetHandleUnsafe(), INVALID_HANDLE_VALUE); EXPECT_EQ(file.GetHandleUnsafe(), INVALID_HANDLE_VALUE);
} }
INSTANTIATE_TEST_SUITE_P(DoNotDeleteOnClose,
MiniFileTest,
::testing::Values(MiniFile::DeleteOnClose::kNo));
INSTANTIATE_TEST_SUITE_P(DeleteOnClose,
MiniFileTest,
::testing::Values(MiniFile::DeleteOnClose::kYes));
} // namespace mini_installer } // namespace mini_installer
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
#include "chrome/installer/mini_installer/mini_file.h" #include "chrome/installer/mini_installer/mini_file.h"
namespace mini_installer {
PEResource::PEResource(HRSRC resource, HMODULE module) PEResource::PEResource(HRSRC resource, HMODULE module)
: resource_(resource), module_(module) {} : resource_(resource), module_(module) {}
...@@ -24,42 +26,43 @@ size_t PEResource::Size() { ...@@ -24,42 +26,43 @@ size_t PEResource::Size() {
return ::SizeofResource(module_, resource_); return ::SizeofResource(module_, resource_);
} }
bool PEResource::WriteToDisk(const wchar_t* full_path) { bool PEResource::WriteToDisk(const wchar_t* full_path, MiniFile& file) {
// Resource handles are not real HGLOBALs so do not attempt to close them. // Resource handles are not real HGLOBALs so do not attempt to close them.
// Resources are freed when the containing module is unloaded. // Resources are freed when the containing module is unloaded.
HGLOBAL data_handle = ::LoadResource(module_, resource_); HGLOBAL data_handle = ::LoadResource(module_, resource_);
if (nullptr == data_handle) if (!data_handle)
return false; return false;
const char* data = reinterpret_cast<const char*>(::LockResource(data_handle)); const char* data = reinterpret_cast<const char*>(::LockResource(data_handle));
if (nullptr == data) if (!data)
return false; return false;
mini_installer::MiniFile file;
if (!file.Create(full_path)) if (!file.Create(full_path))
return false; return false;
// Don't write all of the data at once because this can lead to kernel // Don't write all of the data at once because this can lead to kernel
// address-space exhaustion on 32-bit Windows (see https://crbug.com/1001022 // address-space exhaustion on 32-bit Windows (see https://crbug.com/1001022
// for details). // for details).
constexpr size_t kMaxWriteAmount = 8 * 1024 * 1024; constexpr DWORD kMaxWriteAmount = 8 * 1024 * 1024;
const size_t resource_size = Size(); const DWORD resource_size = Size();
for (size_t total_written = 0; total_written < resource_size; /**/) { for (DWORD total_written = 0; total_written < resource_size; /**/) {
const size_t write_amount = const DWORD write_amount =
std::min(kMaxWriteAmount, resource_size - total_written); std::min(kMaxWriteAmount, resource_size - total_written);
DWORD written = 0; DWORD written = 0;
if (!::WriteFile(file.GetHandleUnsafe(), data + total_written, if (!::WriteFile(file.GetHandleUnsafe(), data + total_written, write_amount,
static_cast<DWORD>(write_amount), &written, nullptr)) { &written, nullptr)) {
const auto write_error = ::GetLastError(); const auto write_error = ::GetLastError();
// Delete the file since the write failed. // Delete the file since the write failed.
file.DeleteOnClose();
file.Close(); file.Close();
::SetLastError(write_error); ::SetLastError(write_error);
return false; return false;
} }
total_written += write_amount; total_written += written;
} }
return true; return true;
} }
} // namespace mini_installer
...@@ -9,6 +9,10 @@ ...@@ -9,6 +9,10 @@
#include <stddef.h> #include <stddef.h>
namespace mini_installer {
class MiniFile;
// This class models a windows PE resource. It does not pretend to be a full // This class models a windows PE resource. It does not pretend to be a full
// API wrapper and it is just concerned with loading it to memory and writing // API wrapper and it is just concerned with loading it to memory and writing
// it to disk. Each resource is unique only in the context of a loaded module, // it to disk. Each resource is unique only in the context of a loaded module,
...@@ -30,14 +34,17 @@ class PEResource { ...@@ -30,14 +34,17 @@ class PEResource {
// not valid. // not valid.
size_t Size(); size_t Size();
// Creates a file in 'path' with a copy of the resource. If the resource can // Writes the resource to the file |path|. Returns true on success, in which
// not be loaded into memory or if it cannot be written to disk it returns // case |file| holds an open handle to the destination file. |file| will be
// false. // opened with exclusive write access and shared read and delete access, and
bool WriteToDisk(const wchar_t* path); // will be marked as delete-on-close.
bool WriteToDisk(const wchar_t* path, MiniFile& file);
private: private:
HRSRC resource_; HRSRC resource_;
HMODULE module_; HMODULE module_;
}; };
} // namespace mini_installer
#endif // CHROME_INSTALLER_MINI_INSTALLER_PE_RESOURCE_H_ #endif // CHROME_INSTALLER_MINI_INSTALLER_PE_RESOURCE_H_
// Copyright 2020 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/installer/mini_installer/pe_resource.h"
#include <windows.h>
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "chrome/installer/mini_installer/mini_file.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace mini_installer {
TEST(PEResourceTest, WriteToDisk) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
const base::FilePath manifest_path =
temp_dir.GetPath().Append(FILE_PATH_LITERAL("manifest"));
PEResource manifest(MAKEINTRESOURCE(1), RT_MANIFEST,
::GetModuleHandle(nullptr));
ASSERT_TRUE(manifest.IsValid());
MiniFile manifest_file(MiniFile::DeleteOnClose::kYes);
ASSERT_TRUE(
manifest.WriteToDisk(manifest_path.value().c_str(), manifest_file));
ASSERT_TRUE(manifest_file.IsValid());
EXPECT_PRED1(base::PathExists, manifest_path);
manifest_file.Close();
EXPECT_FALSE(base::PathExists(manifest_path));
EXPECT_TRUE(temp_dir.Delete());
}
} // namespace mini_installer
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment