/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* This source code was derived from Chromium code, and as such is also subject
 * to the [Chromium license](ipc/chromium/src/LICENSE). */

#include "SharedMemoryPlatform.h"

#include <windows.h>

#include "nsDebug.h"
#ifdef MOZ_MEMORY
#  include "mozmemory_stall.h"
#endif

namespace {
// NtQuerySection is an internal (but believed to be stable) API and the
// structures it uses are defined in nt_internals.h.
// So we have to define them ourselves.
typedef enum _SECTION_INFORMATION_CLASS {
  SectionBasicInformation,
} SECTION_INFORMATION_CLASS;

typedef struct _SECTION_BASIC_INFORMATION {
  PVOID BaseAddress;
  ULONG Attributes;
  LARGE_INTEGER Size;
} SECTION_BASIC_INFORMATION, *PSECTION_BASIC_INFORMATION;

typedef ULONG(__stdcall* NtQuerySectionType)(
    HANDLE SectionHandle, SECTION_INFORMATION_CLASS SectionInformationClass,
    PVOID SectionInformation, ULONG SectionInformationLength,
    PULONG ResultLength);

// Checks if the section object is safe to map. At the moment this just means
// it's not an image section.
bool IsSectionSafeToMap(HANDLE aHandle) {
  static NtQuerySectionType nt_query_section_func =
      reinterpret_cast<NtQuerySectionType>(
          ::GetProcAddress(::GetModuleHandle(L"ntdll.dll"), "NtQuerySection"));
  MOZ_DIAGNOSTIC_ASSERT(nt_query_section_func,
                        "NtQuerySection function not found in ntdll.dll");

  // The handle must have SECTION_QUERY access for this to succeed.
  SECTION_BASIC_INFORMATION basic_information = {};
  ULONG status = nt_query_section_func(aHandle, SectionBasicInformation,
                                       &basic_information,
                                       sizeof(basic_information), nullptr);
  if (status) {
    return false;
  }

  return (basic_information.Attributes & SEC_IMAGE) != SEC_IMAGE;
}

// Wrapper around CreateFileMappingW for pagefile-backed regions. When out of
// memory, may attempt to stall and retry rather than returning immediately, in
// hopes that the page file is about to be expanded by Windows. (bug 1822383,
// bug 1716727)
//
// This method is largely a copy of the MozVirtualAlloc method from
// mozjemalloc.cpp, which implements this strategy for VirtualAlloc calls,
// except re-purposed to handle CreateFileMapping.
HANDLE MozCreateFileMappingW(LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
                             DWORD flProtect, DWORD dwMaximumSizeHigh,
                             DWORD dwMaximumSizeLow, LPCWSTR lpName) {
#ifdef MOZ_MEMORY
  constexpr auto IsOOMError = [] {
    return ::GetLastError() == ERROR_COMMITMENT_LIMIT;
  };

  {
    HANDLE handle = ::CreateFileMappingW(
        INVALID_HANDLE_VALUE, lpFileMappingAttributes, flProtect,
        dwMaximumSizeHigh, dwMaximumSizeLow, lpName);
    if (MOZ_LIKELY(handle)) {
      MOZ_DIAGNOSTIC_ASSERT(handle != INVALID_HANDLE_VALUE,
                            "::CreateFileMapping should return NULL, not "
                            "INVALID_HANDLE_VALUE, on failure");
      return handle;
    }

    // We can't do anything for errors other than OOM.
    if (!IsOOMError()) {
      return nullptr;
    }
  }

  // Retry as many times as desired (possibly zero).
  const mozilla::StallSpecs stallSpecs = mozilla::GetAllocatorStallSpecs();

  const auto ret =
      stallSpecs.StallAndRetry(&::Sleep, [&]() -> std::optional<HANDLE> {
        HANDLE handle = ::CreateFileMappingW(
            INVALID_HANDLE_VALUE, lpFileMappingAttributes, flProtect,
            dwMaximumSizeHigh, dwMaximumSizeLow, lpName);

        if (handle) {
          MOZ_DIAGNOSTIC_ASSERT(handle != INVALID_HANDLE_VALUE,
                                "::CreateFileMapping should return NULL, not "
                                "INVALID_HANDLE_VALUE, on failure");
          return handle;
        }

        // Failure for some reason other than OOM.
        if (!IsOOMError()) {
          return nullptr;
        }

        return std::nullopt;
      });

  return ret.value_or(nullptr);
#else
  return ::CreateFileMappingW(INVALID_HANDLE_VALUE, lpFileMappingAttributes,
                              flProtect, dwMaximumSizeHigh, dwMaximumSizeLow,
                              lpName);
#endif
}

}  // namespace

namespace mozilla::ipc::shared_memory {

static Maybe<PlatformHandle> CreateImpl(size_t aSize, bool aFreezable) {
  // If the shared memory object has no DACL, any process can
  // duplicate its handles with any access rights; e.g., re-add write
  // access to a read-only handle.  To prevent that, we give it an
  // empty DACL, so that no process can do that.
  SECURITY_ATTRIBUTES sa, *psa = nullptr;
  SECURITY_DESCRIPTOR sd;
  ACL dacl;

  if (aFreezable) {
    psa = &sa;
    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = &sd;
    sa.bInheritHandle = FALSE;

    if (NS_WARN_IF(!::InitializeAcl(&dacl, sizeof(dacl), ACL_REVISION)) ||
        NS_WARN_IF(!::InitializeSecurityDescriptor(
            &sd, SECURITY_DESCRIPTOR_REVISION)) ||
        NS_WARN_IF(!::SetSecurityDescriptorDacl(&sd, TRUE, &dacl, FALSE))) {
      return Nothing();
    }
  }

  auto handle = MozCreateFileMappingW(psa, PAGE_READWRITE, 0,
                                      static_cast<DWORD>(aSize), nullptr);
  if (!handle) {
    return Nothing();
  } else {
    return Some(handle);
  }
}

bool Platform::Create(MutableHandle& aHandle, size_t aSize) {
  if (auto ph = CreateImpl(aSize, false)) {
    aHandle.mHandle = std::move(*ph);
    aHandle.SetSize(aSize);
    return true;
  }
  return false;
}

bool Platform::CreateFreezable(FreezableHandle& aHandle, size_t aSize) {
  if (auto ph = CreateImpl(aSize, true)) {
    aHandle.mHandle = std::move(*ph);
    aHandle.SetSize(aSize);
    return true;
  }
  return false;
}

PlatformHandle Platform::CloneHandle(const PlatformHandle& aHandle) {
  auto rv = DuplicateFileHandle(aHandle);
  if (!rv) {
    NS_WARNING("DuplicateHandle Failed!");
  }
  return rv;
}

bool Platform::Freeze(FreezableHandle& aHandle) {
  HANDLE ro_handle;
  if (!::DuplicateHandle(::GetCurrentProcess(), aHandle.mHandle.get(),
                         ::GetCurrentProcess(), &ro_handle,
                         GENERIC_READ | FILE_MAP_READ, false, 0)) {
    return false;
  }

  aHandle.mHandle.reset(ro_handle);
  return true;
}

Maybe<void*> Platform::Map(const HandleBase& aHandle, uint64_t aOffset,
                           size_t aSize, void* aFixedAddress, bool aReadOnly) {
  DWORD fileOffsetHigh = (aOffset >> 32) & 0xffffffff;
  DWORD fileOffsetLow = aOffset & 0xffffffff;
  void* mem = ::MapViewOfFileEx(
      aHandle.mHandle.get(),
      aReadOnly ? FILE_MAP_READ : FILE_MAP_READ | FILE_MAP_WRITE,
      fileOffsetHigh, fileOffsetLow, aSize, aFixedAddress);
  if (mem) {
    MOZ_ASSERT(!aFixedAddress || mem == aFixedAddress,
               "MapViewOfFileEx returned an expected address");
    return Some(mem);
  }
  return Nothing();
}

void Platform::Unmap(void* aMemory, size_t aSize) {
  ::UnmapViewOfFile(aMemory);
}

bool Platform::Protect(char* aAddr, size_t aSize, Access aAccess) {
  DWORD flags;
  if ((aAccess & AccessReadWrite) == AccessReadWrite)
    flags = PAGE_READWRITE;
  else if (aAccess & AccessRead)
    flags = PAGE_READONLY;
  else
    flags = PAGE_NOACCESS;

  DWORD oldflags;
  return ::VirtualProtect(aAddr, aSize, flags, &oldflags);
}

void* Platform::FindFreeAddressSpace(size_t aSize) {
  void* memory = ::VirtualAlloc(NULL, aSize, MEM_RESERVE, PAGE_NOACCESS);
  if (memory) {
    ::VirtualFree(memory, 0, MEM_RELEASE);
  }
  return memory;
}

size_t Platform::PageSize() {
  SYSTEM_INFO si;
  ::GetSystemInfo(&si);
  return si.dwPageSize;
}

size_t Platform::AllocationGranularity() {
  SYSTEM_INFO si;
  ::GetSystemInfo(&si);
  return si.dwAllocationGranularity;
}

bool Platform::IsSafeToMap(const PlatformHandle& aHandle) {
  return IsSectionSafeToMap(aHandle.get());
}

}  // namespace mozilla::ipc::shared_memory
