Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 2m0s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Working-tree snapshot taken 2026-07-28 so this work is not lost. To be clear about provenance: NONE of this was written in the session that committed it — it is pre-existing uncommitted work on phase-a-tracing, last committed 2026-07-09, from the xenia-rs decoder/deadlock investigation. It is recorded verbatim, not reviewed and not tested. Contents: 24 tracked modifications (ppc_emit_memory, xex_module, object_table, shim_utils, xboxkrnl threading/rtl, xevent/xobject/xthread, memory) plus the untracked probe sources audit_68_host_mem_watch, audit_69_event_signal_watch, audit_70_semaphore_release_watch, phase_b_snapshot, and cmake/toolchains. Deliberately excluded: build-cross/ (51 GB of build output) and vkd3d-proton.cache. Note third_party/snappy is a submodule pointer change whose target may not be pushed anywhere, so this branch is a preservation record rather than a guaranteed-buildable tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
514 lines
17 KiB
C++
514 lines
17 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2022 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "xenia/kernel/xobject.h"
|
|
|
|
#include "xenia/base/byte_stream.h"
|
|
#include "xenia/kernel/event_log.h"
|
|
#include "xenia/kernel/kernel_state.h"
|
|
#include "xenia/kernel/util/shim_utils.h"
|
|
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
|
|
#include "xenia/kernel/xenumerator.h"
|
|
#include "xenia/kernel/xevent.h"
|
|
#include "xenia/kernel/xfile.h"
|
|
#include "xenia/kernel/xmodule.h"
|
|
#include "xenia/kernel/xmutant.h"
|
|
#include "xenia/kernel/xnotifylistener.h"
|
|
#include "xenia/kernel/xsemaphore.h"
|
|
#include "xenia/kernel/xsymboliclink.h"
|
|
#include "xenia/kernel/xthread.h"
|
|
#include "xenia/xbox.h"
|
|
|
|
namespace xe {
|
|
namespace kernel {
|
|
|
|
XObject::XObject(Type type)
|
|
: kernel_state_(nullptr), pointer_ref_count_(1), type_(type) {
|
|
handles_.reserve(10);
|
|
}
|
|
|
|
XObject::XObject(KernelState* kernel_state, Type type, bool host_object)
|
|
: kernel_state_(kernel_state),
|
|
type_(type),
|
|
pointer_ref_count_(1),
|
|
guest_object_ptr_(0),
|
|
allocated_guest_object_(false),
|
|
host_object_(host_object) {
|
|
handles_.reserve(10);
|
|
|
|
// TODO: Assert kernel_state != nullptr in this constructor.
|
|
if (kernel_state) {
|
|
kernel_state->object_table()->AddHandle(this, nullptr);
|
|
}
|
|
}
|
|
|
|
XObject::~XObject() {
|
|
assert_true(handles_.empty());
|
|
assert_zero(pointer_ref_count_);
|
|
|
|
if (allocated_guest_object_) {
|
|
uint32_t ptr = guest_object_ptr_ - sizeof(X_OBJECT_HEADER);
|
|
auto header = memory()->TranslateVirtual<X_OBJECT_HEADER*>(ptr);
|
|
|
|
// Free the object creation info
|
|
if (header->object_type_ptr) {
|
|
memory()->SystemHeapFree(header->object_type_ptr);
|
|
}
|
|
|
|
memory()->SystemHeapFree(ptr);
|
|
}
|
|
}
|
|
|
|
Emulator* XObject::emulator() const { return kernel_state_->emulator_; }
|
|
KernelState* XObject::kernel_state() const { return kernel_state_; }
|
|
Memory* XObject::memory() const { return kernel_state_->memory(); }
|
|
|
|
XObject::Type XObject::type() const { return type_; }
|
|
|
|
void XObject::RetainHandle() {
|
|
kernel_state_->object_table()->RetainHandle(handles_[0]);
|
|
}
|
|
|
|
bool XObject::ReleaseHandle() {
|
|
// FIXME: Return true when handle is actually released.
|
|
return kernel_state_->object_table()->ReleaseHandle(handles_[0]) ==
|
|
X_STATUS_SUCCESS;
|
|
}
|
|
|
|
void XObject::Retain() { ++pointer_ref_count_; }
|
|
|
|
void XObject::Release() {
|
|
if (--pointer_ref_count_ == 0) {
|
|
delete this;
|
|
}
|
|
}
|
|
|
|
X_STATUS XObject::Delete() {
|
|
if (kernel_state_ == nullptr) {
|
|
// Fake return value for api-scanner
|
|
return X_STATUS_SUCCESS;
|
|
} else {
|
|
if (!name_.empty()) {
|
|
kernel_state_->object_table()->RemoveNameMapping(name_);
|
|
}
|
|
return kernel_state_->object_table()->RemoveHandle(handles_[0]);
|
|
}
|
|
}
|
|
|
|
bool XObject::SaveObject(ByteStream* stream) {
|
|
stream->Write<uint32_t>(allocated_guest_object_);
|
|
stream->Write<uint32_t>(guest_object_ptr_);
|
|
|
|
stream->Write(uint32_t(handles_.size()));
|
|
stream->Write(&handles_[0], handles_.size() * sizeof(X_HANDLE));
|
|
|
|
return true;
|
|
}
|
|
|
|
bool XObject::RestoreObject(ByteStream* stream) {
|
|
allocated_guest_object_ = stream->Read<uint32_t>() > 0;
|
|
guest_object_ptr_ = stream->Read<uint32_t>();
|
|
|
|
handles_.resize(stream->Read<uint32_t>());
|
|
stream->Read(&handles_[0], handles_.size() * sizeof(X_HANDLE));
|
|
|
|
// Restore our pointer to our handles in the object table.
|
|
for (size_t i = 0; i < handles_.size(); i++) {
|
|
kernel_state_->object_table()->RestoreHandle(handles_[i], this);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
object_ref<XObject> XObject::Restore(KernelState* kernel_state, Type type,
|
|
ByteStream* stream) {
|
|
switch (type) {
|
|
case Type::Enumerator:
|
|
break;
|
|
case Type::Event:
|
|
return XEvent::Restore(kernel_state, stream);
|
|
case Type::File:
|
|
return XFile::Restore(kernel_state, stream);
|
|
case Type::IOCompletion:
|
|
break;
|
|
case Type::Module:
|
|
return XModule::Restore(kernel_state, stream);
|
|
case Type::Mutant:
|
|
return XMutant::Restore(kernel_state, stream);
|
|
case Type::NotifyListener:
|
|
return XNotifyListener::Restore(kernel_state, stream);
|
|
case Type::Semaphore:
|
|
return XSemaphore::Restore(kernel_state, stream);
|
|
case Type::Session:
|
|
break;
|
|
case Type::Socket:
|
|
break;
|
|
case Type::SymbolicLink:
|
|
return XSymbolicLink::Restore(kernel_state, stream);
|
|
case Type::Thread:
|
|
return XThread::Restore(kernel_state, stream);
|
|
case Type::Timer:
|
|
break;
|
|
case Type::Undefined:
|
|
break;
|
|
}
|
|
|
|
assert_always("No restore handler exists for this object!");
|
|
return nullptr;
|
|
}
|
|
|
|
void XObject::SetAttributes(uint32_t obj_attributes_ptr) {
|
|
if (!obj_attributes_ptr) {
|
|
return;
|
|
}
|
|
|
|
auto name = util::TranslateAnsiStringAddress(
|
|
memory(), xe::load_and_swap<uint32_t>(
|
|
memory()->TranslateVirtual(obj_attributes_ptr + 4)));
|
|
if (!name.empty()) {
|
|
name_ = std::string(name);
|
|
kernel_state_->object_table()->AddNameMapping(name_, handles_[0]);
|
|
}
|
|
}
|
|
|
|
uint32_t XObject::TimeoutTicksToMs(int64_t timeout_ticks) {
|
|
if (timeout_ticks > 0) {
|
|
// NetDll_WSAWaitForMultipleEvents provides timeout in form of MS.
|
|
return (uint32_t)timeout_ticks;
|
|
} else if (timeout_ticks < 0) {
|
|
// Relative time.
|
|
return (uint32_t)(-timeout_ticks / 10000); // Ticks -> MS
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
X_STATUS XObject::Wait(uint32_t wait_reason, uint32_t processor_mode,
|
|
uint32_t alertable, uint64_t* opt_timeout) {
|
|
auto wait_handle = GetWaitHandle();
|
|
if (!wait_handle) {
|
|
// Object doesn't support waiting.
|
|
return X_STATUS_SUCCESS;
|
|
}
|
|
|
|
auto timeout_ms =
|
|
opt_timeout ? std::chrono::milliseconds(Clock::ScaleGuestDurationMillis(
|
|
TimeoutTicksToMs(*opt_timeout)))
|
|
: std::chrono::milliseconds::max();
|
|
|
|
auto result =
|
|
xe::threading::Wait(wait_handle, alertable ? true : false, timeout_ms);
|
|
|
|
switch (result) {
|
|
case xe::threading::WaitResult::kSuccess:
|
|
case xe::threading::WaitResult::kUserCallback: {
|
|
auto current_thread = XThread::GetCurrentThread();
|
|
if (current_thread) {
|
|
current_thread->BoostOnWake(priority_increment());
|
|
}
|
|
if (result == xe::threading::WaitResult::kSuccess) {
|
|
WaitCallback();
|
|
return X_STATUS_SUCCESS;
|
|
}
|
|
return X_STATUS_USER_APC;
|
|
}
|
|
case xe::threading::WaitResult::kTimeout:
|
|
xe::threading::MaybeYield();
|
|
return X_STATUS_TIMEOUT;
|
|
default:
|
|
case xe::threading::WaitResult::kAbandoned:
|
|
case xe::threading::WaitResult::kFailed:
|
|
return X_STATUS_ABANDONED_WAIT_0;
|
|
}
|
|
}
|
|
|
|
X_STATUS XObject::SignalAndWait(XObject* signal_object, XObject* wait_object,
|
|
uint32_t wait_reason, uint32_t processor_mode,
|
|
uint32_t alertable, uint64_t* opt_timeout) {
|
|
auto timeout_ms =
|
|
opt_timeout ? std::chrono::milliseconds(Clock::ScaleGuestDurationMillis(
|
|
TimeoutTicksToMs(*opt_timeout)))
|
|
: std::chrono::milliseconds::max();
|
|
|
|
auto result = xe::threading::SignalAndWait(
|
|
signal_object->GetWaitHandle(), wait_object->GetWaitHandle(),
|
|
alertable ? true : false, timeout_ms);
|
|
|
|
switch (result) {
|
|
case xe::threading::WaitResult::kSuccess:
|
|
case xe::threading::WaitResult::kUserCallback: {
|
|
auto current_thread = XThread::GetCurrentThread();
|
|
if (current_thread) {
|
|
current_thread->BoostOnWake(wait_object->priority_increment());
|
|
}
|
|
if (result == xe::threading::WaitResult::kSuccess) {
|
|
wait_object->WaitCallback();
|
|
return X_STATUS_SUCCESS;
|
|
}
|
|
return X_STATUS_USER_APC;
|
|
}
|
|
case xe::threading::WaitResult::kTimeout:
|
|
xe::threading::MaybeYield();
|
|
return X_STATUS_TIMEOUT;
|
|
default:
|
|
case xe::threading::WaitResult::kAbandoned:
|
|
case xe::threading::WaitResult::kFailed:
|
|
return X_STATUS_ABANDONED_WAIT_0;
|
|
}
|
|
}
|
|
|
|
X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects,
|
|
uint32_t wait_type, uint32_t wait_reason,
|
|
uint32_t processor_mode, uint32_t alertable,
|
|
uint64_t* opt_timeout) {
|
|
xe::threading::WaitHandle* wait_handles[64];
|
|
|
|
for (size_t i = 0; i < count; ++i) {
|
|
wait_handles[i] = objects[i]->GetWaitHandle();
|
|
assert_not_null(wait_handles[i]);
|
|
}
|
|
|
|
auto timeout_ms =
|
|
opt_timeout ? std::chrono::milliseconds(Clock::ScaleGuestDurationMillis(
|
|
TimeoutTicksToMs(*opt_timeout)))
|
|
: std::chrono::milliseconds::max();
|
|
|
|
X_STATUS status;
|
|
uint32_t boost_increment = 0;
|
|
if (wait_type) {
|
|
auto result = xe::threading::WaitAny(wait_handles, count,
|
|
alertable ? true : false, timeout_ms);
|
|
switch (result.first) {
|
|
case xe::threading::WaitResult::kSuccess:
|
|
objects[result.second]->WaitCallback();
|
|
boost_increment = objects[result.second]->priority_increment();
|
|
status = X_STATUS(result.second);
|
|
break;
|
|
case xe::threading::WaitResult::kUserCallback:
|
|
status = X_STATUS_USER_APC;
|
|
break;
|
|
case xe::threading::WaitResult::kTimeout:
|
|
xe::threading::MaybeYield();
|
|
status = X_STATUS_TIMEOUT;
|
|
break;
|
|
case xe::threading::WaitResult::kAbandoned:
|
|
status = X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second);
|
|
break;
|
|
default:
|
|
case xe::threading::WaitResult::kFailed:
|
|
status = X_STATUS_UNSUCCESSFUL;
|
|
break;
|
|
}
|
|
} else {
|
|
auto result = xe::threading::WaitAll(wait_handles, count,
|
|
alertable ? true : false, timeout_ms);
|
|
switch (result) {
|
|
case xe::threading::WaitResult::kSuccess:
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
objects[i]->WaitCallback();
|
|
// Use the largest increment among the signaled objects.
|
|
if (objects[i]->priority_increment() > boost_increment) {
|
|
boost_increment = objects[i]->priority_increment();
|
|
}
|
|
}
|
|
status = X_STATUS_SUCCESS;
|
|
break;
|
|
case xe::threading::WaitResult::kUserCallback:
|
|
status = X_STATUS_USER_APC;
|
|
break;
|
|
case xe::threading::WaitResult::kTimeout:
|
|
xe::threading::MaybeYield();
|
|
status = X_STATUS_TIMEOUT;
|
|
break;
|
|
default:
|
|
case xe::threading::WaitResult::kAbandoned:
|
|
case xe::threading::WaitResult::kFailed:
|
|
status = X_STATUS_ABANDONED_WAIT_0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Apply priority boost if the thread actually blocked (not on
|
|
// timeout/failure).
|
|
if (status != X_STATUS_TIMEOUT && status != X_STATUS_UNSUCCESSFUL &&
|
|
status != X_STATUS_ABANDONED_WAIT_0) {
|
|
auto current_thread = XThread::GetCurrentThread();
|
|
if (current_thread) {
|
|
current_thread->BoostOnWake(boost_increment);
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
|
|
uint8_t* XObject::CreateNative(uint32_t size) {
|
|
auto global_lock = xe::global_critical_region::AcquireDirect();
|
|
|
|
uint32_t total_size = size + sizeof(X_OBJECT_HEADER);
|
|
|
|
auto mem = memory()->SystemHeapAlloc(total_size);
|
|
if (!mem) {
|
|
// Out of memory!
|
|
return nullptr;
|
|
}
|
|
|
|
allocated_guest_object_ = true;
|
|
memory()->Zero(mem, total_size);
|
|
SetNativePointer(mem + sizeof(X_OBJECT_HEADER), true);
|
|
|
|
auto header = memory()->TranslateVirtual<X_OBJECT_HEADER*>(mem);
|
|
|
|
auto object_type = memory()->SystemHeapAlloc(sizeof(X_OBJECT_TYPE));
|
|
if (object_type) {
|
|
// Set it up in the header.
|
|
// Some kernel method is accessing this struct and dereferencing a member
|
|
// @ offset 0x14
|
|
header->object_type_ptr = object_type;
|
|
}
|
|
|
|
return memory()->TranslateVirtual(guest_object_ptr_);
|
|
}
|
|
|
|
void XObject::SetNativePointer(uint32_t native_ptr, bool uninitialized) {
|
|
auto global_lock = xe::global_critical_region::AcquireDirect();
|
|
|
|
// If hit: We've already setup the native ptr with CreateNative!
|
|
assert_zero(guest_object_ptr_);
|
|
|
|
auto header =
|
|
kernel_state_->memory()->TranslateVirtual<X_DISPATCH_HEADER*>(native_ptr);
|
|
|
|
// Memory uninitialized, so don't bother with the check.
|
|
if (!uninitialized) {
|
|
assert_true(!(header->wait_list.blink_ptr & 0x1));
|
|
}
|
|
|
|
// Stash pointer in struct.
|
|
// FIXME: This assumes the object has a dispatch header (some don't!)
|
|
StashHandle(header, handle());
|
|
|
|
guest_object_ptr_ = native_ptr;
|
|
}
|
|
|
|
object_ref<XObject> XObject::GetNativeObject(KernelState* kernel_state,
|
|
void* native_ptr, int32_t as_type,
|
|
bool already_locked) {
|
|
assert_not_null(native_ptr);
|
|
|
|
// Unfortunately the XDK seems to inline some KeInitialize calls, meaning
|
|
// we never see it and just randomly start getting passed events/timers/etc.
|
|
// Luckily it seems like all other calls (Set/Reset/Wait/etc) are used and
|
|
// we don't have to worry about PPC code poking the struct. Because of that,
|
|
// we init on first use, store our handle in the struct, and dereference it
|
|
// each time.
|
|
// We identify this by setting wait_list.flink_ptr to a magic value. When set,
|
|
// wait_list.blink_ptr will hold a handle to our object.
|
|
if (!already_locked) {
|
|
global_critical_region::mutex().lock();
|
|
}
|
|
|
|
XObject* result;
|
|
|
|
auto header = reinterpret_cast<X_DISPATCH_HEADER*>(native_ptr);
|
|
if (as_type == -1) {
|
|
as_type = header->type;
|
|
}
|
|
|
|
if (header->wait_list.flink_ptr == kXObjSignature) {
|
|
// Already initialized.
|
|
// TODO: assert if the type of the object != as_type
|
|
uint32_t handle = header->wait_list.blink_ptr;
|
|
result = kernel_state->object_table()
|
|
->LookupObject<XObject>(handle, true)
|
|
.release();
|
|
} else {
|
|
// First use, create new.
|
|
// https://www.nirsoft.net/kernel_struct/vista/KOBJECTS.html
|
|
XObject* object = nullptr;
|
|
// Phase C+18: bracket the wrapper ctor + AddHandle path with a
|
|
// host-TLS flag so the centralized `ObjectTable::AddHandle` emit
|
|
// hook short-circuits its per-thread `handle.create`. We emit a
|
|
// shared-global `handle.create` explicitly below with a SID keyed
|
|
// on `(native_ptr, object_type)` — scheduling-invariant across
|
|
// engines/threads. See `event_log.h`/`event_log.cc` C+18 helpers
|
|
// and ours's `ensure_dispatcher_object`.
|
|
phase_a::SetInGetNativeObject(true);
|
|
uint32_t schema_object_type = phase_a::kObjUnknown;
|
|
switch (as_type) {
|
|
case 0: // EventNotificationObject
|
|
case 1: // EventSynchronizationObject
|
|
{
|
|
auto ev = new XEvent(kernel_state);
|
|
ev->InitializeNative(native_ptr, header);
|
|
object = ev;
|
|
schema_object_type = phase_a::kObjEvent;
|
|
} break;
|
|
case 2: // MutantObject
|
|
{
|
|
auto mutant = new XMutant(kernel_state);
|
|
mutant->InitializeNative(native_ptr, header);
|
|
object = mutant;
|
|
schema_object_type = phase_a::kObjMutant;
|
|
} break;
|
|
case 5: // SemaphoreObject
|
|
{
|
|
auto sem = new XSemaphore(kernel_state);
|
|
auto success = sem->InitializeNative(native_ptr, header);
|
|
// Can't report failure to the guest at late initialization:
|
|
assert_true(success);
|
|
object = sem;
|
|
schema_object_type = phase_a::kObjSemaphore;
|
|
} break;
|
|
case 3: // ProcessObject
|
|
case 4: // QueueObject
|
|
case 6: // ThreadObject
|
|
case 7: // GateObject
|
|
case 8: // TimerNotificationObject
|
|
case 9: // TimerSynchronizationObject
|
|
case 18: // ApcObject
|
|
case 19: // DpcObject
|
|
case 20: // DeviceQueueObject
|
|
case 21: // EventPairObject
|
|
case 22: // InterruptObject
|
|
case 23: // ProfileObject
|
|
case 24: // ThreadedDpcObject
|
|
default:
|
|
assert_always();
|
|
result = nullptr;
|
|
}
|
|
phase_a::SetInGetNativeObject(false);
|
|
// Stash pointer in struct.
|
|
// FIXME: This assumes the object contains a dispatch header (some don't!)
|
|
if (object) {
|
|
StashHandle(header, object->handle());
|
|
// Phase C+18: emit the shared-global `handle.create` here, AFTER
|
|
// StashHandle, with the deterministic SID. The `native_ptr` is
|
|
// already a guest VA (`X_DISPATCH_HEADER*`) so a host-pointer cast
|
|
// to uint32_t is meaningful in canary's memory model only when the
|
|
// value is the guest address — translate via the same memory base.
|
|
if (phase_a::IsEnabled()) {
|
|
uint32_t guest_ptr =
|
|
kernel_state->memory()->HostToGuestVirtual(native_ptr);
|
|
const std::string& name = object->name();
|
|
phase_a::EmitHandleCreateSharedGlobal(
|
|
guest_ptr, schema_object_type, object->handle(),
|
|
name.empty() ? nullptr : name.c_str());
|
|
}
|
|
}
|
|
result = object;
|
|
}
|
|
|
|
if (!already_locked) {
|
|
global_critical_region::mutex().unlock();
|
|
}
|
|
return object_ref<XObject>(result);
|
|
}
|
|
|
|
} // namespace kernel
|
|
} // namespace xe
|