/*
 *      The PCI Library -- Physical memory mapping for Windows systems
 *
 *      Copyright (c) 2023 Pali Rohár <pali@kernel.org>
 *
 *      Can be freely distributed and used under the terms of the GNU GPL v2+
 *
 *      SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "internal.h"

#include <windows.h>
#include <errno.h>
#include <stdlib.h>

#include "physmem.h"
#include "win32-helpers.h"

#ifndef NTSTATUS
#define NTSTATUS LONG
#endif
#ifndef STATUS_INVALID_HANDLE
#define STATUS_INVALID_HANDLE ((NTSTATUS)0xC0000008)
#endif
#ifndef STATUS_INVALID_PARAMETER
#define STATUS_INVALID_PARAMETER ((NTSTATUS)0xC000000D)
#endif
#ifndef STATUS_CONFLICTING_ADDRESSES
#define STATUS_CONFLICTING_ADDRESSES ((NTSTATUS)0xC0000018)
#endif
#ifndef STATUS_NOT_MAPPED_VIEW
#define STATUS_NOT_MAPPED_VIEW ((NTSTATUS)0xC0000019)
#endif
#ifndef STATUS_INVALID_VIEW_SIZE
#define STATUS_INVALID_VIEW_SIZE ((NTSTATUS)0xC000001F)
#endif
#ifndef STATUS_ACCESS_DENIED
#define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022)
#endif
#ifndef STATUS_OBJECT_NAME_NOT_FOUND
#define STATUS_OBJECT_NAME_NOT_FOUND ((NTSTATUS)0xC0000034)
#endif
#ifndef STATUS_INVALID_PAGE_PROTECTION
#define STATUS_INVALID_PAGE_PROTECTION ((NTSTATUS)0xC0000045)
#endif
#ifndef STATUS_SECTION_PROTECTION
#define STATUS_SECTION_PROTECTION ((NTSTATUS)0xC000004E)
#endif
#ifndef STATUS_INSUFFICIENT_RESOURCES
#define STATUS_INSUFFICIENT_RESOURCES ((NTSTATUS)0xC000009A)
#endif
#ifndef STATUS_INVALID_PARAMETER_3
#define STATUS_INVALID_PARAMETER_3 ((NTSTATUS)0xC00000F1)
#endif
#ifndef STATUS_INVALID_PARAMETER_4
#define STATUS_INVALID_PARAMETER_4 ((NTSTATUS)0xC00000F2)
#endif
#ifndef STATUS_INVALID_PARAMETER_5
#define STATUS_INVALID_PARAMETER_5 ((NTSTATUS)0xC00000F3)
#endif
#ifndef STATUS_INVALID_PARAMETER_8
#define STATUS_INVALID_PARAMETER_8 ((NTSTATUS)0xC00000F6)
#endif
#ifndef STATUS_INVALID_PARAMETER_9
#define STATUS_INVALID_PARAMETER_9 ((NTSTATUS)0xC00000F7)
#endif
#ifndef STATUS_MAPPED_ALIGNMENT
#define STATUS_MAPPED_ALIGNMENT ((NTSTATUS)0xC0000220)
#endif

#ifndef OBJ_CASE_INSENSITIVE
#define OBJ_CASE_INSENSITIVE 0x00000040L
#endif

#ifndef SECTION_INHERIT
#define SECTION_INHERIT ULONG
#endif
#ifndef ViewUnmap
#define ViewUnmap ((SECTION_INHERIT)2)
#endif

#ifndef IMAGE_NT_OPTIONAL_HDR_MAGIC
#ifdef _WIN64
#define IMAGE_NT_OPTIONAL_HDR_MAGIC 0x20b
#else
#define IMAGE_NT_OPTIONAL_HDR_MAGIC 0x10b
#endif
#endif

#ifndef EOVERFLOW
#define EOVERFLOW 132
#endif

#if _WIN32_WINNT < 0x0500
typedef ULONG ULONG_PTR;
typedef ULONG_PTR SIZE_T, *PSIZE_T;
#endif

#ifndef __UNICODE_STRING_DEFINED
#define __UNICODE_STRING_DEFINED
typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
#endif

#ifndef __OBJECT_ATTRIBUTES_DEFINED
#define __OBJECT_ATTRIBUTES_DEFINED
typedef struct _OBJECT_ATTRIBUTES {
  ULONG Length;
  HANDLE RootDirectory;
  PUNICODE_STRING ObjectName;
  ULONG Attributes;
  PVOID SecurityDescriptor;
  PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
#endif

#ifndef InitializeObjectAttributes
#define InitializeObjectAttributes(p, n, a, r, s) \
{ \
  (p)->Length = sizeof(OBJECT_ATTRIBUTES); \
  (p)->RootDirectory = (r); \
  (p)->Attributes = (a); \
  (p)->ObjectName = (n); \
  (p)->SecurityDescriptor = (s); \
  (p)->SecurityQualityOfService = NULL; \
}
#endif

#ifndef RtlInitUnicodeString
#define RtlInitUnicodeString(d, s) \
{ \
  (d)->Length = wcslen(s) * sizeof(WCHAR); \
  (d)->MaximumLength = (d)->Length + sizeof(WCHAR); \
  (d)->Buffer = (PWCHAR)(s); \
}
#endif

#define VWIN32_DEVICE_ID 0x002A /* from vmm.h */
#define WIN32_SERVICE_ID(device, function) (((device) << 16) | (function))
#define VWIN32_Int31Dispatch WIN32_SERVICE_ID(VWIN32_DEVICE_ID, 0x29)
#define DPMI_PHYSICAL_ADDRESS_MAPPING 0x0800

struct physmem {
  HANDLE section_handle;
  NTSTATUS (NTAPI *NtOpenSection)(PHANDLE SectionHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes);
  NTSTATUS (NTAPI *NtMapViewOfSection)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID *BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, SECTION_INHERIT InheritDisposition, ULONG AllocationType, ULONG Win32Protect);
  NTSTATUS (NTAPI *NtUnmapViewOfSection)(HANDLE ProcessHandle, PVOID BaseAddress);
  ULONG (NTAPI *RtlNtStatusToDosError)(NTSTATUS Status);
#if defined(__i386__) || defined(_M_IX86)
  DWORD (WINAPI *VxDCall2)(DWORD Service, DWORD Arg1, DWORD Arg2);
  LPVOID w32skrnl_dpmi_lcall_ptr;
  DWORD base_addr_offset;
#endif
};

#if defined(__i386__) || defined(_M_IX86)

static BOOL
w32skrnl_physical_address_mapping(struct physmem *physmem, DWORD phys_addr, DWORD size, DWORD *virt_addr)
{
  DWORD address_hi = phys_addr >> 16;
  DWORD address_lo = phys_addr & 0xffff;
  DWORD size_hi = size >> 16;
  DWORD size_lo = size & 0xffff;
  BYTE failed;

  /*
   * Physical address mapping via w32skrnl.dll on Windows maps physical memory
   * and translates it to the virtual space of the current process memory.
   * Works only for aligned address / length and first 1 MB cannot be mapped
   * by this method. Expect that first 1 MB is already 1:1 mapped by the OS.
   * So accept request for physical memory range which is whole below 1 MB
   * without error and return virtual address same as the physical one.
   */
  if (phys_addr < 1*1024*1024UL)
    {
      if ((u64)phys_addr + size > 1*1024*1024UL)
        {
          errno = ENXIO;
          return FALSE;
        }

      *virt_addr = phys_addr;
      return TRUE;
    }

  /*
   * Unfortunately w32skrnl.dll provides only 48-bit fword pointer to physical
   * address mapping function and such pointer type is not supported by GCC,
   * nor by MSVC and therefore it is not possible call this function in C code.
   * So call this function with all parameters passed via inline assembly.
   */
#if defined(__GNUC__)
  asm volatile (
    "stc\n\t"
    "lcall *(%3)\n\t"
    "setc %0\n\t"
      : "=qm" (failed), "+b" (address_hi), "+c" (address_lo)
      : "r" (physmem->w32skrnl_dpmi_lcall_ptr), "a" (DPMI_PHYSICAL_ADDRESS_MAPPING), "S" (size_hi), "D" (size_lo)
      : "cc", "memory"
  );
#elif defined(_MSC_VER)
  __asm {
    mov esi, size_hi
    mov edi, size_lo
    mov ebx, address_hi
    mov ecx, address_lo
    mov eax, DPMI_PHYSICAL_ADDRESS_MAPPING
    stc
    mov edx, physmem
    mov edx, [edx]physmem.w32skrnl_dpmi_lcall_ptr
    call fword ptr [edx]
    setc failed
    mov address_hi, ebx
    mov address_lo, ecx
  }
#else
#error "Unsupported compiler"
#endif

  /*
   * Windows does not provide any error code when this function call fails.
   * So set errno just to the generic EACCES value.
   */
  if (failed)
    {
      errno = EACCES;
      return FALSE;
    }

  *virt_addr = ((address_hi & 0xffff) << 16) | (address_lo & 0xffff);
  return TRUE;
}

#if defined(__GNUC__) && ((__GNUC__ == 4 && __GNUC_MINOR__ >= 8) || (__GNUC__ > 4)) && (__GNUC__ <= 11)
/*
 * GCC versions 4.8 - 11 are buggy and throw error "'asm' operand has impossible
 * constraints" for inline assembly when optimizations (O1+) are enabled. So for
 * these GCC versions disable buggy optimizations by enforcing O0 optimize flag
 * affecting just this one function.
 */
__attribute__((optimize("O0")))
#endif
static BOOL
vdxcall_physical_address_mapping(struct physmem *physmem, DWORD phys_addr, DWORD size, DWORD *virt_addr)
{
  DWORD address_hi = phys_addr >> 16;
  DWORD address_lo = phys_addr & 0xffff;
  DWORD size_hi = size >> 16;
  DWORD size_lo = size & 0xffff;
  BYTE failed;

  /*
   * Physical address mapping via VxDCall2() on Windows maps physical memory
   * and translates it to the virtual space of the current process memory.
   * There are no restrictions for aligning or physical address ranges.
   * Works with any (unaligned) address or length, including low 1MB range.
   */

  /*
   * Function VxDCall2() has strange calling convention. First 3 arguments are
   * passed on stack, which callee pops (same as stdcall convention) but rest
   * of the function arguments are passed in ESI, EDI and EBX registers. And
   * return value is in carry flag (CF) and AX, BX and CX registers. GCC and
   * neither MSVC do not support this strange calling convention, so call this
   * function via inline assembly.
   *
   * Pseudocode with stdcall calling convention of that function looks like:
   * ESI = size_hi
   * EDI = size_lo
   * EBX = address_hi
   * VxDCall2(VWIN32_Int31Dispatch, DPMI_PHYSICAL_ADDRESS_MAPPING, address_lo)
   * failed = CF
   * address_hi = BX (if not failed)
   * address_lo = CX (if not failed)
   */

#if defined(__GNUC__)
  asm volatile (
    "pushl %6\n\t"
    "pushl %5\n\t"
    "pushl %4\n\t"
    "stc\n\t"
    "call *%P3\n\t"
    "setc %0\n\t"
      : "=qm" (failed), "+b" (address_hi), "=c" (address_lo)
      : "rmi" (physmem->VxDCall2), "rmi" (VWIN32_Int31Dispatch), "rmi" (DPMI_PHYSICAL_ADDRESS_MAPPING),
        "rmi" (address_lo), "S" (size_hi), "D" (size_lo)
/* Specify all call clobbered scratch registers for stdcall calling convention. */
      : "eax", "edx",
/*
 * Since GCC version 4.9.0 it is possible to specify x87 registers as clobbering
 * if they are not disabled by -mno-80387 switch (which defines _SOFT_FLOAT).
 */
#if ((__GNUC__ == 4 && __GNUC_MINOR__ >= 9) || (__GNUC__ > 4)) && !defined(_SOFT_FLOAT)
        "st", "st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)",
#endif
#ifdef __MMX__
        "mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm6",
#endif
#ifdef __SSE__
        "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",
#endif
        "cc", "memory"
  );
#elif defined(_MSC_VER)
  __asm {
    mov esi, size_hi
    mov edi, size_lo
    mov ebx, address_hi
    push address_lo
    push DPMI_PHYSICAL_ADDRESS_MAPPING
    push VWIN32_Int31Dispatch
    stc
    mov eax, physmem
    call [eax]physmem.VxDCall2
    setc failed
    mov address_hi, ebx
    mov address_lo, ecx
  }
#else
#error "Unsupported compiler"
#endif

  /*
   * Windows does not provide any error code when this function call fails.
   * So set errno just to the generic EACCES value.
   */
  if (failed)
    {
      errno = EACCES;
      return FALSE;
    }

  *virt_addr = ((address_hi & 0xffff) << 16) | (address_lo & 0xffff);
  return TRUE;
}

static BOOL
win32_get_physmem_offset(DWORD *offset)
{
  WORD DSsel;
  LDT_ENTRY DSentry;

  /*
   * Read DS selector. For this purpose there is WinAPI function and when called
   * as GetThreadContext(GetCurrentThread(), ...) with CONTEXT_SEGMENTS param,
   * it fills SegDs value. But on some Windows versions, GetThreadContext() can
   * be called only for threads attached to debugger. Hence we cannot use it for
   * our current thread. So instead read DS selector directly from ds register
   * via inline assembly code.
   */
#if defined(__GNUC__)
  asm ("movw %%ds, %w0" : "=rm" (DSsel));
#elif defined(_MSC_VER)
  __asm { mov DSsel, ds }
#else
#error "Unsupported compiler"
#endif

  if (!GetThreadSelectorEntry(GetCurrentThread(), DSsel, &DSentry))
    return FALSE;

  *offset = DSentry.BaseLow | (DSentry.HighWord.Bytes.BaseMid << 0x10) | (DSentry.HighWord.Bytes.BaseHi << 0x18);
  return TRUE;
}

static BYTE *
win32_get_baseaddr_from_hmodule(HMODULE module)
{
  WORD (WINAPI *ImteFromHModule)(HMODULE);
  BYTE *(WINAPI *BaseAddrFromImte)(WORD);
  HMODULE w32skrnl;
  WORD imte;

  if ((GetVersion() & 0xC0000000) != 0x80000000)
    return (BYTE *)module;

  w32skrnl = GetModuleHandleA("w32skrnl.dll");
  if (!w32skrnl)
    return NULL;

  ImteFromHModule = (LPVOID)GetProcAddress(w32skrnl, "_ImteFromHModule@4");
  BaseAddrFromImte = (LPVOID)GetProcAddress(w32skrnl, "_BaseAddrFromImte@4");
  if (!ImteFromHModule || !BaseAddrFromImte)
    return NULL;

  imte = ImteFromHModule(module);
  if (imte == 0xffff)
    return NULL;

  return BaseAddrFromImte(imte);
}

static FARPROC
win32_get_proc_address_by_ordinal(HMODULE module, DWORD ordinal, BOOL must_be_without_name)
{
  BYTE *baseaddr;
  IMAGE_DOS_HEADER *dos_header;
  IMAGE_NT_HEADERS *nt_header;
  DWORD export_dir_offset, export_dir_size;
  IMAGE_EXPORT_DIRECTORY *export_dir;
  DWORD base_ordinal, func_count;
  DWORD *func_addrs;
  FARPROC func_ptr;
  DWORD names_count, i;
  USHORT *names_idxs;
  UINT prev_error_mode;
  char module_name[MAX_PATH];
  DWORD module_name_len;
  char *export_name;
  char *endptr;
  long num;

  baseaddr = win32_get_baseaddr_from_hmodule(module);
  if (!baseaddr)
    return NULL;

  dos_header = (IMAGE_DOS_HEADER *)baseaddr;
  if (dos_header->e_magic != IMAGE_DOS_SIGNATURE)
    return NULL;

  nt_header = (IMAGE_NT_HEADERS *)((BYTE *)dos_header + dos_header->e_lfanew);
  if (nt_header->Signature != IMAGE_NT_SIGNATURE)
    return NULL;

  if (nt_header->FileHeader.SizeOfOptionalHeader < offsetof(IMAGE_OPTIONAL_HEADER, DataDirectory))
    return NULL;

  if (nt_header->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC)
    return NULL;

  if (nt_header->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT)
    return NULL;

  export_dir_offset = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
  export_dir_size = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

  if (!export_dir_offset || !export_dir_size)
    return NULL;

  export_dir = (IMAGE_EXPORT_DIRECTORY *)(baseaddr + export_dir_offset);
  base_ordinal = export_dir->Base;
  func_count = export_dir->NumberOfFunctions;
  func_addrs = (DWORD *)(baseaddr + (DWORD)export_dir->AddressOfFunctions);

  if (ordinal < base_ordinal || ordinal - base_ordinal > func_count)
    return NULL;

  if (must_be_without_name)
    {
      /* Check that function with ordinal number does not have any name. */
      names_count = export_dir->NumberOfNames;
      names_idxs = (USHORT *)(baseaddr + (DWORD)export_dir->AddressOfNameOrdinals);
      for (i = 0; i < names_count; i++)
        {
          if (names_idxs[i] == ordinal - base_ordinal)
            return NULL;
        }
    }

  func_ptr = (FARPROC)(baseaddr + func_addrs[ordinal - base_ordinal]);
  if ((BYTE *)func_ptr >= (BYTE *)export_dir && (BYTE *)func_ptr < (BYTE *)export_dir + export_dir_size)
    {
      /*
       * We need to locate the _last_ dot character (separator of library name
       * and export symbol name) because library name may contain dot character
       * (used when specifying file name with explicit extension). For example
       * wine is using this kind of strange symbol redirection to different
       * library with non-standard file name extension (different than .dll).
       */
      export_name = strrchr((char *)func_ptr, '.');
      if (!export_name)
        return NULL;
      export_name++;

      module_name_len = export_name - 1 - (char *)func_ptr;
      if (module_name_len >= sizeof(module_name))
        return NULL;

      memcpy(module_name, func_ptr, module_name_len);
      module_name[module_name_len] = 0;

      prev_error_mode = win32_change_error_mode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX, TRUE);
      module = LoadLibraryA(module_name);
      win32_change_error_mode(prev_error_mode, FALSE);
      if (!module)
        {
          FreeLibrary(module);
          return NULL;
        }

      if (*export_name == '#')
        {
          export_name++;
          errno = 0;
          num = strtol(export_name, &endptr, 10);
          if (*export_name < '0' || *export_name > '9' || errno || *endptr || num < 0 || (unsigned long)num >= ((DWORD)-1)/2)
            {
              FreeLibrary(module);
              return NULL;
            }
          ordinal = num;
          func_ptr = win32_get_proc_address_by_ordinal(module, ordinal, FALSE);
        }
      else
        {
          func_ptr = GetProcAddress(module, export_name);
        }

      if (!func_ptr)
        FreeLibrary(module);
    }

  return func_ptr;
}

static int
init_physmem_w32skrnl(struct physmem *physmem, struct pci_access *a)
{
  HMODULE w32skrnl;
  LPVOID (WINAPI *GetThunkBuff)(VOID);
  LPVOID buf_ptr;
  DWORD raw_version;
  USHORT build_num;
  BOOL build_num_valid;

  a->debug("resolving DPMI function via GetThunkBuff() function from w32skrnl.dll...");
  w32skrnl = GetModuleHandleA("w32skrnl.dll");
  if (!w32skrnl)
    {
      a->debug("failed: library not present.");
      errno = ENOENT;
      return 0;
    }

  GetThunkBuff = (LPVOID)GetProcAddress(w32skrnl, "_GetThunkBuff@0");
  if (!GetThunkBuff)
    {
      a->debug("failed: symbol not found.");
      errno = ENOENT;
      return 0;
    }

  raw_version = GetVersion();
  build_num = (raw_version >> 16) & 0x3FFF;
  build_num_valid = ((raw_version & 0xC0000000) == 0x80000000 && (raw_version & 0xff) < 4);

  /* Builds older than 88 (match version 1.1) are not supported. */
  if (build_num_valid && build_num < 88)
    {
      a->debug("failed: found old incompatible version.");
      errno = ENOENT;
      return 0;
    }

  if (!win32_get_physmem_offset(&physmem->base_addr_offset))
    {
      a->debug("failed: cannot retrieve physical address offset: %s.", win32_strerror(GetLastError()));
      errno = EINVAL;
      return 0;
    }

  buf_ptr = GetThunkBuff();
  if (!buf_ptr)
    {
      a->debug("failed: cannot retrieve DPMI function pointer.");
      errno = EINVAL;
      return 0;
    }

  /*
   * Builds 88-103 (match versions 1.1-1.15) have DPMI function at offset 0xa0.
   * Builds 111-172 (match versions 1.15a-1.30c) have DPMI function at offset 0xa4.
   * If build number is unavailable then expects the latest version.
   */
  physmem->w32skrnl_dpmi_lcall_ptr = (LPVOID)((BYTE *)buf_ptr + ((build_num_valid && build_num < 111) ? 0xa0 : 0xa4));

  a->debug("success.");
  return 1;
}

static int
init_physmem_vxdcall(struct physmem *physmem, struct pci_access *a)
{
  HMODULE kernel32;
  BOOL success;
  DWORD addr;

  a->debug("resolving VxDCall2() function from kernel32.dll...");
  kernel32 = GetModuleHandleA("kernel32.dll");
  if (!kernel32)
    {
      a->debug("failed: library not present.");
      errno = ENOENT;
      return 0;
    }

  /*
   * New Windows versions do not export VxDCall2 symbol by name anymore,
   * so try also locating this symbol by its ordinal number, which is 3.
   * Old Windows versions prevents using GetProcAddress() for locating
   * kernel32.dll symbol by ordinal number, so use our own custom function.
   * When locating via ordinal number, check that this ordinal number does
   * not have any name assigned (to ensure that it is really VxDCall2).
   */
  physmem->VxDCall2 = (LPVOID)GetProcAddress(kernel32, "VxDCall2");
  if (!physmem->VxDCall2)
    physmem->VxDCall2 = (LPVOID)win32_get_proc_address_by_ordinal(kernel32, 3, TRUE);

  if (!physmem->VxDCall2)
    {
      a->debug("failed: symbol not found.");
      errno = ENOENT;
      return 0;
    }

  /*
   * Wine implementation of VxDCall2() does not support physical address
   * mapping but returns success with virtual address same as passed physical
   * address. Detect this broken wine behavior by trying to map zero address
   * of zero range. Broken wine implementation returns NULL pointer. This
   * prevents accessing unmapped memory or dereferencing NULL pointer.
   */
  success = vdxcall_physical_address_mapping(physmem, 0x0, 0x0, &addr);
  if (success && addr == 0x0)
    {
      a->debug("failed: physical address mapping via VxDCall2() is broken.");
      physmem->VxDCall2 = NULL;
      errno = EINVAL;
      return 0;
    }
  else if (!success)
    {
      a->debug("failed: physical address mapping via VxDCall2() is unsupported.");
      physmem->VxDCall2 = NULL;
      errno = ENOENT;
      return 0;
    }

  /* Retrieve base address - offset for all addresses returned by VxDCall2(). */
  if (!win32_get_physmem_offset(&physmem->base_addr_offset))
    {
      a->debug("failed: cannot retrieve physical address offset: %s.", win32_strerror(GetLastError()));
      physmem->VxDCall2 = NULL;
      errno = EINVAL;
      return 0;
    }

  a->debug("success.");
  return 1;
}

#endif

static int
init_physmem_ntdll(struct physmem *physmem, struct pci_access *a, const char *filename, int w)
{
  wchar_t *wide_filename;
  UNICODE_STRING unicode_filename;
  OBJECT_ATTRIBUTES attributes;
  NTSTATUS status;
  HMODULE ntdll;
  int len;

  a->debug("resolving section functions from ntdll.dll...");
  ntdll = GetModuleHandle(TEXT("ntdll.dll"));
  if (!ntdll)
    {
      a->debug("failed: library ntdll.dll is not present.");
      errno = ENOENT;
      return 0;
    }

  physmem->RtlNtStatusToDosError = (LPVOID)GetProcAddress(ntdll, "RtlNtStatusToDosError");

  physmem->NtOpenSection = (LPVOID)GetProcAddress(ntdll, "NtOpenSection");
  if (!physmem->NtOpenSection)
    {
      a->debug("failed: function NtOpenSection() not found.");
      errno = ENOENT;
      return 0;
    }

  physmem->NtMapViewOfSection = (LPVOID)GetProcAddress(ntdll, "NtMapViewOfSection");
  if (!physmem->NtMapViewOfSection)
    {
      a->debug("failed: function NtMapViewOfSection() not found.");
      errno = ENOENT;
      return 0;
    }

  physmem->NtUnmapViewOfSection = (LPVOID)GetProcAddress(ntdll, "NtUnmapViewOfSection");
  if (!physmem->NtUnmapViewOfSection)
    {
      a->debug("failed: function NtUnmapViewOfSection() not found.");
      errno = ENOENT;
      return 0;
    }

  a->debug("success.");

  /*
   * Note: It is not possible to use WinAPI function OpenFileMappingA() because
   * it takes path relative to the NT base path \\Sessions\\X\\BaseNamedObjects\\
   * and so it does not support to open sections outside that NT directory.
   * NtOpenSection() does not have this restriction and supports specifying any
   * path, including path in absolute format. Unfortunately NtOpenSection()
   * takes path in UNICODE_STRING structure, unlike OpenFileMappingA() which
   * takes path in 8-bit char*. So first it is needed to do conversion from
   * char* string to wchar_t* string via function MultiByteToWideChar() and
   * then fill UNICODE_STRING structure from that wchar_t* string via function
   * RtlInitUnicodeString().
   */

  len = MultiByteToWideChar(CP_ACP, 0, filename, -1, NULL, 0);
  if (len <= 0)
    {
      a->debug("Option devmem.path '%s' is invalid multibyte string.", filename);
      errno = EINVAL;
      return 0;
    }

  wide_filename = pci_malloc(a, len * sizeof(wchar_t));
  len = MultiByteToWideChar(CP_ACP, 0, filename, -1, wide_filename, len);
  if (len <= 0)
    {
      a->debug("Option devmem.path '%s' is invalid multibyte string.", filename);
      pci_mfree(wide_filename);
      errno = EINVAL;
      return 0;
    }

  RtlInitUnicodeString(&unicode_filename, wide_filename);
  InitializeObjectAttributes(&attributes, &unicode_filename, OBJ_CASE_INSENSITIVE, NULL, NULL);

  a->debug("trying to open NT Section %s in %s mode...", filename, w ? "read/write" : "read-only");
  physmem->section_handle = INVALID_HANDLE_VALUE;
  status = physmem->NtOpenSection(&physmem->section_handle, SECTION_MAP_READ | (w ? SECTION_MAP_WRITE : 0), &attributes);

  pci_mfree(wide_filename);

  if (status < 0 || physmem->section_handle == INVALID_HANDLE_VALUE)
    {
      physmem->section_handle = INVALID_HANDLE_VALUE;
      if (status == 0)
        a->debug("failed.");
      else if (physmem->RtlNtStatusToDosError)
        a->debug("failed: %s (0x%lx).", win32_strerror(physmem->RtlNtStatusToDosError(status)), status);
      else
        a->debug("failed: 0x%lx.", status);
      switch (status)
        {
        case STATUS_INVALID_PARAMETER: /* SectionHandle or ObjectAttributes parameter is invalid */
          errno = EINVAL;
          break;
        case STATUS_OBJECT_NAME_NOT_FOUND: /* Section name in ObjectAttributes.ObjectName does not exist */
          errno = ENOENT;
          break;
        case STATUS_ACCESS_DENIED: /* No permission to access Section name in ObjectAttributes.ObjectName */
          errno = EACCES;
          break;
        default: /* Other unspecified error */
          errno = EINVAL;
          break;
        }
      return 0;
    }

  a->debug("success.");
  return 1;
}

void
physmem_init_config(struct pci_access *a)
{
  pci_define_param(a, "devmem.path", PCI_PATH_DEVMEM_DEVICE, "NT path to the PhysicalMemory NT Section"
#if defined(__i386__) || defined(_M_IX86)
    " or \"vxdcall\" or \"w32skrnl\""
#endif
  );
}

int
physmem_access(struct pci_access *a, int w)
{
  struct physmem *physmem = physmem_open(a, w);
  if (!physmem)
    return -1;
  physmem_close(physmem);
  return 0;
}

struct physmem *
physmem_open(struct pci_access *a, int w)
{
  const char *devmem = pci_get_param(a, "devmem.path");
#if defined(__i386__) || defined(_M_IX86)
  int force_vxdcall = strcmp(devmem, "vxdcall") == 0;
  int force_w32skrnl = strcmp(devmem, "w32skrnl") == 0;
#endif
  struct physmem *physmem = pci_malloc(a, sizeof(*physmem));

  memset(physmem, 0, sizeof(*physmem));
  physmem->section_handle = INVALID_HANDLE_VALUE;

  errno = ENOENT;

  if (
#if defined(__i386__) || defined(_M_IX86)
      !force_vxdcall && !force_w32skrnl &&
#endif
      init_physmem_ntdll(physmem, a, devmem, w))
    return physmem;

#if defined(__i386__) || defined(_M_IX86)
  if (!force_w32skrnl && init_physmem_vxdcall(physmem, a))
    return physmem;

  if (!force_vxdcall && init_physmem_w32skrnl(physmem, a))
    return physmem;
#endif

  a->debug("no windows method for physical memory access.");
  pci_mfree(physmem);
  return NULL;
}

void
physmem_close(struct physmem *physmem)
{
  if (physmem->section_handle != INVALID_HANDLE_VALUE)
    CloseHandle(physmem->section_handle);
  pci_mfree(physmem);
}

long
physmem_get_pagesize(struct physmem *physmem UNUSED)
{
  SYSTEM_INFO system_info;
  system_info.dwPageSize = 0;
  GetSystemInfo(&system_info);
  return system_info.dwPageSize;
}

void *
physmem_map(struct physmem *physmem, u64 addr, size_t length, int w)
{
  LARGE_INTEGER section_offset;
  NTSTATUS status;
  SIZE_T view_size;
  VOID *ptr;

  if (physmem->section_handle != INVALID_HANDLE_VALUE)
    {
      /*
       * Note: Do not use WinAPI function MapViewOfFile() because it makes memory
       * mapping available also for all child processes that are spawned in the
       * future. NtMapViewOfSection() allows to specify ViewUnmap parameter which
       * creates mapping just for this process and not for future child processes.
       * For security reasons we do not want this physical address mapping to be
       * present also in future spawned processes.
       */
      ptr = NULL;
      section_offset.QuadPart = addr;
      view_size = length;
      status = physmem->NtMapViewOfSection(physmem->section_handle, GetCurrentProcess(), &ptr, 0, 0, &section_offset, &view_size, ViewUnmap, 0, w ? PAGE_READWRITE : PAGE_READONLY);
      if (status < 0)
        {
          switch (status)
            {
            case STATUS_INVALID_HANDLE: /* Invalid SectionHandle (physmem->section_handle) */
            case STATUS_INVALID_PARAMETER_3: /* Invalid BaseAddress parameter (&ptr) */
            case STATUS_CONFLICTING_ADDRESSES: /* Invalid value of BaseAddress pointer (ptr) */
            case STATUS_MAPPED_ALIGNMENT: /* Invalid value of BaseAddress pointer (ptr) or SectionOffset (section_offset) */
            case STATUS_INVALID_PARAMETER_4: /* Invalid ZeroBits parameter (0) */
            case STATUS_INVALID_PARAMETER_5: /* Invalid CommitSize parameter (0) */
            case STATUS_INVALID_PARAMETER_8: /* Invalid InheritDisposition parameter (ViewUnmap) */
            case STATUS_INVALID_PARAMETER_9: /* Invalid AllocationType parameter (0) */
            case STATUS_SECTION_PROTECTION: /* Invalid Protect parameter (based on w) */
            case STATUS_INVALID_PAGE_PROTECTION: /* Invalid Protect parameter (based on w) */
              errno = EINVAL;
              break;
            case STATUS_INVALID_VIEW_SIZE: /* Invalid SectionOffset / ViewSize range (section_offset, view_size) */
              errno = ENXIO;
              break;
            case STATUS_INSUFFICIENT_RESOURCES: /* Quota limit exceeded */
            case STATUS_NO_MEMORY: /* Memory limit exceeded */
              errno = ENOMEM;
              break;
            case STATUS_ACCESS_DENIED: /* No permission to create mapping */
              errno = EPERM;
              break;
            default: /* Other unspecified error */
              errno = EACCES;
              break;
            }
          return (void *)-1;
        }

      return ptr;
    }
#if defined(__i386__) || defined(_M_IX86)
  else if (physmem->VxDCall2 || physmem->w32skrnl_dpmi_lcall_ptr)
    {
      BOOL success;
      DWORD virt;

      /*
       * These two methods support mapping only the first 4 GB of physical memory
       * and mapped memory is always read/write. There is no way to create
       * read-only mapping, so argument "w" is ignored.
       */
      if (addr >= 0xffffffffUL || addr + length > 0xffffffffUL)
        {
          errno = EOVERFLOW;
          return (void *)-1;
        }

      if (physmem->VxDCall2)
        success = vdxcall_physical_address_mapping(physmem, (DWORD)addr, length, &virt);
      else
        success = w32skrnl_physical_address_mapping(physmem, (DWORD)addr, length, &virt);

      /* Both above functions set errno on failure. */
      if (!success)
        return (void *)-1;

      /* Virtual address from our view is calculated from the base offset. */
      ptr = (VOID *)(virt - physmem->base_addr_offset);
      return ptr;
    }
#endif


  /* invalid physmem parameter */
  errno = EBADF;
  return (void *)-1;
}

int
physmem_unmap(struct physmem *physmem, void *ptr, size_t length)
{
  long pagesize = physmem_get_pagesize(physmem);
  MEMORY_BASIC_INFORMATION info;
  NTSTATUS status;
  DWORD region_size;

  if (physmem->section_handle != INVALID_HANDLE_VALUE)
    {
      /*
       * NtUnmapViewOfSection() unmaps entire memory range previously mapped by
       * NtMapViewOfSection(). The specified ptr (BaseAddress) does not have to
       * point to the beginning of the mapped memory range.
       *
       * So verify that the ptr argument is the beginning of the mapped range
       * and length argument is the total length of mapped range.
       *
       * VirtualQuery() does not have to return whole mapped range, but just
       * some region subset. So call VirtualQuery() multiple times for each
       * region subset until we receive size of the whole mapped region range.
       */

      for (region_size = 0; region_size < length; region_size += info.RegionSize)
        {
          if (VirtualQuery(ptr + region_size, &info, sizeof(info)) != sizeof(info))
            {
              errno = EINVAL;
              return -1;
            }

          if (info.AllocationBase != ptr)
            {
              errno = EINVAL;
              return -1;
            }
        }

      /* RegionSize is already page aligned, but length does not have to be. */
      if (region_size != ((length + pagesize-1) & ~(pagesize-1)))
        {
          errno = EINVAL;
          return -1;
        }

      status = physmem->NtUnmapViewOfSection(GetCurrentProcess(), ptr);
      if (status < 0)
        {
          switch (status)
            {
            case STATUS_NOT_MAPPED_VIEW: /* BaseAddress parameter (ptr) not mapped */
              errno = EINVAL;
              break;
            case STATUS_ACCESS_DENIED: /* No permission to unmap BaseAddress (ptr) */
              errno = EPERM;
              break;
            default: /* Other unspecified error */
              errno = EACCES;
              break;
            }
          return -1;
        }

      return 0;
    }
#if defined(__i386__) || defined(_M_IX86)
  else if (physmem->VxDCall2 || physmem->w32skrnl_dpmi_lcall_ptr)
    {
      /* There is no way to unmap physical memory mapped by these methods. */
      errno = ENOSYS;
      return -1;
    }
#endif

  /* invalid physmem parameter */
  errno = EBADF;
  return -1;
}
