chromium/chrome/installer/util/lzma_util.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/installer/util/lzma_util.h"

#include <windows.h>

#include <ntstatus.h>
#include <stddef.h>

#include <set>

#include "base/files/file_util.h"
#include "base/files/memory_mapped_file.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "third_party/lzma_sdk/google/seven_zip_reader.h"

namespace {

class SevenZipDelegateImpl : public seven_zip::Delegate {
 public:
  SevenZipDelegateImpl(const base::FilePath& location,
                       base::FilePath* output_file);

  SevenZipDelegateImpl(const SevenZipDelegateImpl&) = delete;
  SevenZipDelegateImpl& operator=(const SevenZipDelegateImpl&) = delete;

  std::optional<DWORD> error_code() const { return error_code_; }
  UnPackStatus unpack_error() const { return unpack_error_; }

  // seven_zip::Delegate implementation:
  void OnOpenError(seven_zip::Result result) override;
  base::File OnTempFileRequest() override;
  bool OnEntry(const seven_zip::EntryInfo& entry,
               base::span<uint8_t>& temp_file) override;
  bool OnDirectory(const seven_zip::EntryInfo& entry) override;
  bool EntryDone(seven_zip::Result result,
                 const seven_zip::EntryInfo& entry) override;

 private:
  bool CreateDirectory(const base::FilePath& dir);

  const base::FilePath location_;
  const raw_ptr<base::FilePath> output_file_;

  std::set<base::FilePath> directories_created_;
  std::optional<DWORD> error_code_;
  base::File current_file_;
  std::optional<base::MemoryMappedFile> mapped_file_;
  UnPackStatus unpack_error_ = UNPACK_NO_ERROR;
};

SevenZipDelegateImpl::SevenZipDelegateImpl(const base::FilePath& location,
                                           base::FilePath* output_file)
    : location_(location), output_file_(output_file) {}

bool SevenZipDelegateImpl::CreateDirectory(const base::FilePath& dir) {
  bool result = true;
  if (directories_created_.find(dir) == directories_created_.end()) {
    result = base::CreateDirectory(dir);
    if (result)
      directories_created_.insert(dir);
  }
  return result;
}

void SevenZipDelegateImpl::OnOpenError(seven_zip::Result result) {
  switch (result) {
    case seven_zip::Result::kMalformedArchive:
    case seven_zip::Result::kBadCrc:
    case seven_zip::Result::kUnsupported: {
      auto error_code = ::GetLastError();
      if (error_code != ERROR_SUCCESS)
        error_code_ = error_code;
      unpack_error_ = UNPACK_SZAREX_OPEN_ERROR;
      break;
    }
    case seven_zip::Result::kFailedToAllocate:
      unpack_error_ = UNPACK_ALLOCATE_ERROR;
      break;
    case seven_zip::Result::kIoError:
      unpack_error_ = UNPACK_IO_DEVICE_ERROR;
      break;
    case seven_zip::Result::kUnknownError:
    case seven_zip::Result::kDiskFull:
    case seven_zip::Result::kSuccess:
    case seven_zip::Result::kMemoryMappingFailed:
    case seven_zip::Result::kNoFilename:
    case seven_zip::Result::kEncryptedHeaders:
      NOTREACHED_IN_MIGRATION();
      return;
  }
}

base::File SevenZipDelegateImpl::OnTempFileRequest() {
  base::FilePath temp_path;
  base::File temp_file;
  if (base::CreateTemporaryFileInDir(location_, &temp_path)) {
    temp_file.Initialize(
        temp_path,
        base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_READ |
            base::File::FLAG_WRITE | base::File::FLAG_WIN_EXCLUSIVE_READ |
            base::File::FLAG_WIN_EXCLUSIVE_WRITE |
            base::File::FLAG_WIN_TEMPORARY | base::File::FLAG_DELETE_ON_CLOSE |
            base::File::FLAG_WIN_SHARE_DELETE);
  }

  if (!temp_file.IsValid()) {
    error_code_ = ::GetLastError();
    unpack_error_ = UNPACK_CREATE_FILE_ERROR;
  }

  return temp_file;
}

bool SevenZipDelegateImpl::OnDirectory(const seven_zip::EntryInfo& entry) {
  if (!CreateDirectory(location_.Append(entry.file_path))) {
    error_code_ = ::GetLastError();
    unpack_error_ = UNPACK_CREATE_FILE_ERROR;
    return false;
  }

  return true;
}

bool SevenZipDelegateImpl::OnEntry(const seven_zip::EntryInfo& entry,
                                   base::span<uint8_t>& output) {
  if (entry.file_path.ReferencesParent()) {
    PLOG(ERROR) << "Path contains a parent directory traversal which is not "
                   "allowed because it could become a security issue: "
                << entry.file_path;
    unpack_error_ = UNPACK_CREATE_FILE_ERROR;
    return false;
  }

  base::FilePath file_path = location_.Append(entry.file_path);
  if (output_file_)
    *output_file_ = file_path;

  CreateDirectory(file_path.DirName());

  current_file_ =
      base::File(file_path, base::File::FLAG_CREATE_ALWAYS |
                                base::File::FLAG_READ | base::File::FLAG_WRITE |
                                base::File::FLAG_WIN_EXCLUSIVE_READ |
                                base::File::FLAG_WIN_EXCLUSIVE_WRITE |
                                base::File::FLAG_CAN_DELETE_ON_CLOSE |
                                base::File::FLAG_WIN_SHARE_DELETE);
  if (!current_file_.IsValid()) {
    PLOG(ERROR) << "Invalid file";
    error_code_ = ::GetLastError();
    unpack_error_ = UNPACK_CREATE_FILE_ERROR;
    return false;
  }

  // The target file is deleted by default unless extracting succeeds.
  current_file_.DeleteOnClose(true);

  if (entry.file_size > 0) {
    mapped_file_.emplace();
    bool mapped_file_ok = mapped_file_->Initialize(
        current_file_.Duplicate(), {0, static_cast<size_t>(entry.file_size)},
        base::MemoryMappedFile::READ_WRITE_EXTEND);
    if (!mapped_file_ok) {
      PLOG(ERROR) << "Can't map file to memory";
      error_code_ = ::GetLastError();
      unpack_error_ = UNPACK_ALLOCATE_ERROR;
      return false;
    }

    output = base::span<uint8_t>(mapped_file_->data(), mapped_file_->length());
  } else {
    output = base::span<uint8_t>();
  }

  // Clear the last error code before the entry is extracted to reduce the
  // likelihood that it will hold an unrelated error code in case extraction
  // fails.
  ::SetLastError(ERROR_SUCCESS);

  return true;
}

bool SevenZipDelegateImpl::EntryDone(seven_zip::Result result,
                                     const seven_zip::EntryInfo& entry) {
  // Take ownership of `current_file_` so that it is always closed when this
  // function exits.
  base::File current_file = std::move(current_file_);

  if (result != seven_zip::Result::kSuccess) {
    auto error_code = ::GetLastError();
    if (error_code != ERROR_SUCCESS)
      error_code_ = error_code;

    switch (result) {
      case seven_zip::Result::kSuccess:
        NOTREACHED_IN_MIGRATION();
        break;
      case seven_zip::Result::kFailedToAllocate:
        unpack_error_ = UNPACK_ALLOCATE_ERROR;
        break;
      case seven_zip::Result::kIoError:
        unpack_error_ = UNPACK_IO_DEVICE_ERROR;
        break;
      case seven_zip::Result::kDiskFull:
        unpack_error_ = UNPACK_DISK_FULL;
        break;
      case seven_zip::Result::kNoFilename:
        LOG(ERROR) << "Couldn't get file name";
        unpack_error_ = UNPACK_NO_FILENAME_ERROR;
        break;
      case seven_zip::Result::kUnknownError:
      case seven_zip::Result::kBadCrc:
      case seven_zip::Result::kMemoryMappingFailed:
      case seven_zip::Result::kMalformedArchive:
      case seven_zip::Result::kUnsupported:
      case seven_zip::Result::kEncryptedHeaders:
        unpack_error_ = UNPACK_EXTRACT_ERROR;
        break;
    }

    mapped_file_.reset();
    return false;
  }

  if (mapped_file_) {
    // Modified pages are not written to disk until they're evicted from the
    // working set. Explicitly kick off the write to disk now
    // (asynchronously) to improve the odds that the file's contents are
    // on-disk when another process (such as chrome.exe) would like to use
    // them.
    ::FlushViewOfFile(mapped_file_->data(), 0);
    // Unmap the target file from the process's address space.
    mapped_file_.reset();
    // Flush to avoid odd behavior, such as the bug in Windows 7 through
    // Windows 10 1809 for PE files described in
    // https://randomascii.wordpress.com/2018/02/25/compiler-bug-linker-bug-windows-kernel-bug/.
    // We've also observed oddly empty files on other Windows versions, so
    // this is unconditional.
    current_file.Flush();
  }

  // On success, `current_file` is kept.
  current_file.DeleteOnClose(false);

  if (!entry.last_modified_time.is_null()) {
    FILETIME filetime = entry.last_modified_time.ToFileTime();
    // Make a best-effort attempt to set the file time.
    SetFileTime(current_file.GetPlatformFile(), nullptr, nullptr, &filetime);
  }

  return true;
}

}  // namespace

UnPackStatus UnPackArchive(const base::FilePath& archive,
                           const base::FilePath& output_dir,
                           base::FilePath* output_file) {
  VLOG(1) << "Opening archive " << archive.value();
  LzmaUtilImpl lzma_util;
  UnPackStatus status;
  if ((status = lzma_util.OpenArchive(archive)) != UNPACK_NO_ERROR) {
    PLOG(ERROR) << "Unable to open install archive: " << archive.value();
  } else {
    VLOG(1) << "Uncompressing archive to path " << output_dir.value();
    if ((status = lzma_util.UnPack(output_dir, output_file)) != UNPACK_NO_ERROR)
      PLOG(ERROR) << "Unable to uncompress archive: " << archive.value();
  }

  if (status != UNPACK_NO_ERROR) {
    std::optional<DWORD> error_code = lzma_util.GetErrorCode();
    if (error_code.value_or(ERROR_SUCCESS) == ERROR_DISK_FULL)
      return UNPACK_DISK_FULL;
    if (error_code.value_or(ERROR_SUCCESS) == ERROR_IO_DEVICE)
      return UNPACK_IO_DEVICE_ERROR;
  }

  return status;
}

LzmaUtilImpl::LzmaUtilImpl() = default;
LzmaUtilImpl::~LzmaUtilImpl() = default;

UnPackStatus LzmaUtilImpl::OpenArchive(const base::FilePath& archivePath) {
  // Make sure file is not already open.
  CloseArchive();

  archive_file_.Initialize(archivePath,
                           base::File::FLAG_OPEN | base::File::FLAG_READ |
                               base::File::FLAG_WIN_EXCLUSIVE_WRITE |
                               base::File::FLAG_WIN_SHARE_DELETE);
  if (archive_file_.IsValid())
    return UNPACK_NO_ERROR;
  error_code_ = ::GetLastError();
  return archive_file_.error_details() == base::File::FILE_ERROR_NOT_FOUND
             ? UNPACK_ARCHIVE_NOT_FOUND
             : UNPACK_ARCHIVE_CANNOT_OPEN;
}

UnPackStatus LzmaUtilImpl::UnPack(const base::FilePath& location) {
  return UnPack(location, nullptr);
}

UnPackStatus LzmaUtilImpl::UnPack(const base::FilePath& location,
                                  base::FilePath* output_file) {
  DCHECK(archive_file_.IsValid());

  SevenZipDelegateImpl delegate(location, output_file);
  std::unique_ptr<seven_zip::SevenZipReader> reader =
      seven_zip::SevenZipReader::Create(archive_file_.Duplicate(), delegate);
  if (reader) {
    reader->Extract();
  }
  error_code_ = delegate.error_code();
  return delegate.unpack_error();
}

void LzmaUtilImpl::CloseArchive() {
  archive_file_.Close();
  error_code_ = std::nullopt;
}