Bug 2057112: Part 2 - Pass child process's module list by section handle r=win-reviewers,gstoll
Child processes now report loaded DLLs to the parent by section HANDLE instead of by filename. The DLL blocklist's NtMapViewOfSection hook already receives the section a module is being mapped from, so it duplicates that section, read-only, onto the ModuleLoadInfo for the load. The parent identifies the module from the HANDLE. Because the section outlives the view, this also fixes reporting for two kinds of load: a module the blocklist rejected, which is unmapped immediately, and a module unloaded before the parent processed the batch. Previously, either could be deleted before the parent had a chance to consider them. A handle cannot be forged into a reference to a different file and a child can only send one it holds. ValidateAndResolveModuleSection sanity checks: 1. The section must map, and the view must be MEM_IMAGE. A data section can be made over any openable file but an image section means the kernel accepted the file as a PE and the child's image-load policy applied to it. 2. The file must not be on a remote device. 3. The file must not carry a mandatory integrity label below medium. These checks replicate the checks of the MITIGATION_IMAGE_LOAD_NO_REMOTE and MITIGATION_IMAGE_LOAD_NO_LOW_LABEL child sandbox behaviors. A child can create a non-IMAGE section over any file it can open, so what it hands us is not bounded by the image-load policy. The check avoid considering such files at all. This currently applies to all child processes: content, gpu, RDD, socket, utility (all kinds), and GMP. All have these sandbox mitigations. Also fixes a couple of ways that a version resource could misuse VerQueryValueW's output in the parent: a zero length underflowing to SIZE_MAX in QueryStringValue and a too-short buffer dereference in GetFromImage. Differential Revision: https://phabricator.services.mozilla.com/D324703
This commit is contained in:
committed by
daparks@mozilla.com
parent
75b4e31ffc
commit
476da53d95
@@ -403,8 +403,9 @@ CrossProcessDllInterceptor::FuncHookType<NtMapViewOfSectionPtr>
|
||||
// All the code for patched_NtMapViewOfSection that relies on checked stack
|
||||
// buffers (e.g. mbi, sectionFileName) should be put in this helper function
|
||||
// (see bug 1733532).
|
||||
MOZ_NEVER_INLINE NTSTATUS AfterMapViewOfExecutableSection(
|
||||
HANDLE aProcess, PVOID* aBaseAddress, NTSTATUS aStubStatus) {
|
||||
MOZ_NEVER_INLINE NTSTATUS
|
||||
AfterMapViewOfExecutableSection(HANDLE aSection, HANDLE aProcess,
|
||||
PVOID* aBaseAddress, NTSTATUS aStubStatus) {
|
||||
// We don't care about mappings that aren't MEM_IMAGE.
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
NTSTATUS ntStatus =
|
||||
@@ -525,9 +526,30 @@ MOZ_NEVER_INLINE NTSTATUS AfterMapViewOfExecutableSection(
|
||||
}
|
||||
|
||||
if (nt::RtlGetProcessHeap()) {
|
||||
// Make a read-only duplicate of the section for the parent process. Only
|
||||
// child processes need this.
|
||||
// DuplicateHandle is in kernel32 and this intercepted function can run
|
||||
// before that is loaded, so use ntdll's equivalent.
|
||||
nt::AutoHandle sectionForParent;
|
||||
bool sectionForParentUnavailable = false;
|
||||
if (gBlocklistInitFlags & eDllBlocklistInitFlagIsChildProcess) {
|
||||
HANDLE duplicate = nullptr;
|
||||
if (NT_SUCCESS(::NtDuplicateObject(
|
||||
nt::kCurrentProcess, aSection, nt::kCurrentProcess, &duplicate,
|
||||
SECTION_QUERY | SECTION_MAP_READ, 0, 0)) &&
|
||||
duplicate) {
|
||||
sectionForParent = nt::AutoHandle(duplicate);
|
||||
} else {
|
||||
// Not fatal: the load proceeds and the parent simply cannot evaluate
|
||||
// this module.
|
||||
sectionForParentUnavailable = true;
|
||||
}
|
||||
}
|
||||
|
||||
ModuleLoadFrame::NotifySectionMap(
|
||||
nt::AllocatedUnicodeString(sectionFileName), *aBaseAddress, aStubStatus,
|
||||
loadStatus, isInjectedDependent);
|
||||
loadStatus, isInjectedDependent, std::move(sectionForParent),
|
||||
sectionForParentUnavailable);
|
||||
}
|
||||
|
||||
if (loadStatus == ModuleLoadInfo::Status::Loaded ||
|
||||
@@ -604,8 +626,8 @@ NTSTATUS NTAPI patched_NtMapViewOfSection(
|
||||
return stubStatus;
|
||||
}
|
||||
|
||||
NTSTATUS rv =
|
||||
AfterMapViewOfExecutableSection(aProcess, aBaseAddress, stubStatus);
|
||||
NTSTATUS rv = AfterMapViewOfExecutableSection(aSection, aProcess,
|
||||
aBaseAddress, stubStatus);
|
||||
if (FAILED(rv)) {
|
||||
rollback();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ ModuleLoadFrame::ModuleLoadFrame(PCUNICODE_STRING aRequestedDllName)
|
||||
ModuleLoadFrame::ModuleLoadFrame(nt::AllocatedUnicodeString&& aSectionName,
|
||||
const void* aMapBaseAddr, NTSTATUS aNtStatus,
|
||||
ModuleLoadInfo::Status aLoadStatus,
|
||||
bool aIsDependent)
|
||||
bool aIsDependent,
|
||||
nt::AutoHandle&& aSectionHandle,
|
||||
bool aSectionHandleUnavailable)
|
||||
: mPrev(sTopFrame.get()),
|
||||
mContext(nullptr),
|
||||
mLSPSubstitutionRequired(false),
|
||||
@@ -33,6 +35,11 @@ ModuleLoadFrame::ModuleLoadFrame(nt::AllocatedUnicodeString&& aSectionName,
|
||||
aIsDependent) {
|
||||
sTopFrame.set(this);
|
||||
|
||||
// This constructor serves a mapping that did not pass through LdrLoadDll, so
|
||||
// OnSectionMap never runs for it and the section handle has to be taken here.
|
||||
mLoadInfo.mSectionHandle = std::move(aSectionHandle);
|
||||
mLoadInfo.mSectionHandleUnavailable = aSectionHandleUnavailable;
|
||||
|
||||
gLoaderPrivateAPI.NotifyBeginDllLoad(&mContext, mLoadInfo.mSectionName);
|
||||
}
|
||||
|
||||
@@ -71,7 +78,8 @@ void ModuleLoadFrame::SetLSPSubstitutionRequired(PCUNICODE_STRING aLeafName) {
|
||||
void ModuleLoadFrame::NotifySectionMap(
|
||||
nt::AllocatedUnicodeString&& aSectionName, const void* aMapBaseAddr,
|
||||
NTSTATUS aMapNtStatus, ModuleLoadInfo::Status aLoadStatus,
|
||||
bool aIsDependent) {
|
||||
bool aIsDependent, nt::AutoHandle&& aSectionHandle,
|
||||
bool aSectionHandleUnavailable) {
|
||||
ModuleLoadFrame* topFrame = sTopFrame.get();
|
||||
if (!topFrame) {
|
||||
// The only time that this data is useful is during initial mapping of
|
||||
@@ -80,13 +88,15 @@ void ModuleLoadFrame::NotifySectionMap(
|
||||
// initial process startup.
|
||||
if (gLoaderPrivateAPI.IsDefaultObserver()) {
|
||||
OnBareSectionMap(std::move(aSectionName), aMapBaseAddr, aMapNtStatus,
|
||||
aLoadStatus, aIsDependent);
|
||||
aLoadStatus, aIsDependent, std::move(aSectionHandle),
|
||||
aSectionHandleUnavailable);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
topFrame->OnSectionMap(std::move(aSectionName), aMapBaseAddr, aMapNtStatus,
|
||||
aLoadStatus, aIsDependent);
|
||||
aLoadStatus, aIsDependent, std::move(aSectionHandle),
|
||||
aSectionHandleUnavailable);
|
||||
}
|
||||
|
||||
/* static */
|
||||
@@ -96,28 +106,35 @@ void ModuleLoadFrame::OnSectionMap(nt::AllocatedUnicodeString&& aSectionName,
|
||||
const void* aMapBaseAddr,
|
||||
NTSTATUS aMapNtStatus,
|
||||
ModuleLoadInfo::Status aLoadStatus,
|
||||
bool aIsDependent) {
|
||||
bool aIsDependent,
|
||||
nt::AutoHandle&& aSectionHandle,
|
||||
bool aSectionHandleUnavailable) {
|
||||
if (mLoadInfo.mBaseAddr) {
|
||||
// If mBaseAddr is not null then |this| has already seen a module load. This
|
||||
// means that we are witnessing a bare section map.
|
||||
OnBareSectionMap(std::move(aSectionName), aMapBaseAddr, aMapNtStatus,
|
||||
aLoadStatus, aIsDependent);
|
||||
aLoadStatus, aIsDependent, std::move(aSectionHandle),
|
||||
aSectionHandleUnavailable);
|
||||
return;
|
||||
}
|
||||
|
||||
mLoadInfo.mSectionName = std::move(aSectionName);
|
||||
mLoadInfo.mBaseAddr = aMapBaseAddr;
|
||||
mLoadInfo.mStatus = aLoadStatus;
|
||||
mLoadInfo.mSectionHandle = std::move(aSectionHandle);
|
||||
mLoadInfo.mSectionHandleUnavailable = aSectionHandleUnavailable;
|
||||
}
|
||||
|
||||
/* static */
|
||||
void ModuleLoadFrame::OnBareSectionMap(
|
||||
nt::AllocatedUnicodeString&& aSectionName, const void* aMapBaseAddr,
|
||||
NTSTATUS aMapNtStatus, ModuleLoadInfo::Status aLoadStatus,
|
||||
bool aIsDependent) {
|
||||
bool aIsDependent, nt::AutoHandle&& aSectionHandle,
|
||||
bool aSectionHandleUnavailable) {
|
||||
// We call the special constructor variant that is used for bare mappings.
|
||||
ModuleLoadFrame frame(std::move(aSectionName), aMapBaseAddr, aMapNtStatus,
|
||||
aLoadStatus, aIsDependent);
|
||||
aLoadStatus, aIsDependent, std::move(aSectionHandle),
|
||||
aSectionHandleUnavailable);
|
||||
}
|
||||
|
||||
NTSTATUS ModuleLoadFrame::SetLoadStatus(NTSTATUS aNtStatus,
|
||||
|
||||
@@ -30,12 +30,18 @@ class MOZ_RAII ModuleLoadFrame final {
|
||||
static void NotifyLSPSubstitutionRequired(PCUNICODE_STRING aLeafName);
|
||||
|
||||
/**
|
||||
* This static method is called by the NtMapViewOfSection hook.
|
||||
* Helper for the NtMapViewOfSection hook.
|
||||
*
|
||||
* Ownership of aSectionHandle is transfered to this function.
|
||||
* The section handle may be null, in which case the parent simply cannot
|
||||
* evaluate this module.
|
||||
*/
|
||||
static void NotifySectionMap(nt::AllocatedUnicodeString&& aSectionName,
|
||||
const void* aMapBaseAddr, NTSTATUS aMapNtStatus,
|
||||
ModuleLoadInfo::Status aLoadStatus,
|
||||
bool aIsDependent);
|
||||
bool aIsDependent,
|
||||
nt::AutoHandle&& aSectionHandle,
|
||||
bool aSectionHandleUnavailable);
|
||||
static bool ExistsTopFrame();
|
||||
|
||||
/**
|
||||
@@ -55,12 +61,16 @@ class MOZ_RAII ModuleLoadFrame final {
|
||||
*/
|
||||
ModuleLoadFrame(nt::AllocatedUnicodeString&& aSectionName,
|
||||
const void* aMapBaseAddr, NTSTATUS aNtStatus,
|
||||
ModuleLoadInfo::Status aLoadStatus, bool aIsDependent);
|
||||
ModuleLoadInfo::Status aLoadStatus, bool aIsDependent,
|
||||
nt::AutoHandle&& aSectionHandle,
|
||||
bool aSectionHandleUnavailable);
|
||||
|
||||
void SetLSPSubstitutionRequired(PCUNICODE_STRING aLeafName);
|
||||
void OnSectionMap(nt::AllocatedUnicodeString&& aSectionName,
|
||||
const void* aMapBaseAddr, NTSTATUS aMapNtStatus,
|
||||
ModuleLoadInfo::Status aLoadStatus, bool aIsDependent);
|
||||
ModuleLoadInfo::Status aLoadStatus, bool aIsDependent,
|
||||
nt::AutoHandle&& aSectionHandle,
|
||||
bool aSectionHandleUnavailable);
|
||||
|
||||
/**
|
||||
* A "bare" section mapping is one that was mapped without the code passing
|
||||
@@ -70,7 +80,9 @@ class MOZ_RAII ModuleLoadFrame final {
|
||||
static void OnBareSectionMap(nt::AllocatedUnicodeString&& aSectionName,
|
||||
const void* aMapBaseAddr, NTSTATUS aMapNtStatus,
|
||||
ModuleLoadInfo::Status aLoadStatus,
|
||||
bool aIsDependent);
|
||||
bool aIsDependent,
|
||||
nt::AutoHandle&& aSectionHandle,
|
||||
bool aSectionHandleUnavailable);
|
||||
|
||||
private:
|
||||
// Link to the previous frame
|
||||
|
||||
@@ -1789,6 +1789,10 @@ parent:
|
||||
* obtain enough information about a DLL file to determine its
|
||||
* trustworthiness. This API asks the chrome process to perform that
|
||||
* evaluation.
|
||||
*
|
||||
* Each entry carries a handle to the file backing the module, captured by
|
||||
* the DLL blocklist from the handle the loader opened. The parent derives
|
||||
* the module's path from the handle and evaluates the module it targets.
|
||||
*/
|
||||
async GetModulesTrust(ModuleIdentifiers aModIdents, bool aRunAtNormalPriority)
|
||||
returns (ModulesMapResult? modMapResult);
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
#include "mozilla/gmp/PGMPChild.h"
|
||||
|
||||
namespace mozilla {
|
||||
#ifdef XP_WIN
|
||||
struct ModuleIdentifiers;
|
||||
#endif
|
||||
|
||||
namespace ipc {
|
||||
class ByteBuf;
|
||||
|
||||
@@ -106,6 +106,13 @@ NTSTATUS NTAPI NtReadVirtualMemory(HANDLE aProcessHandle, PVOID aBaseAddress,
|
||||
PVOID aBuffer, SIZE_T aNumBytesToRead,
|
||||
PSIZE_T aNumBytesRead);
|
||||
|
||||
NTSTATUS NTAPI NtDuplicateObject(HANDLE aSourceProcessHandle,
|
||||
HANDLE aSourceHandle,
|
||||
HANDLE aTargetProcessHandle,
|
||||
PHANDLE aTargetHandle,
|
||||
ACCESS_MASK aDesiredAccess,
|
||||
ULONG aHandleAttributes, ULONG aOptions);
|
||||
|
||||
NTSTATUS NTAPI LdrLoadDll(PWCHAR aDllPath, PULONG aFlags,
|
||||
PUNICODE_STRING aDllName, PHANDLE aOutHandle);
|
||||
|
||||
@@ -1755,6 +1762,51 @@ class RtlAllocPolicy {
|
||||
[[nodiscard]] bool checkSimulatedOOM() const { return true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* A minimal owning wrapper for a handle, closed with NtClose.
|
||||
*
|
||||
* Code reachable from the DLL blocklist hooks cannot use nsAutoHandle (XPCOM)
|
||||
* or UniqueFileHandle, whose deleters call kernel32's CloseHandle: those hooks
|
||||
* can run before kernel32.dll is available, which is why
|
||||
* Kernel32ExportsSolver exists. NtClose is in ntdll and is always callable,
|
||||
* in the launcher process and inside XUL alike.
|
||||
*/
|
||||
class AutoHandle final {
|
||||
public:
|
||||
AutoHandle() : mHandle(nullptr) {}
|
||||
explicit AutoHandle(HANDLE aHandle) : mHandle(aHandle) {}
|
||||
~AutoHandle() { reset(); }
|
||||
|
||||
AutoHandle(AutoHandle&& aOther) : mHandle(aOther.mHandle) {
|
||||
aOther.mHandle = nullptr;
|
||||
}
|
||||
|
||||
AutoHandle& operator=(AutoHandle&& aOther) {
|
||||
if (this != &aOther) {
|
||||
reset();
|
||||
mHandle = aOther.mHandle;
|
||||
aOther.mHandle = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AutoHandle(const AutoHandle&) = delete;
|
||||
AutoHandle& operator=(const AutoHandle&) = delete;
|
||||
|
||||
HANDLE get() const { return mHandle; }
|
||||
explicit operator bool() const { return !!mHandle; }
|
||||
|
||||
void reset() {
|
||||
if (mHandle) {
|
||||
::NtClose(mHandle);
|
||||
mHandle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE mHandle;
|
||||
};
|
||||
|
||||
class AutoMappedView final {
|
||||
void* mView;
|
||||
|
||||
|
||||
@@ -33,6 +33,12 @@ static bool QueryStringValue(const void* aBlock, DWORD aTranslation,
|
||||
if (!::VerQueryValueW(aBlock, path.get(), (PVOID*)&lpBuffer, &len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// len includes the terminating NUL, so must be greater than 0 when valid.
|
||||
if (!lpBuffer || !len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
aResult.Assign(lpBuffer, (size_t)len - 1);
|
||||
return true;
|
||||
}
|
||||
@@ -79,7 +85,8 @@ bool ModuleVersionInfo::GetFromImage(const nsAString& aPath) {
|
||||
|
||||
VS_FIXEDFILEINFO* vInfo = nullptr;
|
||||
UINT vInfoLen = 0;
|
||||
if (::VerQueryValueW(verInfo.get(), L"\\", (LPVOID*)&vInfo, &vInfoLen)) {
|
||||
if (::VerQueryValueW(verInfo.get(), L"\\", (LPVOID*)&vInfo, &vInfoLen) &&
|
||||
vInfo && vInfoLen >= sizeof(VS_FIXEDFILEINFO)) {
|
||||
mFileVersion =
|
||||
VersionNumber(vInfo->dwFileVersionMS, vInfo->dwFileVersionLS);
|
||||
mProductVersion =
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
# include "mozilla/Maybe.h"
|
||||
# include "mozilla/RefPtr.h"
|
||||
# include "mozilla/TypedEnumBits.h"
|
||||
# include "mozilla/Variant.h"
|
||||
# include "mozilla/Vector.h"
|
||||
# include "mozilla/WinHeaderOnlyUtils.h"
|
||||
# include "mozilla/ipc/FileDescriptor.h"
|
||||
# include "nsCOMPtr.h"
|
||||
# include "nsHashKeys.h"
|
||||
# include "nsIFile.h"
|
||||
@@ -104,28 +104,12 @@ class ModuleRecord final {
|
||||
};
|
||||
|
||||
/**
|
||||
* This type holds module path data using one of two internal representations.
|
||||
* It may be created from either a nsTHashtable or a Vector, and may be
|
||||
* serialized from either representation into a common format over the wire.
|
||||
* Deserialization always uses the Vector representation.
|
||||
* The set of modules a child process wants trust information for. The child
|
||||
* asks about each distinct module once per batch, identifying it by a
|
||||
* read-only duplicate of the section handle it was mapped from, taken by the
|
||||
* DLL blocklist's NtMapViewOfSection hook.
|
||||
*/
|
||||
struct ModuleIdentifiers final {
|
||||
using SetType = nsTHashtable<nsStringCaseInsensitiveHashKey>;
|
||||
using VecType = Vector<nsString>;
|
||||
|
||||
Variant<SetType, VecType> mModuleNtPaths;
|
||||
|
||||
template <typename T>
|
||||
explicit ModuleIdentifiers(T&& aPaths)
|
||||
: mModuleNtPaths(AsVariant(std::forward<T>(aPaths))) {}
|
||||
|
||||
ModuleIdentifiers() : mModuleNtPaths(VecType()) {}
|
||||
|
||||
ModuleIdentifiers(const ModuleIdentifiers& aOther) = delete;
|
||||
ModuleIdentifiers(ModuleIdentifiers&& aOther) = default;
|
||||
ModuleIdentifiers& operator=(const ModuleIdentifiers&) = delete;
|
||||
ModuleIdentifiers& operator=(ModuleIdentifiers&&) = default;
|
||||
};
|
||||
using ModuleIdentifiers = nsTArray<mozilla::ipc::FileDescriptor>;
|
||||
|
||||
class ProcessedModuleLoadEvent final {
|
||||
public:
|
||||
@@ -391,64 +375,6 @@ struct ParamTraits<mozilla::ModulesMap> {
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ParamTraits<mozilla::ModuleIdentifiers> {
|
||||
typedef mozilla::ModuleIdentifiers paramType;
|
||||
|
||||
static void Write(MessageWriter* aWriter, const paramType& aParam) {
|
||||
aParam.mModuleNtPaths.match(
|
||||
[aWriter](const paramType::SetType& aSet) { WriteSet(aWriter, aSet); },
|
||||
[aWriter](const paramType::VecType& aVec) {
|
||||
WriteVector(aWriter, aVec);
|
||||
});
|
||||
}
|
||||
|
||||
static bool Read(MessageReader* aReader, paramType* aResult) {
|
||||
uint32_t len;
|
||||
if (!aReader->ReadUInt32(&len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// As noted in the comments for ModuleIdentifiers, we only deserialize using
|
||||
// the Vector representation.
|
||||
auto& vec = aResult->mModuleNtPaths.as<paramType::VecType>();
|
||||
if (!vec.reserve(len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint32_t idx = 0; idx < len; ++idx) {
|
||||
nsString str;
|
||||
if (!ReadParam(aReader, &str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!vec.emplaceBack(std::move(str))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
// NB: This function must write out the set in the same format as WriteVector
|
||||
static void WriteSet(MessageWriter* aWriter, const paramType::SetType& aSet) {
|
||||
aWriter->WriteUInt32(aSet.Count());
|
||||
for (const auto& key : aSet.Keys()) {
|
||||
WriteParam(aWriter, key);
|
||||
}
|
||||
}
|
||||
|
||||
// NB: This function must write out the vector in the same format as WriteSet
|
||||
static void WriteVector(MessageWriter* aWriter,
|
||||
const paramType::VecType& aVec) {
|
||||
aWriter->WriteUInt32(aVec.length());
|
||||
for (auto const& item : aVec) {
|
||||
WriteParam(aWriter, item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ParamTraits<mozilla::UntrustedModulesData> {
|
||||
typedef mozilla::UntrustedModulesData paramType;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "UntrustedModulesProcessor.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <aclapi.h>
|
||||
#include <psapi.h>
|
||||
|
||||
#include "GMPPlatform.h"
|
||||
#include "GMPServiceParent.h"
|
||||
@@ -12,6 +14,7 @@
|
||||
#include "mozilla/DebugOnly.h"
|
||||
#include "mozilla/dom/ContentChild.h"
|
||||
#include "mozilla/dom/ContentParent.h"
|
||||
#include "mozilla/FileUtilsWin.h"
|
||||
#include "mozilla/Likely.h"
|
||||
#include "mozilla/net/SocketProcessChild.h"
|
||||
#include "mozilla/net/SocketProcessParent.h"
|
||||
@@ -22,17 +25,175 @@
|
||||
#include "mozilla/RDDProcessManager.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/Telemetry.h"
|
||||
#include "mozilla/UniquePtrExtensions.h"
|
||||
#include "ModuleEvaluator.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsHashKeys.h"
|
||||
#include "nsIObserverService.h"
|
||||
#include "nsTHashtable.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "nsWindowsHelpers.h"
|
||||
#include "nsXULAppAPI.h"
|
||||
#include "private/prpriv.h" // For PR_GetThreadID
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
// NT paths are not bounded by MAX_PATH. This is the ceiling we are willing to
|
||||
// grow a path buffer to while resolving one.
|
||||
static const uint32_t kMaxNtPathLen = 0x8000;
|
||||
|
||||
/**
|
||||
* Returns true if aDosPath lives on a remote device.
|
||||
*/
|
||||
static bool IsRemoteFile(const nsAString& aDosPath) {
|
||||
// A UNC path is always remote.
|
||||
if (StringBeginsWith(aDosPath, u"\\\\"_ns)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (aDosPath.Length() < 3 || aDosPath[1] != u':') {
|
||||
// Some shape we do not recognise; do not guess.
|
||||
return true;
|
||||
}
|
||||
|
||||
// GetDriveTypeW also catches a drive letter mapped to a network share.
|
||||
const wchar_t root[] = {static_cast<wchar_t>(aDosPath[0]), L':', L'\\',
|
||||
L'\0'};
|
||||
UINT driveType = ::GetDriveTypeW(root);
|
||||
return driveType == DRIVE_REMOTE || driveType == DRIVE_UNKNOWN ||
|
||||
driveType == DRIVE_NO_ROOT_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the file at aDosPath carries a mandatory integrity label
|
||||
* below medium. A file with no label ACE is medium by default, which is the
|
||||
* most common case, and is accepted.
|
||||
*/
|
||||
static bool IsBelowMediumIntegrityFile(const nsAString& aDosPath) {
|
||||
PACL sacl = nullptr;
|
||||
PSECURITY_DESCRIPTOR rawSd = nullptr;
|
||||
nsAutoString path(aDosPath);
|
||||
if (::GetNamedSecurityInfoW(reinterpret_cast<wchar_t*>(path.BeginWriting()),
|
||||
SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION,
|
||||
nullptr, nullptr, nullptr, &sacl,
|
||||
&rawSd) != ERROR_SUCCESS) {
|
||||
// Treat errors as untrustworthy.
|
||||
return true;
|
||||
}
|
||||
|
||||
UniquePtr<void, LocalFreeDeleter> sd(rawSd);
|
||||
|
||||
if (!sacl) {
|
||||
// No label: medium by default.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (WORD i = 0; i < sacl->AceCount; ++i) {
|
||||
VOID* rawAce = nullptr;
|
||||
if (!::GetAce(sacl, i, &rawAce)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto* header = static_cast<ACE_HEADER*>(rawAce);
|
||||
if (header->AceType != SYSTEM_MANDATORY_LABEL_ACE_TYPE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* labelAce = static_cast<SYSTEM_MANDATORY_LABEL_ACE*>(rawAce);
|
||||
auto* sid = reinterpret_cast<PSID>(&labelAce->SidStart);
|
||||
PUCHAR subAuthorityCount = ::GetSidSubAuthorityCount(sid);
|
||||
if (!subAuthorityCount || !*subAuthorityCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD* rid = ::GetSidSubAuthority(sid, *subAuthorityCount - 1);
|
||||
if (!rid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return *rid < SECURITY_MANDATORY_MEDIUM_RID;
|
||||
}
|
||||
|
||||
// A SACL with no label ACE is also medium by default.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ValidateAndResolveModuleSection(const ipc::FileDescriptor& aSection,
|
||||
nsAString& aOutNtPath) {
|
||||
aOutNtPath.Truncate();
|
||||
|
||||
if (!aSection.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UniqueFileHandle section(aSection.ClonePlatformHandle());
|
||||
if (!section) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recover the backing file's path by mapping a view.
|
||||
nsAutoString resolved;
|
||||
{
|
||||
PVOID view = ::MapViewOfFile(section.get(), FILE_MAP_READ, 0, 0, 0);
|
||||
if (!view) {
|
||||
return false;
|
||||
}
|
||||
auto unmapView = MakeScopeExit([&]() { ::UnmapViewOfFile(view); });
|
||||
|
||||
// A module is an IMAGE section.
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (!::VirtualQuery(view, &mbi, sizeof(mbi)) || mbi.Type != MEM_IMAGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetMappedFileNameW reports the NT device form
|
||||
// (\Device\HarddiskVolumeN\...), which is what the child's loader observer
|
||||
// records via NtQueryVirtualMemory(..., MemorySectionName) and what
|
||||
// ModuleRecord expects, so the two are directly comparable.
|
||||
for (uint32_t bufLen = MAX_PATH; bufLen <= kMaxNtPathLen; bufLen *= 2) {
|
||||
nsAutoString buf;
|
||||
if (!buf.SetLength(bufLen, fallible)) {
|
||||
break;
|
||||
}
|
||||
|
||||
DWORD charsWritten = ::GetMappedFileNameW(
|
||||
::GetCurrentProcess(), view,
|
||||
reinterpret_cast<wchar_t*>(buf.BeginWriting()), bufLen);
|
||||
if (!charsWritten) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (charsWritten >= bufLen - 1) {
|
||||
// May have been truncated; retry with more room.
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.SetLength(charsWritten);
|
||||
resolved = buf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolved.IsEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The remaining checks are about the file, so they need its DOS path.
|
||||
nsAutoString dosPath;
|
||||
if (!NtPathToDosPath(resolved, dosPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject a file on a remote device, or one carrying a mandatory integrity
|
||||
// label below medium.
|
||||
if (IsRemoteFile(dosPath) || IsBelowMediumIntegrityFile(dosPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
aOutNtPath = resolved;
|
||||
return true;
|
||||
}
|
||||
|
||||
class MOZ_RAII BackgroundPriorityRegion final {
|
||||
public:
|
||||
BackgroundPriorityRegion()
|
||||
@@ -808,9 +969,10 @@ UntrustedModulesProcessor::ProcessModuleLoadQueueChildProcess(
|
||||
NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
|
||||
}
|
||||
|
||||
nsTHashtable<nsStringCaseInsensitiveHashKey> moduleNtPathSet;
|
||||
nsTHashtable<nsStringCaseInsensitiveHashKey> alreadyAdded;
|
||||
ModuleIdentifiers moduleIdents;
|
||||
|
||||
// Build a set of modules to be processed by the parent
|
||||
// Build the set of modules to be processed by the parent.
|
||||
for (UnprocessedModuleLoadInfoContainer* container : loadsToProcess) {
|
||||
glue::EnhancedModuleLoadInfo& entry = container->mInfo;
|
||||
|
||||
@@ -819,7 +981,22 @@ UntrustedModulesProcessor::ProcessModuleLoadQueueChildProcess(
|
||||
NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
|
||||
}
|
||||
|
||||
moduleNtPathSet.PutEntry(entry.mNtLoadInfo.mSectionName.AsString());
|
||||
if (!entry.mNtLoadInfo.mSectionHandle) {
|
||||
// No section handle, so nothing the parent can verify.
|
||||
continue;
|
||||
}
|
||||
|
||||
nsDependentString sectionName(entry.mNtLoadInfo.mSectionName.AsString());
|
||||
if (!alreadyAdded.EnsureInserted(sectionName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ipc::FileDescriptor section(entry.mNtLoadInfo.mSectionHandle.get());
|
||||
if (!section.IsValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
moduleIdents.AppendElement(std::move(section));
|
||||
}
|
||||
|
||||
if (!IsReadyForBackgroundProcessing()) {
|
||||
@@ -827,14 +1004,11 @@ UntrustedModulesProcessor::ProcessModuleLoadQueueChildProcess(
|
||||
NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
|
||||
}
|
||||
|
||||
MOZ_ASSERT(!moduleNtPathSet.IsEmpty());
|
||||
if (moduleNtPathSet.IsEmpty()) {
|
||||
if (moduleIdents.IsEmpty()) {
|
||||
// Nothing to process
|
||||
return GetModulesTrustPromise::CreateAndResolve(Nothing(), __func__);
|
||||
}
|
||||
|
||||
ModuleIdentifiers moduleNtPaths(std::move(moduleNtPathSet));
|
||||
|
||||
if (!IsReadyForBackgroundProcessing()) {
|
||||
return GetModulesTrustPromise::CreateAndReject(
|
||||
NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
|
||||
@@ -843,9 +1017,9 @@ UntrustedModulesProcessor::ProcessModuleLoadQueueChildProcess(
|
||||
RefPtr<UntrustedModulesProcessor> self(this);
|
||||
|
||||
auto invoker = [self = std::move(self),
|
||||
moduleNtPaths = std::move(moduleNtPaths),
|
||||
moduleIdentifiers = std::move(moduleIdents),
|
||||
priority = aPriority]() mutable {
|
||||
return self->SendGetModulesTrust(std::move(moduleNtPaths), priority);
|
||||
return self->SendGetModulesTrust(std::move(moduleIdentifiers), priority);
|
||||
};
|
||||
|
||||
RefPtr<GetModulesTrustPromise::Private> p(
|
||||
@@ -1009,7 +1183,7 @@ RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrustInternal(
|
||||
return GetModulesTrustInternal(std::move(aModIdents));
|
||||
}
|
||||
|
||||
// For each module in |aModIdents|, evaluate its trustworthiness and only send
|
||||
// For each module in aModIdents, evaluate its trustworthiness and only send
|
||||
// ModuleRecords for untrusted modules back to the child process. We also save
|
||||
// XUL's ModuleRecord so that the child process may report XUL's load time.
|
||||
RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrustInternal(
|
||||
@@ -1028,18 +1202,24 @@ RefPtr<ModulesTrustPromise> UntrustedModulesProcessor::GetModulesTrustInternal(
|
||||
return ModulesTrustPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
|
||||
}
|
||||
|
||||
for (auto& resolvedNtPath :
|
||||
aModIdents.mModuleNtPaths.as<ModuleIdentifiers::VecType>()) {
|
||||
for (auto& section : aModIdents) {
|
||||
if (!IsReadyForBackgroundProcessing()) {
|
||||
return ModulesTrustPromise::CreateAndReject(
|
||||
NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
|
||||
}
|
||||
|
||||
MOZ_ASSERT(!resolvedNtPath.IsEmpty());
|
||||
if (resolvedNtPath.IsEmpty()) {
|
||||
// The authoritative path, derived from the handle.
|
||||
nsAutoString resolvedNtPath;
|
||||
if (!ValidateAndResolveModuleSection(section, resolvedNtPath) ||
|
||||
resolvedNtPath.IsEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsReadyForBackgroundProcessing()) {
|
||||
return ModulesTrustPromise::CreateAndReject(
|
||||
NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
|
||||
}
|
||||
|
||||
RefPtr<ModuleRecord> module(GetOrAddModuleRecord(modEval, resolvedNtPath));
|
||||
if (!module) {
|
||||
// We failed to obtain trust information.
|
||||
|
||||
@@ -23,6 +23,21 @@ namespace mozilla {
|
||||
|
||||
class ModuleEvaluator;
|
||||
|
||||
/**
|
||||
* Decides whether a section handle a child process sent us is one we should
|
||||
* evaluate and, if so, derives the module's path from it. We require the
|
||||
* handle to refer to a module file, on a local disk, that is not
|
||||
* low-integrity. The requirements reconstruct, on the parent side, the limits
|
||||
* the sandbox mitigations MITIGATION_IMAGE_LOAD_NO_REMOTE and
|
||||
* MITIGATION_IMAGE_LOAD_NO_LOW_LABEL enforce, since the section handle can be
|
||||
* obtained even if the child process isn't permitted to load it as a module.
|
||||
*
|
||||
* Declared here so that gtest can exercise it directly. Nothing outside of the
|
||||
* UntrustedModules implementation and tests should call it.
|
||||
*/
|
||||
bool ValidateAndResolveModuleSection(const ipc::FileDescriptor& aSection,
|
||||
nsAString& aOutNtPath);
|
||||
|
||||
using UntrustedModulesPromise =
|
||||
MozPromise<Maybe<UntrustedModulesData>, nsresult, true>;
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ void DllServices::DisableFull() {
|
||||
}
|
||||
|
||||
RefPtr<ModulesTrustPromise> DllServices::GetModulesTrust(
|
||||
ModuleIdentifiers&& aModIdents, bool aRunAtNormalPriority) {
|
||||
nsTArray<ipc::FileDescriptor>&& aModIdents, bool aRunAtNormalPriority) {
|
||||
if (!mUntrustedModulesProcessor) {
|
||||
return ModulesTrustPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED,
|
||||
__func__);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "mozilla/Maybe.h"
|
||||
#include "mozilla/MozPromise.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "nsTArray.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
@@ -18,7 +19,10 @@ class UntrustedModulesProcessor;
|
||||
using UntrustedModulesPromise =
|
||||
MozPromise<Maybe<UntrustedModulesData>, nsresult, true>;
|
||||
|
||||
struct ModuleIdentifiers;
|
||||
namespace ipc {
|
||||
class FileDescriptor;
|
||||
} // namespace ipc
|
||||
|
||||
class ModulesMapResult;
|
||||
|
||||
using ModulesTrustPromise = MozPromise<ModulesMapResult, nsresult, true>;
|
||||
@@ -37,8 +41,8 @@ class DllServices final : public glue::DllServices {
|
||||
|
||||
RefPtr<UntrustedModulesPromise> GetUntrustedModulesData();
|
||||
|
||||
RefPtr<ModulesTrustPromise> GetModulesTrust(ModuleIdentifiers&& aModIdents,
|
||||
bool aRunAtNormalPriority);
|
||||
RefPtr<ModulesTrustPromise> GetModulesTrust(
|
||||
nsTArray<ipc::FileDescriptor>&& aModIdents, bool aRunAtNormalPriority);
|
||||
|
||||
private:
|
||||
DllServices() = default;
|
||||
|
||||
@@ -28,6 +28,7 @@ struct ModuleLoadInfo final {
|
||||
mThreadId(nt::RtlGetCurrentThreadId()),
|
||||
mRequestedDllName(aRequestedDllName),
|
||||
mBaseAddr(nullptr),
|
||||
mSectionHandleUnavailable(false),
|
||||
mStatus(Status::Loaded),
|
||||
mIsDependent(false) {
|
||||
# if defined(IMPL_MFBT)
|
||||
@@ -49,6 +50,7 @@ struct ModuleLoadInfo final {
|
||||
mThreadId(nt::RtlGetCurrentThreadId()),
|
||||
mSectionName(std::move(aSectionName)),
|
||||
mBaseAddr(aBaseAddr),
|
||||
mSectionHandleUnavailable(false),
|
||||
mStatus(aLoadStatus),
|
||||
mIsDependent(aIsDependent) {
|
||||
# if defined(IMPL_MFBT)
|
||||
@@ -161,6 +163,18 @@ struct ModuleLoadInfo final {
|
||||
nt::AllocatedUnicodeString mSectionName;
|
||||
// The base address of the module's mapped section
|
||||
const void* mBaseAddr;
|
||||
// A read-only duplicate of the section this module was mapped from, taken by
|
||||
// the NtMapViewOfSection hook.
|
||||
//
|
||||
// Null for any load the NtMapViewOfSection hook did not take a handle for.
|
||||
// See mSectionHandleUnavailable for how to tell the two reasons apart.
|
||||
nt::AutoHandle mSectionHandle;
|
||||
// Set when the hook did reach this load and tried to duplicate the section,
|
||||
// but the duplication failed. This is used to distinguish that case from
|
||||
// the case where mSectionHandle is null for the loads the hook deliberately
|
||||
// skips (e.g. non-IMAGEs, low-integrity modules, etc). The distinction is
|
||||
// recorded in telemetry.
|
||||
bool mSectionHandleUnavailable;
|
||||
// If the module was successfully loaded, stack trace of the DLL load request
|
||||
Vector<PVOID, 0, nt::RtlAllocPolicy> mBacktrace;
|
||||
// The status of DLL load
|
||||
|
||||
Reference in New Issue
Block a user