Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,16 @@ if(BUILD_TESTING)
add_test(NAME guest_host_standard_error COMMAND guest_host_standard_error_tests)
set_tests_properties(guest_host_standard_error PROPERTIES TIMEOUT 10)
endif()
add_test_executable(guest_truncate_extend_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncateExtend.cpp)
target_include_directories(guest_truncate_extend_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_truncate_extend_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_truncate_extend_tests)
add_test(NAME guest_truncate_extend COMMAND guest_truncate_extend_tests)
set_tests_properties(guest_truncate_extend PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(guest_truncate_extend PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp)
target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_truncate_tests PRIVATE libkernel libc)
Expand Down
18 changes: 15 additions & 3 deletions core/libs/prx/libkernel/File/src/Stdio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,21 @@ static int NativeFchmod(int descriptor, int mode) {
static int NativeFtruncate(int descriptor, std::int64_t length) {
const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length);
if (result != -2) return result;
const auto handle = reinterpret_cast<HANDLE>(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); }));
if (handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL;
return static_cast<int>(::_chsize_s(descriptor, length));
const auto raw = WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); });
if (raw == -1) return EBADF;
const auto handle = reinterpret_cast<HANDLE>(raw);
if (::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL;
FILE_END_OF_FILE_INFO info{};
info.EndOfFile.QuadPart = length;
if (::SetFileInformationByHandle(handle, FileEndOfFileInfo, &info, sizeof(info))) return 0;
switch (::GetLastError()) {
case ERROR_ACCESS_DENIED: return EACCES;
case ERROR_DISK_FULL:
case ERROR_HANDLE_DISK_FULL: return ENOSPC;
case ERROR_FILE_TOO_LARGE:
case ERROR_INVALID_PARAMETER: return EFBIG;
default: return EINVAL;
}
}
static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) {
const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY);
Expand Down
90 changes: 90 additions & 0 deletions core/libs/tests/GuestTruncateExtend.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif

extern "C" {
int APS5_VABI open_nid_postfix(const char*, int, int);
int APS5_VABI close_nid_postfix(int);
std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::uint64_t);
std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::uint64_t, std::int64_t);
std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int);
int APS5_VABI ftruncate_nid_postfix(int, std::int64_t);
int APS5_VABI truncate_nid_postfix(const char*, std::int64_t);
int APS5_VABI unlink_nid_postfix(const char*);
int* APS5_VABI __error_nid_postfix();
}

namespace {

constexpr int ReadOnly = 0;
constexpr int ReadWrite = 2;
constexpr int Create = 0x200;
constexpr std::int64_t Length = 64ll * 1024 * 1024;
constexpr char FileName[] = "anyps5_truncate_extend_file";

void Cleanup() {
unlink_nid_postfix(FileName);
}

void Require(bool condition, const char* what) {
if (condition) return;
std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix());
Cleanup();
std::abort();
}

std::uint64_t BytesWritten() {
#ifdef _WIN32
IO_COUNTERS counters{};
return GetProcessIoCounters(GetCurrentProcess(), &counters) ? counters.WriteTransferCount : 0;
#else
return 0;
#endif
}

}

int main() {
Cleanup();
const int descriptor = open_nid_postfix(FileName, Create | ReadWrite, 0644);
Require(descriptor >= 0, "open failed");
Require(write_nid_postfix(descriptor, "head", 4) == 4, "write failed");

const std::uint64_t before = BytesWritten();
Require(ftruncate_nid_postfix(descriptor, Length) == 0, "ftruncate to 64 MiB failed");
const std::uint64_t written = BytesWritten() - before;
std::printf("extending by 64 MiB wrote %llu bytes\n", static_cast<unsigned long long>(written));
#ifdef _WIN32
Require(written < static_cast<std::uint64_t>(Length) / 16, "extending a file must not write its new length as zero bytes: the process wrote about as many bytes as the new length");
#endif

Require(lseek_nid_postfix(descriptor, 0, 2) == Length, "the extended file has the wrong size");
Require(lseek_nid_postfix(descriptor, 0, 1) == Length, "ftruncate must not move the file offset");
char head[4] = {};
char tail[8] = {1, 1, 1, 1, 1, 1, 1, 1};
Require(pread_nid_postfix(descriptor, head, 4, 0) == 4 && std::memcmp(head, "head", 4) == 0, "extending must keep the existing bytes");
Require(pread_nid_postfix(descriptor, tail, 8, Length - 8) == 8 && tail[0] == 0 && tail[7] == 0, "the extension must read as zero bytes");

Require(ftruncate_nid_postfix(descriptor, 2) == 0, "shrinking failed");
Require(lseek_nid_postfix(descriptor, 0, 2) == 2, "the shrunk file has the wrong size");
Require(ftruncate_nid_postfix(descriptor, -1) == -1 && *__error_nid_postfix() == 22, "a negative length must fail with EINVAL (22)");
Require(close_nid_postfix(descriptor) == 0, "close failed");

const int readOnly = open_nid_postfix(FileName, ReadOnly, 0);
Require(readOnly >= 0, "reopen failed");
Require(ftruncate_nid_postfix(readOnly, 8) == -1 && *__error_nid_postfix() == 22, "ftruncate on a read-only descriptor must fail with EINVAL (22)");
Require(close_nid_postfix(readOnly) == 0, "close failed");

Require(truncate_nid_postfix(FileName, Length) == 0, "truncate to 64 MiB failed");
const int check = open_nid_postfix(FileName, ReadOnly, 0);
Require(check >= 0 && lseek_nid_postfix(check, 0, 2) == Length, "truncate by path left the wrong size");
Require(close_nid_postfix(check) == 0, "close failed");
Cleanup();
return 0;
}
1 change: 1 addition & 0 deletions docs/dev/debt/libkernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console
- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests
- [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it
- [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD makes the extension a hole on a file system that has them, so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap (a one-byte `pwrite` far behind the end takes as long as writing the gap), as the file is not sparse. None of this was measured on a console.
- [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it
- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown.
- [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields
Expand Down
Loading