/* 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/. */

#include "mozilla/mscom/ProcessRuntime.h"

#include <accctrl.h>
#include <aclapi.h>
#include <objbase.h>
#include <objidl.h>

#include "mozilla/Assertions.h"
#include "mozilla/mscom/COMWrappers.h"
#include "mozilla/mscom/ProcessRuntimeShared.h"
#include "mozilla/RefPtr.h"
#include "mozilla/WindowsProcessMitigations.h"

#if defined(MOZILLA_INTERNAL_API)
#  include "mozilla/mscom/EnsureMTA.h"
#  if defined(MOZ_SANDBOX)
#    include "mozilla/sandboxTarget.h"
#  endif  // defined(MOZ_SANDBOX)
#endif    // defined(MOZILLA_INTERNAL_API)

// This API from oleaut32.dll is not declared in Windows SDK headers
extern "C" void __cdecl SetOaNoCache(void);

using namespace mozilla::mscom::detail;

namespace mozilla {
namespace mscom {

#if defined(MOZILLA_INTERNAL_API)
ProcessRuntime* ProcessRuntime::sInstance = nullptr;

ProcessRuntime::ProcessRuntime() : ProcessRuntime(XRE_GetProcessType()) {}

ProcessRuntime::ProcessRuntime(const GeckoProcessType aProcessType)
    : ProcessRuntime(aProcessType == GeckoProcessType_Default
                         ? ProcessCategory::GeckoBrowserParent
                     : aProcessType == GeckoProcessType_Content
                         ? ProcessCategory::GeckoContent
                         : ProcessCategory::GeckoChild) {}
#endif  // defined(MOZILLA_INTERNAL_API)

ProcessRuntime::ProcessRuntime(const ProcessCategory aProcessCategory)
    : mInitResult(CO_E_NOTINITIALIZED), mProcessCategory(aProcessCategory) {
#if defined(MOZILLA_INTERNAL_API)
  MOZ_DIAGNOSTIC_ASSERT(!sInstance);
  sInstance = this;

  EnsureMTA();
  /**
   * From this point forward, all threads in this process are implicitly
   * members of the multi-threaded apartment, with the following exceptions:
   * 1. If any Win32 GUI APIs were called on the current thread prior to
   *    executing this constructor, then this thread has already been implicitly
   *    initialized as the process's main STA thread; or
   * 2. A thread explicitly and successfully calls CoInitialize(Ex) to specify
   *    otherwise.
   */

  const bool isCurThreadImplicitMTA = IsCurrentThreadImplicitMTA();
  // We only assert that the implicit MTA precondition holds when not running
  // as the Gecko parent process.
  MOZ_DIAGNOSTIC_ASSERT(aProcessCategory ==
                            ProcessCategory::GeckoBrowserParent ||
                        isCurThreadImplicitMTA);

  const bool needsMTAInit =
      GetDesiredApartmentType(mProcessCategory) == COINIT_MULTITHREADED;
#  if defined(MOZ_SANDBOX)
  // Child processes that want MTA and run under the sandbox need special
  // handling: the sandbox provides a privileged impersonation token during
  // early startup that must be active when COM is initialized, and PostInit
  // must be deferred until privilege drop.
  const bool needsSandboxedMTAInit =
      needsMTAInit && SandboxTarget::Instance()->IsSandboxed();
#  else
  const bool needsSandboxedMTAInit = false;
#  endif

  // If we are initializing COM on the MTA, we ideally want our current thread
  // to be running implicitly inside the MTA, but if for some reason we did
  // not end up with that, we may compensate by completing initialization via
  // EnsureMTA's persistent thread.
  if (!isCurThreadImplicitMTA && needsMTAInit) {
    InitUsingPersistentMTAThread(needsSandboxedMTAInit);
    return;
  }
#endif  // defined(MOZILLA_INTERNAL_API)

  mAptRegion.Init(GetDesiredApartmentType(mProcessCategory));

  // It can happen that we are not the outermost COM initialization on this
  // thread. In fact it should regularly be the case that the outermost
  // initialization occurs from outside of XUL, before we show the skeleton UI,
  // at which point we still need to run some things here from within XUL.
  if (!mAptRegion.IsValidOutermost()) {
    mInitResult = mAptRegion.GetHResult();
#if defined(MOZILLA_INTERNAL_API)
    MOZ_ASSERT(mProcessCategory == ProcessCategory::GeckoBrowserParent);
    if (mProcessCategory != ProcessCategory::GeckoBrowserParent) {
      // This is unexpected unless we're GeckoBrowserParent
      return;
    }

    ProcessInitLock lock;

    // Is another instance of ProcessRuntime responsible for the outer
    // initialization?
    const bool prevInit =
        lock.GetInitState() == ProcessInitState::FullyInitialized;
    MOZ_ASSERT(prevInit);
    if (prevInit) {
      PostInit();
    }
#endif  // defined(MOZILLA_INTERNAL_API)
    return;
  }

  InitInsideApartment();
  if (FAILED(mInitResult)) {
    return;
  }

#if defined(MOZILLA_INTERNAL_API)
#  if defined(MOZ_SANDBOX)
  if (needsSandboxedMTAInit) {
    // In sandboxed MTA child processes, defer PostInit until priv drop
    SandboxTarget::Instance()->RegisterSandboxStartCallback([self = this]() {
      // Ensure that we're still live and the init was successful before
      // calling PostInit()
      if (self == sInstance && SUCCEEDED(self->mInitResult)) {
        PostInit();
      }
    });
    return;
  }
#  endif  // defined(MOZ_SANDBOX)

  PostInit();
#endif  // defined(MOZILLA_INTERNAL_API)
}

#if defined(MOZILLA_INTERNAL_API)
ProcessRuntime::~ProcessRuntime() {
  MOZ_DIAGNOSTIC_ASSERT(sInstance == this);
  sInstance = nullptr;
}

void ProcessRuntime::InitUsingPersistentMTAThread(bool aNeedsSandboxedInit) {
  if (!aNeedsSandboxedInit) {
    EnsureMTA([this]() { InitInsideApartment(); },
              EnsureMTA::Option::ForceDispatchToPersistentThread);
    if (SUCCEEDED(mInitResult)) {
      PostInit();
    }

    return;
  }

#  if defined(MOZ_SANDBOX)
  HANDLE rawCurThreadImpToken;
  if (!::OpenThreadToken(::GetCurrentThread(), TOKEN_DUPLICATE | TOKEN_QUERY,
                         FALSE, &rawCurThreadImpToken)) {
    mInitResult = HRESULT_FROM_WIN32(::GetLastError());
    return;
  }
  nsAutoHandle curThreadImpToken(rawCurThreadImpToken);

  // Ensure that our current token is still an impersonation token (ie, we
  // have not yet called RevertToSelf() on this thread).
  DWORD len;
  TOKEN_TYPE tokenType;
  MOZ_RELEASE_ASSERT(
      ::GetTokenInformation(rawCurThreadImpToken, TokenType, &tokenType,
                            sizeof(tokenType), &len) &&
      len == sizeof(tokenType) && tokenType == TokenImpersonation);

  // Create an impersonation token based on the current thread's token
  HANDLE rawMtaThreadImpToken = nullptr;
  if (!::DuplicateToken(rawCurThreadImpToken, SecurityImpersonation,
                        &rawMtaThreadImpToken)) {
    mInitResult = HRESULT_FROM_WIN32(::GetLastError());
    return;
  }
  nsAutoHandle mtaThreadImpToken(rawMtaThreadImpToken);

  // Impersonate and initialize.
  bool tokenSet = false;
  EnsureMTA(
      [this, rawMtaThreadImpToken, &tokenSet]() -> void {
        if (!::SetThreadToken(nullptr, rawMtaThreadImpToken)) {
          mInitResult = HRESULT_FROM_WIN32(::GetLastError());
          return;
        }

        tokenSet = true;
        InitInsideApartment();
      },
      EnsureMTA::Option::ForceDispatchToPersistentThread);

  if (!tokenSet) {
    return;
  }

  SandboxTarget::Instance()->RegisterSandboxStartCallback(
      [self = this]() -> void {
        EnsureMTA(
            []() -> void {
              // This is a security risk if it fails, so we release assert
              MOZ_RELEASE_ASSERT(::RevertToSelf(),
                                 "mscom::ProcessRuntime RevertToSelf failed");
            },
            EnsureMTA::Option::ForceDispatchToPersistentThread);

        // Ensure that we're still live and the init was successful before
        // calling PostInit()
        if (self == sInstance && SUCCEEDED(self->mInitResult)) {
          PostInit();
        }
      });
#  endif  // defined(MOZ_SANDBOX)
}
#endif  // defined(MOZILLA_INTERNAL_API)

/* static */
COINIT ProcessRuntime::GetDesiredApartmentType(
    const ProcessRuntime::ProcessCategory aProcessCategory) {
  switch (aProcessCategory) {
    case ProcessCategory::GeckoBrowserParent:
      return COINIT_APARTMENTTHREADED;
    case ProcessCategory::GeckoContent:
      return COINIT_MULTITHREADED;
    case ProcessCategory::GeckoChild:
      if (!IsWin32kLockedDown()) {
        // If Win32k is not locked down then we probably still need STA.
        // We disable DDE since that is not usable from child processes.
        return static_cast<COINIT>(COINIT_APARTMENTTHREADED |
                                   COINIT_DISABLE_OLE1DDE);
      }
      [[fallthrough]];
    default:
      return COINIT_MULTITHREADED;
  }
}

void ProcessRuntime::InitInsideApartment() {
  ProcessInitLock lock;
  const ProcessInitState prevInitState = lock.GetInitState();
  if (prevInitState == ProcessInitState::FullyInitialized) {
    // COM has already been initialized by a previous ProcessRuntime instance
    mInitResult = S_OK;
    return;
  }

  if (prevInitState < ProcessInitState::PartialSecurityInitialized) {
    // We are required to initialize security prior to configuring global
    // options.
    mInitResult = InitializeSecurity(mProcessCategory);
    // Downgrading from a MOZ_DIAGNOSTIC_ASSERT while investigating
    // bug 1930846.
    MOZ_ASSERT(SUCCEEDED(mInitResult));

    // Even though this isn't great, we should try to proceed even when
    // CoInitializeSecurity has previously been called: the additional settings
    // we want to change are important enough that we don't want to skip them.
    if (FAILED(mInitResult) && mInitResult != RPC_E_TOO_LATE) {
      return;
    }

    lock.SetInitState(ProcessInitState::PartialSecurityInitialized);
  }

  if (prevInitState < ProcessInitState::PartialGlobalOptions) {
    RefPtr<IGlobalOptions> globalOpts;
    mInitResult = wrapped::CoCreateInstance(
        CLSID_GlobalOptions, nullptr, CLSCTX_INPROC_SERVER, IID_IGlobalOptions,
        getter_AddRefs(globalOpts));
    MOZ_ASSERT(SUCCEEDED(mInitResult));
    if (FAILED(mInitResult)) {
      return;
    }

    // Disable COM's catch-all exception handler
    mInitResult = globalOpts->Set(COMGLB_EXCEPTION_HANDLING,
                                  COMGLB_EXCEPTION_DONOT_HANDLE_ANY);
    MOZ_ASSERT(SUCCEEDED(mInitResult));
    if (FAILED(mInitResult)) {
      return;
    }

    lock.SetInitState(ProcessInitState::PartialGlobalOptions);
  }

  // Disable the BSTR cache (as it never invalidates, thus leaking memory)
  // (This function is itself idempotent, so we do not concern ourselves with
  // tracking whether or not we've already called it.)
  ::SetOaNoCache();

  lock.SetInitState(ProcessInitState::FullyInitialized);
}

#if defined(MOZILLA_INTERNAL_API)
/**
 * Guaranteed to run *after* the COM (and possible sandboxing) initialization
 * has successfully completed and stabilized. This method MUST BE IDEMPOTENT!
 */
/* static */ void ProcessRuntime::PostInit() {
  // Currently "roughed-in" but unused.
}
#endif  // defined(MOZILLA_INTERNAL_API)

/* static */
DWORD
ProcessRuntime::GetClientThreadId() {
  DWORD callerTid;
  HRESULT hr = ::CoGetCallerTID(&callerTid);
  // Don't return callerTid unless the call succeeded and returned S_FALSE,
  // indicating that the caller originates from a different process.
  if (hr != S_FALSE) {
    return 0;
  }

  return callerTid;
}

MOZ_COLD static HRESULT DiagnosticAssertOrHResultFromLastError() {
  HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
  MOZ_DIAGNOSTIC_ASSERT(SUCCEEDED(hr));
  return hr;
}

/* static */
HRESULT
ProcessRuntime::InitializeSecurity(const ProcessCategory aProcessCategory) {
  // Reserve enough room on the stack for the ACL to hold the maximum number of
  // ACCESS_ALLOWED_ACEs we might add. The calculation is explained in the
  // Remarks section of the documentation for InitializeAcl.
  const size_t kMaxAceCount = 5;
  constexpr auto requiredACLSize =
      sizeof(ACL) + (kMaxAceCount * (sizeof(ACCESS_ALLOWED_ACE) -
                                     sizeof(ACCESS_ALLOWED_ACE::SidStart) +
                                     SECURITY_MAX_SID_SIZE));
  static_assert((requiredACLSize % sizeof(DWORD)) == 0,
                "ACL length must be DWORD aligned.");
  alignas(DWORD) BYTE aclBytes[requiredACLSize];
  auto* pAcl = reinterpret_cast<ACL*>(aclBytes);
  if (!::InitializeAcl(pAcl, requiredACLSize, ACL_REVISION)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  HANDLE rawProcessToken;
  if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY,
                          &rawProcessToken)) {
    return DiagnosticAssertOrHResultFromLastError();
  }
  nsAutoHandle processToken(rawProcessToken);

  // Grant access to the user's SID.
  DWORD len = 0;
  SE_TOKEN_USER tokenUser;
  if (!::GetTokenInformation(processToken, TokenUser, &tokenUser,
                             sizeof(tokenUser), &len)) {
    return DiagnosticAssertOrHResultFromLastError();
  }
  if (!::AddAccessAllowedAce(pAcl, ACL_REVISION, COM_RIGHTS_EXECUTE,
                             &tokenUser.Sid)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  // Grant access to the SYSTEM SID.
  SE_SID wellKnownSid;
  DWORD wellKnownSidSize = sizeof(wellKnownSid);
  if (!::CreateWellKnownSid(WinLocalSystemSid, nullptr, &wellKnownSid,
                            &wellKnownSidSize)) {
    return DiagnosticAssertOrHResultFromLastError();
  }
  if (!::AddAccessAllowedAce(pAcl, ACL_REVISION, COM_RIGHTS_EXECUTE,
                             &wellKnownSid)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  // Grant access to the Administrator SID.
  wellKnownSidSize = sizeof(wellKnownSid);
  if (!::CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, &wellKnownSid,
                            &wellKnownSidSize)) {
    return DiagnosticAssertOrHResultFromLastError();
  }
  if (!::AddAccessAllowedAce(pAcl, ACL_REVISION, COM_RIGHTS_EXECUTE,
                             &wellKnownSid)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  // If we are the browser process, grant access to all non restricted app
  // containers.
  if (aProcessCategory == ProcessCategory::GeckoBrowserParent) {
    wellKnownSidSize = sizeof(wellKnownSid);
    if (!::CreateWellKnownSid(WinBuiltinAnyPackageSid, nullptr, &wellKnownSid,
                              &wellKnownSidSize)) {
      return DiagnosticAssertOrHResultFromLastError();
    }
    if (!::AddAccessAllowedAce(pAcl, ACL_REVISION, COM_RIGHTS_EXECUTE,
                               &wellKnownSid)) {
      return DiagnosticAssertOrHResultFromLastError();
    }
  }

  // If in an app container grant access to it. Don't fail if we get an error
  // retrieving the app container SID.
  alignas(TOKEN_APPCONTAINER_INFORMATION)
      BYTE appContainerInfoBuf[TOKEN_APPCONTAINER_SID_MAX_SIZE];
  auto* appContainerInfo =
      reinterpret_cast<TOKEN_APPCONTAINER_INFORMATION*>(appContainerInfoBuf);
  bool haveAppContainerSid =
      ::GetTokenInformation(processToken, TokenAppContainerSid,
                            appContainerInfo, sizeof(appContainerInfoBuf),
                            &len) &&
      appContainerInfo->TokenAppContainer;
  if (haveAppContainerSid) {
    if (!::AddAccessAllowedAce(pAcl, ACL_REVISION, COM_RIGHTS_EXECUTE,
                               appContainerInfo->TokenAppContainer)) {
      return DiagnosticAssertOrHResultFromLastError();
    }
  }

  alignas(TOKEN_PRIMARY_GROUP)
      BYTE primaryGroupBuf[sizeof(TOKEN_PRIMARY_GROUP) + SECURITY_MAX_SID_SIZE];
  auto* primaryGroup = reinterpret_cast<TOKEN_PRIMARY_GROUP*>(primaryGroupBuf);
  if (!::GetTokenInformation(processToken, TokenPrimaryGroup, primaryGroup,
                             sizeof(primaryGroupBuf), &len)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  SECURITY_DESCRIPTOR sd;
  if (!::InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  if (!::SetSecurityDescriptorDacl(&sd, TRUE, pAcl, FALSE)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  if (!::SetSecurityDescriptorOwner(&sd, tokenUser.User.Sid, FALSE)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  if (!::SetSecurityDescriptorGroup(&sd, primaryGroup->PrimaryGroup, FALSE)) {
    return DiagnosticAssertOrHResultFromLastError();
  }

  HRESULT hr = wrapped::CoInitializeSecurity(
      &sd, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT,
      RPC_C_IMP_LEVEL_IDENTIFY, nullptr, EOAC_NONE, nullptr);
  MOZ_DIAGNOSTIC_ASSERT(SUCCEEDED(hr));
  return hr;
}

}  // namespace mscom
}  // namespace mozilla
