[+] New Linux futex based IPCMutex to better ensure robust breakage

This commit is contained in:
Reece Wilson 2022-08-07 05:18:34 +01:00
parent c730d4fc58
commit 5dd2be4763
6 changed files with 448 additions and 73 deletions

View File

@ -32,6 +32,10 @@
#include "Grug/Grug.hpp"
#include "Threading/Sleep.hpp"
#if defined(AURORA_IS_LINUX_DERIVED)
void LinuxSuperSecretIOTick();
#endif
static void Init()
{
#if defined(AURORA_PLATFORM_WIN32)
@ -40,6 +44,7 @@ static void Init()
Crypto::InitCrypto();
Aurora::RNG::Init();
Aurora::Threading::InitSleep();
Aurora::Process::InitProcessMap();
Aurora::SWInfo::InitSwInfo();
@ -56,7 +61,6 @@ static void Init()
Aurora::Locale::Init();
Aurora::CmdLine::Init();
Aurora::Processes::Init();
Aurora::RNG::Init();
Aurora::Hashing::InitHashing();
Aurora::Async::InitAsync();
}
@ -64,6 +68,10 @@ static void Init()
static void Pump()
{
Aurora::Console::Pump();
#if defined(AURORA_IS_LINUX_DERIVED)
::LinuxSuperSecretIOTick();
#endif
}
static void Deinit()

View File

@ -18,6 +18,11 @@
#include <Source/RuntimeInternal.hpp>
#include <Source/Console/Flusher.hpp>
#if defined(AURORA_IS_LINUX_DERIVED)
void LinuxSuperSecretIOTick();
void LinuxSuperSecretFuckGlibc();
#endif
namespace Aurora::Grug
{
static const auto kGrugSleepMs = 100;
@ -36,6 +41,10 @@ namespace Aurora::Grug
// grug require only 1 strand
static void GrugWorld()
{
#if defined(AURORA_IS_LINUX_DERIVED)
LinuxSuperSecretFuckGlibc();
#endif
// grug surive first night
SlowStartupTasks();
@ -82,6 +91,11 @@ namespace Aurora::Grug
// grug sleep 100ms
AuThreading::Sleep(kGrugSleepMs);
}
#if defined(AURORA_IS_LINUX_DERIVED)
::LinuxSuperSecretIOTick();
#endif
}
}

View File

@ -10,19 +10,23 @@
#include "IPC.hpp"
#include "IPCHandle.hpp"
#include "IPCMutexFutex.Linux.hpp"
#include "IPCPrimitives.Linux.hpp"
#include "IPCMemory.Unix.hpp"
// LINUX SYSCALL APIS
#include <linux/futex.h>
#include <syscall.h>
#include <syscall.h>
// INTERNAL UTILS
// ...IO / FD SHARING
#include <Source/IO/UNIX/FDIpcServer.hpp>
// ...TIME UTILS
#include <Source/Time/Time.hpp>
////////////////////////////////////////////////////////////////////////////////////
// SysCalls
// SYSCALLS
////////////////////////////////////////////////////////////////////////////////////
#if !defined(FUTEX_OWNER_DIED)
#define FUTEX_OWNER_DIED 0x40000000
#endif
static int futex(uint32_t *uaddr, int futex_op, uint32_t val,
const struct timespec *timeout, /* or: uint32_t val2 */
uint32_t *uaddr2, uint32_t val3)
@ -35,12 +39,17 @@ static int futex_wait(uint32_t *addr, uint32_t expected)
return futex(addr, FUTEX_WAIT, expected, 0, 0, 0);
}
static int futex_wait(uint32_t *addr, uint32_t expected, const struct timespec *timeout)
{
return futex(addr, FUTEX_WAIT, expected, timeout, 0, 0);
}
static int futex_wake(uint32_t *addr, uint32_t nthreads)
{
return futex(addr, FUTEX_WAKE, nthreads, 0, 0, 0);
}
static long set_robust_list(struct procmon_head *head, size_t len)
static long set_robust_list(struct robust_list_head *head, size_t len)
{
return syscall(SYS_set_robust_list, head, len);
}
@ -55,33 +64,56 @@ static long get_robust_list(int pid, struct robust_list_head **head_ptr, size_t
// CONSTANTS
////////////////////////////////////////////////////////////////////////////////////
static const AuUInt8 kMaxFutexes = 128;
static const AuUInt32 kFutexArraySize = AuUInt32(kMaxFutexes) * 4;
static const AuUInt8 kMaxFutexes = 128;
static const AuUInt32 kFutexArraySize = AuUInt32(kMaxFutexes) * (sizeof(AuUInt64) * 3);
static const AuUInt32 kFutexValueOwner = 0x80000000;
static const AuUInt32 kFutexValueLocked = kFutexValueOwner | 1;
static const AuUInt32 kFutexValueUnlocked = kFutexValueOwner;
static const AuUInt32 kFutexIsValid = 0x80000000;
static const AuUInt32 kFutexIsDead = 0x40000000;
static const AuUInt32 kFutexIsHasOwner = 0x20000000;
//static const AuUInt32 kFutexValueLocked = kFutexIsHasOwner | kFutexIsValid | 1;
static const AuUInt32 kFutexValueUnlocked = kFutexIsHasOwner;
static const AuUInt32 kFutexValueNULL = 0;
static AuUInt32 gConstNull = 0;
static const AuUInt32 kFutexValueMask = kFutexValueLocked;
static const AuUInt32 kFutexValueMaskOwner = kFutexValueOwner | FUTEX_OWNER_DIED;
////////////////////////////////////////////////////////////////////////////////////
// VARIABLES
////////////////////////////////////////////////////////////////////////////////////
struct FutexObject
{
union
{
AuUInt64 maxWord;
void *nextPtr;
};
union
{
AuUInt32 futex;
AuUInt64 futexPadded;
};
};
static_assert(sizeof(FutexObject) == 16);
static AuThreadPrimitives::SpinLock gLock;
static AuSPtr<AuIPC::IPCSharedMemory> gFutexSharedMemory;
static AuUInt32 *gFutexArray;
static FutexObject *gFutexArray;
static bool gFutexInit {};
static AuIOIPC::IMutexClosedHook * gFutexCallbacks[kMaxFutexes];
////////////////////////////////////////////////////////////////////////////////////
// IMPLEMENTATION
////////////////////////////////////////////////////////////////////////////////////
static void InitFutexAPI()
{
static AuThreadPrimitives::SpinLock lock;
AU_LOCK_GUARD(lock);
AU_LOCK_GUARD(gLock);
if (AuExchange(gFutexInit, true))
{
@ -91,24 +123,91 @@ static void InitFutexAPI()
gFutexSharedMemory = AuIOIPC::NewSharedMemory(kFutexArraySize);
SysAssert(gFutexSharedMemory);
gFutexArray = gFutexSharedMemory->GetMemory().Begin<AuUInt32>();
gFutexArray = gFutexSharedMemory->GetMemory().Begin<FutexObject>();
}
struct MagicFutexLinkHeader : robust_list_head
{
AuUInt32 linkCount {};
};
struct FutexContext
{
bool bInit {};
// https://github.com/bminor/glibc/blob/78fb88827362fbd2cc8aa32892ae5b015106e25c/sysdeps/nptl/dl-tls_init_tp.c#L93
// Every thread should have an arraylist head
robust_list_head *head;
AuThreadPrimitives::SpinLock lock;
AuUInt32 tid {};
bool Init();
bool SetGrugSelf();
bool Link(AuUInt32 *ptr);
bool Unlink(AuUInt32 *ptr);
MagicFutexLinkHeader futexArrayHeader {};
};
static FutexContext gFutexContext;
void LinuxSuperSecretFuckGlibc();
static void LinuxLockFutex(AuUInt32 *futex);
void LinuxSuperSecretFuckGlibc()
{
// Fun: You have to define an entry-relative-offset within a linked list header, hard coding an ABI, for every user
// program in the tasks/processes address space. Low-iq linux kernel comments state "tehe" check with the glibc folk.
//
// Counter, they aren't "folk," they aren't even real people.
// Fuck you, and fuck your buzzword mutex that hasn't evolved into a stable API since its initial hack of an
// implementation 20 years ago. I don't want to make a fucking array of pthreads (40+ bytes for a single atomic).
// I don't want to fucking define a system-wide abi, matching ONE OF MANY fucking glibc ABIs, any given process
// could be arbitrarily linked against.
//
// As a wise man once said, "Linus says fuck you Nvidia, I say f............! That's what I say."
// ---------------------------------------------------------------------------------------------------------------------
// Unrelated, here's what FreeBSD says about the UMUTEX_ROBUST list set operation:
// Note that if any 32-bit ABI compatibility is being requested, then care
// must be taken with robust lists. A single thread may not mix 32-bit com-
// patible robust lists with native robust lists. The first
// UMTX_OP_ROBUST_LISTS call in a given thread determines which ABI that
// thread will use for robust lists going forward.
//
// Code: https://github.com/freebsd/freebsd-src/blob/27a9392d543933f1aaa4e4ddae2a1585a72db1b2/sys/kern/kern_umtx.c#L4150
//
// ...FreeBSD just copied the fucking glibc tards between 2012-present didn't they? I bet they were "inspired" by glibcs
// nptl implementation on Linux, and thought hey, let's just copy this so we can have a functional CRT with shared pt-mutexes
// ---------------------------------------------------------------------------------------------------------------------
// I'll just have to use a dedicated watcher process that'll teardown a robust list of only standard
// entries limited to the Aurora IPC origin. Will use the grug thread for this bc i dont foresee user
// code locking shared pthread-mutexs under the few user-interfaces grug may call out to.
//
SysAssert(gFutexContext.SetGrugSelf());
}
bool FutexContext::SetGrugSelf()
{
// Specify the magic word to use...
this->tid = gettid();
// Update the header
this->futexArrayHeader.list.next = &this->futexArrayHeader.list;
this->futexArrayHeader.futex_offset = 8;
this->futexArrayHeader.list_op_pending = NULL;
// Update TLS
if (::set_robust_list(AuReinterpretCast<robust_list_head *>(&this->futexArrayHeader.list), sizeof(robust_list_head)) != 0)
{
SysPushErrorIO("Set robust list failed");
return false;
}
return true;
}
bool FutexContext::Init()
{
AU_LOCK_GUARD(this->lock);
if (AuExchange(this->bInit, true))
{
return true;
@ -116,14 +215,70 @@ bool FutexContext::Init()
InitFutexAPI();
return this->bInit;
return true;
}
static thread_local FutexContext tlsFutexContext;
bool FutexContext::Link(AuUInt32 *ptr)
{
if (!ptr)
{
return false;
}
if (!Init())
{
return false;
}
AU_LOCK_GUARD(this->lock);
auto temp = AuReinterpretCast<FutexObject *>(AuUInt(ptr) - offsetof(FutexObject, futex));
auto firstElement = futexArrayHeader.list.next;
temp->nextPtr = firstElement;
futexArrayHeader.list_op_pending = nullptr;
futexArrayHeader.list.next = (robust_list *)temp;
futexArrayHeader.linkCount++;
return true;
}
bool FutexContext::Unlink(AuUInt32 *ptr)
{
if (!ptr)
{
return false;
}
if (!Init())
{
return false;
}
AU_LOCK_GUARD(this->lock);
FutexObject *cur { (FutexObject *)this->futexArrayHeader.list.next };
FutexObject *prevLink { cur };
for (int i = 0; i < this->futexArrayHeader.linkCount; i++)
{
if (&cur->futex != ptr)
{
prevLink = cur;
cur = (FutexObject *)cur->nextPtr;
continue;
}
prevLink->nextPtr = cur->nextPtr;
this->futexArrayHeader.linkCount--;
return true;
}
return false;
}
static AuUInt8 AllocateFutex()
{
tlsFutexContext.Init();
gFutexContext.Init();
if (!gFutexArray)
{
return 255;
@ -133,8 +288,13 @@ static AuUInt8 AllocateFutex()
i < kMaxFutexes;
i++)
{
if (AuAtomicCompareExchange<AuUInt32>(&gFutexArray[i], kFutexValueUnlocked, AuUInt32(0)) == 0)
if (AuAtomicCompareExchange<AuUInt32>(&gFutexArray[i].futex, kFutexValueUnlocked, kFutexValueNULL) == kFutexValueNULL)
{
if (gFutexCallbacks[i])
{
continue;
}
return i;
}
}
@ -144,50 +304,119 @@ static AuUInt8 AllocateFutex()
static bool TryReleaseFutex(AuUInt8 index)
{
auto &cb = gFutexCallbacks[index];
if (cb)
auto old = gFutexCallbacks[index];
auto oldState = gFutexArray[index].futexPadded;
if (AuAtomicCompareExchange<AuUInt32>(&gFutexArray[index].futex, kFutexValueUnlocked, oldState) != oldState)
{
if (!cb->OnClosed())
{
return false;
}
return false;
}
if ((old) &&
(!old->OnClosed()))
{
gFutexArray[index].futexPadded = oldState;
return false;
}
cb = nullptr;
gFutexArray[index] = 0;
gFutexCallbacks[index] = nullptr;
return true;
}
static void FreeFutex(AuUInt8 index)
{
gFutexArray[index] = 0;
gFutexArray[index].futexPadded = 0;
gFutexCallbacks[index] = nullptr;
}
static void LinuxAddRobustFutexSlow(AuUInt32 *futex)
{
tlsFutexContext.Init();
SysAssert(gFutexContext.Init());
SysAssert(gFutexContext.Link(futex));
}
static void LinuxRemoveRobustFutexSlow(AuUInt32 *futex)
{
tlsFutexContext.Init();
SysAssert(gFutexContext.Init());
if (!gFutexContext.Unlink(futex))
{
SysPushErrorIO("Unlink futex error: {}", fmt::ptr(futex));
}
}
static void LinuxLockFutex(AuUInt32 *futex)
static bool LinuxLockFutex(AuUInt32 *futex, AuUInt32 timeout)
{
bool bContended;
struct timespec tspec;
AuUInt32 value = gFutexContext.tid;
if (timeout)
{
AuTime::ms2tsabs(&tspec, timeout);
}
do
{
bContended = AuAtomicCompareExchange<AuUInt32>(futex, value, kFutexValueUnlocked) != kFutexValueUnlocked;
if (bContended)
{
int res = ::futex_wait(futex, kFutexValueUnlocked, timeout ? &tspec : nullptr);
if (res < 0)
{
if (res != -EAGAIN)
{
SysPushErrorIO("FUTEX ERROR: {}", res);
return false;
}
else
{
//EAGAIN
bContended = true;
}
}
else
{
// SUCCESS
bContended = false;
}
}
}
while (bContended);
::LinuxAddRobustFutexSlow(futex);
return true;
}
static void LinuxUnlockFutex(AuUInt32 *futex)
static bool LinuxTryLockFutex(AuUInt32 *futex)
{
if (AuAtomicCompareExchange<AuUInt32>(futex,
gFutexContext.tid,
kFutexValueUnlocked) != kFutexValueUnlocked)
{
return false;
}
LinuxAddRobustFutexSlow(futex);
return true;
}
static void LinuxSuperSecretIOTick()
static bool LinuxUnlockFutex(AuUInt32 *futex)
{
LinuxRemoveRobustFutexSlow(futex);
if (AuAtomicCompareExchange<AuUInt32>(futex,
kFutexValueUnlocked,
gFutexContext.tid) != gFutexContext.tid)
{
return false;
}
::futex_wake(futex, 1);
return true;
}
void LinuxSuperSecretIOTick()
{
if (!gFutexInit)
{
@ -199,20 +428,32 @@ static void LinuxSuperSecretIOTick()
return;
}
static Aurora::Utility::RateLimiter gLimiter;
if (!gLimiter.nextTriggerTime)
{
gLimiter.noCatchUp = true;
gLimiter.SetNextStep(AuMSToNS<AuUInt64>(3'000));
}
if (!gLimiter.CheckExchangePass())
{
return;
}
for (AuUInt32 i = 0;
i < kMaxFutexes;
i++)
{
if (gFutexArray[i] == FUTEX_OWNER_DIED)
auto val = gFutexArray[i].futex;
val &= ~(0x3fffffff);
if ((val & kFutexIsDead) != 0)
{
TryReleaseFutex(i);
::TryReleaseFutex(i);
}
}
}
#include "IPCPrimitives.Linux.hpp"
#include "IPCMemory.Unix.hpp"
namespace Aurora::IO::IPC
{
static AuThreadPrimitives::SpinLock gLock;
@ -222,7 +463,10 @@ namespace Aurora::IO::IPC
// Mutexes
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
IPCMutexProxy::IPCMutexProxy(AuUInt32 index) : mutex_(), bOwned(true), index_(index)
IPCMutexProxy::IPCMutexProxy(AuUInt32 index) :
mutex_(),
bOwned(true),
index_(index)
{
if (this->mutex_.HasValidHandle())
{
@ -237,6 +481,7 @@ namespace Aurora::IO::IPC
}
this->mem_ = gFutexSharedMemory;
this->mutex_.bNoAutoRel = true;
}
IPCMutexProxy::IPCMutexProxy(int handle, AuSPtr<IPCSharedMemory> mem, AuUInt32 index) :
@ -255,6 +500,8 @@ namespace Aurora::IO::IPC
this->mutex_.~LSMutex();
}
}
this->mutex_.bNoAutoRel = true;
}
IPCMutexProxy::~IPCMutexProxy()
@ -263,23 +510,109 @@ namespace Aurora::IO::IPC
if (this->bOwned)
{
FreeFutex(this->index_);
::FreeFutex(this->index_);
}
}
AuUInt32 *IPCMutexProxy::GetFutex()
{
return &this->mem_->GetMemory().Begin<FutexObject>()[this->index_].futex;
}
bool IPCMutexProxy::Unlock()
{
return this->mutex_.Unlock();
auto futex = this->GetFutex();
if (!futex)
{
return false;
}
if (!::LinuxUnlockFutex(futex))
{
return false;
}
SysAssert(this->mutex_.Unlock());
this->leakSelf_.reset();
return true;
}
bool IPCMutexProxy::IsSignaled()
{
return this->mutex_.IsSignaled();
auto futex = this->GetFutex();
if (!futex)
{
return false;
}
if (!::LinuxTryLockFutex(futex))
{
return false;
}
if (!this->mutex_.IsSignaled())
{
::LinuxUnlockFutex(futex);
return false;
}
this->leakSelf_ = AuSharedFromThis();
return true;
}
bool IPCMutexProxy::WaitOn(AuUInt32 timeout)
{
return this->mutex_.WaitOn(timeout);
auto futex = this->GetFutex();
if (!futex)
{
return false;
}
if (!::LinuxLockFutex(futex, timeout))
{
return false;
}
if (!this->mutex_.IsSignaled())
{
::LinuxUnlockFutex(futex);
return false;
}
this->leakSelf_ = AuSharedFromThis();
return true;
}
bool IPCMutexProxy::OnClosed()
{
auto futex = this->GetFutex();
if (this->pMutexClosedHook)
{
if (!this->pMutexClosedHook->OnClosed())
{
return false;
}
}
if (futex)
{
if (*futex == kFutexIsDead)
{
this->mutex_.Unlock();
}
// This atomic is dumb and makes no sense
if (AuAtomicCompareExchange<AuUInt32>(futex, kFutexValueUnlocked, kFutexIsDead) != kFutexIsDead)
{
return false;
}
}
return true;
}
Loop::ELoopSource IPCMutexProxy::GetType()
@ -301,7 +634,7 @@ namespace Aurora::IO::IPC
AUKN_SYM AuSPtr<IPCMutex> NewMutex()
{
auto futex = AllocateFutex();
auto futex = ::AllocateFutex();
if (futex == 255)
{
return {};
@ -404,7 +737,7 @@ namespace Aurora::IO::IPC
return {};
}
auto mem = decodedHandle.GetToken(IPC::EIPCHandleType::eIPCMemory, 0);
auto mem = decodedHandle.GetToken(IPC::EIPCHandleType::eIPCMemory, 1);
if (!mem)
{
SysPushErrorParseError("Invalid handle: {}", handle);
@ -413,12 +746,4 @@ namespace Aurora::IO::IPC
return ImportMutexEx(val->token, mem->token, val->token.word);
}
}
namespace Aurora::Grug
{
void LinuxSuperSecretIOTick()
{
::LinuxSuperSecretIOTick();
}
}

View File

@ -10,11 +10,8 @@ namespace Aurora::IO::IPC
virtual bool OnClosed() = 0;
};
// IPC::IPCHandle handle;
struct IPCPipeImpl;
struct IPCMutexProxy : IPCMutex, Loop::ILoopSourceEx
struct IPCMutexProxy : IPCMutex, Loop::ILoopSourceEx, AuEnableSharedFromThis<IPCMutexProxy>, IMutexClosedHook
{
IPCMutexProxy(AuUInt32 index);
IPCMutexProxy(int handle, AuSPtr<IPCSharedMemory> mem, AuUInt32 index);
@ -22,6 +19,8 @@ namespace Aurora::IO::IPC
PROXY_INTERNAL_INTERFACE(mutex_)
bool OnClosed() override;
bool Unlock() override;
bool IsSignaled() override;
@ -29,12 +28,18 @@ namespace Aurora::IO::IPC
Loop::ELoopSource GetType() override;
AuString ExportToString() override;
AuUInt32 *GetFutex();
IMutexClosedHook *pMutexClosedHook {};
private:
bool bOwned {};
IPCToken token_;
AuSPtr<IPCSharedMemory> mem_;
AuUInt32 index_;
Loop::LSMutex mutex_;
AuSPtr<void> leakSelf_;
friend IPCPipeImpl;
};

View File

@ -24,6 +24,13 @@ namespace Aurora::IO::Loop
LSMutex::~LSMutex()
{
if ((!this->bOwns) &&
(!this->bNoAutoRel /*must the remote grug clean up the IPC mutex? lets not hit a double unlock assert...*/))
{
SysPushErrorIO("Mutex owned by calling process during destruction... Forcefully unlocking...");
Unlock();
}
if ((this->handle != 0) &&
(this->handle != -1))
{
@ -44,7 +51,12 @@ namespace Aurora::IO::Loop
bool LSMutex::Unlock()
{
AuUInt64 plsNoOverflow {1};
return ::write(this->handle, &plsNoOverflow, sizeof(plsNoOverflow)) == 8;
bool ok = ::write(this->handle, &plsNoOverflow, sizeof(plsNoOverflow)) == 8;
if (ok)
{
this->bOwns = false;
}
return ok;
}
bool LSMutex::IsSignaled()
@ -60,8 +72,17 @@ namespace Aurora::IO::Loop
bool LSMutex::IsSignaledNonblocking()
{
AuUInt64 oldSemaphoreValue {};
auto ok = ::read(this->handle, &oldSemaphoreValue, sizeof(oldSemaphoreValue)) == 8;
auto read = ::read(this->handle, &oldSemaphoreValue, sizeof(oldSemaphoreValue));
auto ok = read == 8;
SysAssertDbg(!ok || oldSemaphoreValue == 1, "Double unlock caught");
if (!ok)
{
// TODO: Might hook here
}
else
{
this->bOwns = true;
}
return ok;
}

View File

@ -24,8 +24,10 @@ namespace Aurora::IO::Loop
bool WaitOn(AuUInt32 timeout) override;
virtual ELoopSource GetType() override;
bool bNoAutoRel {};
private:
void Init();
bool IsSignaledNonblocking();
bool bOwns {};
};
}