mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-05 13:20:11 +00:00
Merge pull request #9491 from EagleBearerYS/fix/libkernel-sema-handoff
fix(libkernel): hand signalled semaphore tokens to queued waiters
This commit is contained in:
6 files changed
+227
-44
No files matched your search
@@ -1033,6 +1033,16 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(guest_pthread_cancel_recovery guest_pthread_cancel_recovery_coarse PROPERTIES
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(guest_semaphore_handoff_tests ${LIBS_INCLUDE_DIR}/tests/GuestSemaphoreHandoff.cpp)
|
||||
target_include_directories(guest_semaphore_handoff_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_semaphore_handoff_tests PRIVATE libkernel libc)
|
||||
configure_windows_unwind(guest_semaphore_handoff_tests)
|
||||
add_test(NAME guest_semaphore_handoff COMMAND guest_semaphore_handoff_tests)
|
||||
set_tests_properties(guest_semaphore_handoff PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_semaphore_handoff PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(guest_semaphore_tests ${LIBS_INCLUDE_DIR}/tests/GuestSemaphore.cpp)
|
||||
target_include_directories(guest_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_semaphore_tests PRIVATE libkernel libc)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
@@ -13,6 +14,12 @@
|
||||
|
||||
constexpr int KERNEL_SEMA_OK = 0;
|
||||
|
||||
struct KernelSemaWaiter {
|
||||
std::int32_t need;
|
||||
bool released = false;
|
||||
int priority = 0;
|
||||
};
|
||||
|
||||
struct KernelSemaPrivate {
|
||||
KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCount, std::string name, bool isFifo);
|
||||
|
||||
@@ -23,9 +30,10 @@ struct KernelSemaPrivate {
|
||||
std::int32_t initCount;
|
||||
std::int32_t maxCount;
|
||||
bool isFifo;
|
||||
bool priorityQueue = false;
|
||||
bool deleted = false;
|
||||
std::int32_t waiterCount = 0;
|
||||
std::int32_t cancelableCount = 0;
|
||||
std::list<KernelSemaWaiter*> queue;
|
||||
std::uint64_t cancelGeneration = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "prx/libkernel/Semaphore/include/Semaphore.hpp"
|
||||
#include "prx/libkernel/include/Api.hpp"
|
||||
#include "prx/libkernel/Time/include/Time.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -29,6 +31,7 @@ int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t at
|
||||
return SCE_KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
*sem = new KernelSemaPrivate(init, max, std::string(name), (attr & 3u) == 1u);
|
||||
(*sem)->priorityQueue = (attr & 3u) == 2u;
|
||||
if (TraceSync()) std::fprintf(stderr, "[sema] create %p '%s' attr=0x%x init=%d max=%d\n", static_cast<void*>(*sem), name, attr, init, max);
|
||||
return KERNEL_SEMA_OK;
|
||||
}
|
||||
@@ -64,6 +67,16 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) {
|
||||
const auto before = sem->tokenCount;
|
||||
sem->tokenCount += count;
|
||||
if (TraceSync()) std::fprintf(stderr, "[sema] signal %p '%s' count=%d have=%d->%d\n", static_cast<void*>(sem), sem->name.c_str(), count, before, sem->tokenCount);
|
||||
for (auto it = sem->queue.begin(); it != sem->queue.end();) {
|
||||
KernelSemaWaiter* waiter = *it;
|
||||
if (waiter->need > sem->tokenCount) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
sem->tokenCount -= waiter->need;
|
||||
waiter->released = true;
|
||||
it = sem->queue.erase(it);
|
||||
}
|
||||
sem->condition.NotifyAll();
|
||||
return KERNEL_SEMA_OK;
|
||||
}
|
||||
@@ -77,51 +90,59 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time)
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(sem->mutex);
|
||||
const std::uint64_t generation = sem->cancelGeneration;
|
||||
++sem->waiterCount;
|
||||
++sem->cancelableCount;
|
||||
struct WaiterGuard {
|
||||
KernelSemaPrivate* sem;
|
||||
std::uint64_t generation;
|
||||
~WaiterGuard() {
|
||||
--sem->waiterCount;
|
||||
if (sem->cancelGeneration == generation) {
|
||||
--sem->cancelableCount;
|
||||
}
|
||||
sem->condition.NotifyAll();
|
||||
}
|
||||
} waiterGuard{sem, generation};
|
||||
|
||||
const auto waitStart = std::chrono::steady_clock::now();
|
||||
const void* caller = __builtin_return_address(0);
|
||||
if (TraceSync()) std::fprintf(stderr, "[sema] wait begin %p '%s' need=%d have=%d timeout=%lld caller=%p\n", static_cast<void*>(sem), sem->name.c_str(), need, sem->tokenCount, time ? static_cast<long long>(*time) : -1LL, caller);
|
||||
const auto traceSyncWaitEnd = [&](int result) {
|
||||
if (TraceSync()) std::fprintf(stderr, "[sema] wait end %p '%s' result=0x%x have=%d caller=%p\n", static_cast<void*>(sem), sem->name.c_str(), result, sem->tokenCount, caller);
|
||||
};
|
||||
const auto traceWait = [&](bool timedOut) {
|
||||
KernelTraceWait_nid_postfix("sema", caller, static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - waitStart).count()), timedOut);
|
||||
};
|
||||
const auto released = [&] { return sem->tokenCount >= need || sem->deleted || sem->cancelGeneration != generation; };
|
||||
if (time == nullptr) {
|
||||
sem->condition.Wait(lock, released);
|
||||
traceWait(false);
|
||||
if (sem->deleted) {
|
||||
traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES);
|
||||
return SCE_KERNEL_ERROR_EACCES;
|
||||
}
|
||||
if (sem->cancelGeneration != generation) {
|
||||
traceSyncWaitEnd(SCE_KERNEL_ERROR_ECANCELED);
|
||||
return SCE_KERNEL_ERROR_ECANCELED;
|
||||
}
|
||||
if (sem->deleted) {
|
||||
traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES);
|
||||
return SCE_KERNEL_ERROR_EACCES;
|
||||
}
|
||||
if (sem->tokenCount >= need) {
|
||||
sem->tokenCount -= need;
|
||||
traceSyncWaitEnd(KERNEL_SEMA_OK);
|
||||
return KERNEL_SEMA_OK;
|
||||
}
|
||||
const std::uint64_t generation = sem->cancelGeneration;
|
||||
KernelSemaWaiter waiter{need};
|
||||
if (sem->priorityQueue && scePthreadGetprio(scePthreadSelf(), &waiter.priority) == 0) {
|
||||
const auto lower = std::find_if(sem->queue.begin(), sem->queue.end(), [&](const KernelSemaWaiter* queued) { return queued->priority > waiter.priority; });
|
||||
sem->queue.insert(lower, &waiter);
|
||||
} else {
|
||||
sem->queue.push_back(&waiter);
|
||||
}
|
||||
++sem->waiterCount;
|
||||
struct WaiterGuard {
|
||||
KernelSemaPrivate* sem;
|
||||
KernelSemaWaiter* waiter;
|
||||
~WaiterGuard() {
|
||||
--sem->waiterCount;
|
||||
if (!waiter->released) {
|
||||
sem->queue.remove(waiter);
|
||||
}
|
||||
sem->condition.NotifyAll();
|
||||
}
|
||||
} waiterGuard{sem, &waiter};
|
||||
|
||||
const std::uint64_t deadline = TimedWait::DeadlineNanos(*time);
|
||||
const bool acquired = sem->condition.WaitUntil(lock, deadline, released);
|
||||
*time = static_cast<KernelUseconds>(TimedWait::RemainingMicros(deadline));
|
||||
const auto waitStart = std::chrono::steady_clock::now();
|
||||
const auto traceWait = [&](bool timedOut) {
|
||||
KernelTraceWait_nid_postfix("sema", caller, static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - waitStart).count()), timedOut);
|
||||
};
|
||||
const auto released = [&] { return waiter.released || sem->deleted || sem->cancelGeneration != generation; };
|
||||
bool acquired = true;
|
||||
if (time == nullptr) {
|
||||
sem->condition.Wait(lock, released);
|
||||
} else {
|
||||
const std::uint64_t deadline = TimedWait::DeadlineNanos(*time);
|
||||
acquired = sem->condition.WaitUntil(lock, deadline, released);
|
||||
*time = static_cast<KernelUseconds>(TimedWait::RemainingMicros(deadline));
|
||||
}
|
||||
traceWait(!acquired);
|
||||
if (waiter.released) {
|
||||
traceSyncWaitEnd(KERNEL_SEMA_OK);
|
||||
return KERNEL_SEMA_OK;
|
||||
}
|
||||
if (sem->deleted) {
|
||||
traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES);
|
||||
return SCE_KERNEL_ERROR_EACCES;
|
||||
@@ -130,13 +151,8 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time)
|
||||
traceSyncWaitEnd(SCE_KERNEL_ERROR_ECANCELED);
|
||||
return SCE_KERNEL_ERROR_ECANCELED;
|
||||
}
|
||||
if (!acquired) {
|
||||
traceSyncWaitEnd(SCE_KERNEL_ERROR_ETIMEDOUT);
|
||||
return SCE_KERNEL_ERROR_ETIMEDOUT;
|
||||
}
|
||||
sem->tokenCount -= need;
|
||||
traceSyncWaitEnd(KERNEL_SEMA_OK);
|
||||
return KERNEL_SEMA_OK;
|
||||
traceSyncWaitEnd(SCE_KERNEL_ERROR_ETIMEDOUT);
|
||||
return SCE_KERNEL_ERROR_ETIMEDOUT;
|
||||
}
|
||||
|
||||
int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) {
|
||||
@@ -149,10 +165,10 @@ int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) {
|
||||
return SCE_KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
if (threads != nullptr) {
|
||||
*threads = sem->cancelableCount;
|
||||
*threads = static_cast<int>(sem->queue.size());
|
||||
}
|
||||
sem->tokenCount = count < 0 ? sem->initCount : count;
|
||||
sem->cancelableCount = 0;
|
||||
sem->queue.clear();
|
||||
++sem->cancelGeneration;
|
||||
sem->condition.NotifyAll();
|
||||
return KERNEL_SEMA_OK;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "prx/libkernel/Semaphore/include/Semaphore.hpp"
|
||||
#include "prx/libkernel/include/Api.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
|
||||
static constexpr int SCE_OK = 0;
|
||||
static constexpr std::uint32_t SemaFifo = 1;
|
||||
static constexpr std::uint32_t SemaPriority = 2;
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
struct Waiter {
|
||||
KernelSema sem = nullptr;
|
||||
int result = -1;
|
||||
};
|
||||
|
||||
static void* APS5_VABI Wait(void* arg) {
|
||||
auto& waiter = *static_cast<Waiter*>(arg);
|
||||
KernelUseconds timeout = 1000000;
|
||||
waiter.result = sceKernelWaitSema(waiter.sem, 1, &timeout);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static std::atomic<int> released{0};
|
||||
|
||||
static int QueuedCount(KernelSema sem) {
|
||||
std::lock_guard<std::mutex> lock(sem->mutex);
|
||||
return static_cast<int>(sem->queue.size());
|
||||
}
|
||||
|
||||
struct QueuedWaiter {
|
||||
KernelSema sem = nullptr;
|
||||
int need = 1;
|
||||
int priority = 0;
|
||||
int result = -1;
|
||||
std::atomic<int> order{0};
|
||||
};
|
||||
|
||||
static void* APS5_VABI WaitInQueue(void* arg) {
|
||||
auto& waiter = *static_cast<QueuedWaiter*>(arg);
|
||||
if (waiter.priority != 0) Require(scePthreadSetprio(scePthreadSelf(), waiter.priority) == SCE_OK);
|
||||
KernelUseconds timeout = 5000000;
|
||||
waiter.result = sceKernelWaitSema(waiter.sem, waiter.need, &timeout);
|
||||
waiter.order = ++released;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void WaitForQueue(KernelSema sem, int count) {
|
||||
for (int i = 0; i < 5000 && QueuedCount(sem) != count; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
Require(QueuedCount(sem) == count);
|
||||
}
|
||||
|
||||
static void WaitForReleased(int count) {
|
||||
for (int i = 0; i < 5000 && released.load() < count; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
Require(released.load() == count);
|
||||
}
|
||||
|
||||
static Pthread StartQueued(QueuedWaiter& waiter, int queued) {
|
||||
Pthread thread = nullptr;
|
||||
Require(scePthreadCreate(&thread, nullptr, WaitInQueue, &waiter, nullptr) == SCE_OK);
|
||||
WaitForQueue(waiter.sem, queued);
|
||||
return thread;
|
||||
}
|
||||
|
||||
static void CheckPassesOverALargerNeed() {
|
||||
released = 0;
|
||||
KernelSema sem = nullptr;
|
||||
Require(sceKernelCreateSema(&sem, "handoff-skip", SemaFifo, 0, 4, nullptr) == SCE_OK);
|
||||
QueuedWaiter large{sem, 2};
|
||||
QueuedWaiter small{sem, 1};
|
||||
const Pthread largeThread = StartQueued(large, 1);
|
||||
const Pthread smallThread = StartQueued(small, 2);
|
||||
Require(sceKernelSignalSema(sem, 1) == SCE_OK);
|
||||
WaitForReleased(1);
|
||||
Require(small.order == 1 && small.result == SCE_OK);
|
||||
Require(QueuedCount(sem) == 1 && sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY);
|
||||
Require(sceKernelSignalSema(sem, 2) == SCE_OK);
|
||||
WaitForReleased(2);
|
||||
Require(large.order == 2 && large.result == SCE_OK);
|
||||
Require(scePthreadJoin(largeThread, nullptr) == SCE_OK && scePthreadJoin(smallThread, nullptr) == SCE_OK);
|
||||
Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY);
|
||||
Require(sceKernelDeleteSema(sem) == SCE_OK);
|
||||
}
|
||||
|
||||
static void CheckDeletedRefusesFreeTokens() {
|
||||
KernelSema sem = nullptr;
|
||||
Require(sceKernelCreateSema(&sem, "handoff-deleted", SemaFifo, 2, 2, nullptr) == SCE_OK);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(sem->mutex);
|
||||
sem->deleted = true;
|
||||
}
|
||||
Require(sceKernelWaitSema(sem, 1, nullptr) == SCE_KERNEL_ERROR_EACCES);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(sem->mutex);
|
||||
Require(sem->tokenCount == 2);
|
||||
sem->deleted = false;
|
||||
}
|
||||
Require(sceKernelDeleteSema(sem) == SCE_OK);
|
||||
}
|
||||
|
||||
static void CheckServesInOrder(std::uint32_t attr, int firstPriority, int secondPriority, bool firstServedFirst) {
|
||||
released = 0;
|
||||
KernelSema sem = nullptr;
|
||||
Require(sceKernelCreateSema(&sem, "handoff-order", attr, 0, 2, nullptr) == SCE_OK);
|
||||
QueuedWaiter first{sem, 1, firstPriority};
|
||||
QueuedWaiter second{sem, 1, secondPriority};
|
||||
const Pthread firstThread = StartQueued(first, 1);
|
||||
const Pthread secondThread = StartQueued(second, 2);
|
||||
Require(sceKernelSignalSema(sem, 1) == SCE_OK);
|
||||
WaitForReleased(1);
|
||||
Require((firstServedFirst ? first.order : second.order) == 1);
|
||||
Require(QueuedCount(sem) == 1);
|
||||
Require(sceKernelSignalSema(sem, 1) == SCE_OK);
|
||||
WaitForReleased(2);
|
||||
Require(first.result == SCE_OK && second.result == SCE_OK);
|
||||
Require(scePthreadJoin(firstThread, nullptr) == SCE_OK && scePthreadJoin(secondThread, nullptr) == SCE_OK);
|
||||
Require(sceKernelDeleteSema(sem) == SCE_OK);
|
||||
}
|
||||
|
||||
int main() {
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
KernelSema sem = nullptr;
|
||||
Require(sceKernelCreateSema(&sem, "handoff", 1, 0, 1, nullptr) == SCE_OK);
|
||||
Waiter waiter{sem};
|
||||
Pthread thread = nullptr;
|
||||
Require(scePthreadCreate(&thread, nullptr, Wait, &waiter, nullptr) == SCE_OK);
|
||||
WaitForQueue(sem, 1);
|
||||
Require(sceKernelSignalSema(sem, 1) == SCE_OK);
|
||||
Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY);
|
||||
Require(scePthreadJoin(thread, nullptr) == SCE_OK);
|
||||
Require(waiter.result == SCE_OK);
|
||||
Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY);
|
||||
Require(sceKernelDeleteSema(sem) == SCE_OK);
|
||||
}
|
||||
CheckPassesOverALargerNeed();
|
||||
CheckDeletedRefusesFreeTokens();
|
||||
CheckServesInOrder(SemaFifo, 700, 700, true);
|
||||
CheckServesInOrder(SemaFifo, 767, 256, true);
|
||||
CheckServesInOrder(SemaPriority, 767, 256, false);
|
||||
CheckServesInOrder(SemaPriority, 256, 767, true);
|
||||
CheckServesInOrder(SemaPriority, 700, 700, true);
|
||||
return 0;
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <thread>
|
||||
|
||||
extern "C" void KernelTraceWait_nid_postfix(const char*, const void*, std::uint64_t, bool) {}
|
||||
extern "C" Pthread APS5_VABI scePthreadSelf() { return nullptr; }
|
||||
extern "C" int APS5_VABI scePthreadGetprio(Pthread, int*) { return -1; }
|
||||
|
||||
int main() {
|
||||
KernelSema sem = nullptr;
|
||||
|
||||
@@ -170,6 +170,7 @@
|
||||
- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; a shrinking truncate of a watched file can also report `NOTE_WRITE` on Windows, because NTFS can show the truncate's new `LastWriteTime` before its new size (2 to 12 times per million back-to-back shrinking `SetEndOfFile` calls on a loaded machine, also through a single `NtQueryInformationFile(FileAllInformation)`), and a snapshot taken in between sees a write at the old size; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags)
|
||||
- [sem_open, sem_close, sem_unlink](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) keep named semaphores in a table inside the process; FreeBSD 11.4's [`sem_new.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sem_new.c#L138-L308) backs each with a file `/tmp/SEMD<name>`. A semaphore lasts until `sem_unlink` and its last `sem_close` but is not shared with other processes, and the mode is ignored (no permission errors, as for a root caller). Whether a title can create files under `/tmp` on the console is unverified. A name with a further `/` throws, as it would resolve below `/tmp`, and so does an initial value above `SEM_VALUE_MAX`, which FreeBSD stores unchecked.
|
||||
- [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. A payload process on a PS5 (firmware 13.60) gave the same results: with a count available, a null timeout, `tv_nsec` of -1 or 1000000000 and a past deadline all returned 0 and took the count; with none, an invalid `tv_nsec` failed with `EINVAL`, and a past deadline or a negative `tv_sec` with `ETIMEDOUT`. One title (PPSA29866) disassembled for this entry has one call site (0x130916), in a wait helper that reads the clock with id 0 into a zeroed 16-byte time structure, adds the milliseconds multiplied by 1,000,000 to the nanoseconds and normalises them below 1e9, so its deadline is never null. It repeats the wait with the same deadline when the error is 4, returns false for any other error and when reading the clock fails, and does not tell a timeout from other errors. This title therefore gives no evidence on null or invalid timeouts.
|
||||
- [sceKernelWaitSema and sceKernelSignalSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) queue waiters in arrival order, or with `SCE_KERNEL_SEMA_ATTR_TH_PRIO` (0x02) by the priority the waiting thread has when it starts to wait, lower values first and equal ones in arrival order. `sceKernelSignalSema` hands its tokens to them in that order, passing over one that needs more than is left, and a `sceKernelWaitSema` or `sceKernelPollSema` that finds enough tokens takes them without queueing, as shadPS4's [semaphore](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/kernel/threads/semaphore.cpp#L159-L206) does. A priority change while a thread waits does not move it in the queue, an attribute of 0 queues in arrival order, and the console's order and its rule for a waiter that needs more than is left are unverified
|
||||
- [sem_setname](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) takes `(sem, const char* name)` and registers the name once through the kernel's `namedobj_create`, which is not modelled here: only the one-time registration is kept, as a flag on the host semaphore, and the name is not stored or read back. The export is in `libkernel` and the first dword of the semaphore holds `0xffff736d` before the call and `(id << 16) | 0x736d` after; a second call, and a call on a zeroed semaphore, return -1 with `EAGAIN` and no syscall, all taken from BunnyFl's firmware 13.42 disassembly and emulator run on #11267. A destroyed semaphore (zeroed here) was not measured on its own and answers as a zeroed one. A null semaphore faults in the module and a null name is passed unchecked to the kernel, so both throw here, as does a failing `namedobj_create`, whose errors are not known.
|
||||
- [kevent measurements](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - in payload processes on a PS5 (firmware 12.70), re-adding an existing periodic `EVFILT_TIMER` with `EV_ADD`, `fflags` 0 and a positive period kept the armed expiry and the expirations already counted and applied the new period after that expiry, as FreeBSD 11.0 does, where 11.4's `filt_timertouch` restarts the timer: a 50 ms timer re-added with 10 ms fired first 46 ms after the re-add and then every 10 ms, a 10 ms timer re-added with 100 ms fired first 5 ms after the re-add and then every 100 ms, the 12 expirations counted before a re-add were reported after it, and `EV_ONESHOT` in the re-add left the timer periodic. Here such a re-add does the same; on firmware 13.60 a re-add with a unit in `fflags` (`NOTE_MSECONDS`, `NOTE_NSECONDS`) also applied the new period after the armed expiry, and re-adding a disabled timer returned 0 and left it disabled, as here; re-adding an armed one-shot timer, a period of 0 or less, a period too large to add to the armed expiry, other `fflags` or flags other than `EV_ONESHOT` were not measured and throw here, and the other changes to an existing timer except `EV_DELETE` throw here too. There `EV_ADD` failed with `EPERM` for `EVFILT_AIO`, `EVFILT_LIO` and `EVFILT_SENDFILE` at every ident tried (0, 5, 999, 2^40 and an open pipe) and, at ident 5, also with `EV_CLEAR`, `EV_ONESHOT` or `EV_DISABLE` added, and `EV_DELETE` and `EV_ENABLE` without `EV_ADD` failed with `ENOENT`. `EVFILT_PROCDESC` failed with `EBADF` on a descriptor that is not open and on an ident above `INT_MAX`, and with `EINVAL` on every open descriptor tried: Unix-domain stream and datagram sockets, descriptor 0 (a socket), an `AF_INET` UDP socket, both ends of a pipe, a regular file, a directory, another kqueue and the kqueue itself; `EV_DELETE` on a socket failed with `ENOENT`. Here these four filters answer the same way. `EV_ADD` of `EVFILT_PROCDESC` fails with `EINVAL` on the kinds of descriptor tried, since there are no process descriptors here, and also on shared memory and JIT shared memory, which are host files here: FreeBSD 11.0 gives `shm_open` objects [`invfo_kqfilter`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_shm.c#L136), which answers `EINVAL`, but neither was measured. It throws on any other kind, such as `/dev/random` and `/dev/urandom`, and on descriptors 0 to 2 that are not sockets or kqueues, whatever they hold: the host's standard streams, a pipe or file that `dup2` put on one of them, or a descriptor opened on a number that was free when libkernel loaded. A change of any of the four without `EV_ADD` fails with `ENOENT`, for `EVFILT_PROCDESC` after the descriptor check, as `kqueue_register` finds no knote. When `EV_ADD` of one of the four fails with `EPERM`, or with `EINVAL` on a descriptor other than the kqueue itself, the error event has `fflags` 0 and the errno in `data`, as FreeBSD 11.0's [`kqueue_register`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1302-L1316) clears `fflags` and `data` before the attach and [`kqueue_kevent`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005-L1006) then stores the error in `data`; a descriptor that is not open, the kqueue itself and `EV_ENABLE` with `EV_DISABLE` fail before that and keep their `fflags`. That error event was not measured. `EVFILT_PROC` on the process itself with `NOTE_EXIT`, `EVFILT_SIGNAL` for `SIGUSR1` and `EVFILT_FS` at ident 0 were accepted there and still throw here. Also measured there on Unix-domain socketpairs, and still throwing here: a `NOTE_LOWAT` mark of 0 on an empty stream socket gave a read event at once (`data` 0), and so did re-adding that knote with a mark of -1 once the socket was drained; a write mark of 2 on the other end of that stream socketpair gave an event (`data` 8192), and re-adding that knote with 2^30 gave none; a mark of 5 on a datagram socket gave no read event after one 1-byte datagram and a read event (`data` 11) after a second, 10-byte datagram. `EVFILT_READ` on another kqueue reported the number of events pending in it there (1, then 2) and throws here; `EVFILT_WRITE` on a kqueue and a kqueue registered on itself failed with `EINVAL` there, as they do here
|
||||
- [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here
|
||||
|
||||
Reference in new issue
Block a user