Merge pull request #3568 from DariusHQ/feat/libkernel-sendfile

feat(libkernel): implement sendfile
This commit is contained in:
Adrià 2026-10-11 23:43:14 +02:00 committed by GitHub
commit 57ba0e7bca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 209 additions and 0 deletions

View File

@ -720,6 +720,16 @@ if(BUILD_TESTING)
set_tests_properties(guest_execvp PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_sendfile_tests ${LIBS_INCLUDE_DIR}/tests/GuestSendfile.cpp)
target_include_directories(guest_sendfile_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_sendfile_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_sendfile_tests)
add_test(NAME guest_sendfile COMMAND guest_sendfile_tests)
set_tests_properties(guest_sendfile PROPERTIES TIMEOUT 60)
if(WIN32)
set_tests_properties(guest_sendfile PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelErrors.cpp)
target_include_directories(guest_kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_kernel_error_tests PRIVATE libkernel libc)

View File

@ -858,6 +858,86 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message,
message->controlLength = 0;
return received;
}
struct GuestSendfileHeaders {
GuestIovec* headers;
int headerCount;
GuestIovec* trailers;
int trailerCount;
};
static_assert(sizeof(GuestSendfileHeaders) == 32 && offsetof(GuestSendfileHeaders, trailers) == 16);
std::int64_t APS5_VABI sceKernelPread(int descriptor, void* buffer, std::size_t length, std::int64_t offset);
static bool SendfileIovecs(const GuestIovec* iov, int count) {
if (!iov) return true;
if (count < 0 || count > 1024) return false;
std::uint64_t total = 0;
for (int i = 0; i < count; ++i) {
if (iov[i].length > INT_MAX - total) return false;
total += iov[i].length;
}
return true;
}
static int SendAll(const Socket& socket, const void* data, std::uint64_t length, std::int64_t& sent) {
if (!data && length) return 14;
const char* bytes = static_cast<const char*>(data);
while (length) {
const auto result = ::send(socket.value, bytes, static_cast<int>(std::min<std::uint64_t>(length, INT_MAX)), NativeSendFlags(GuestNoSignal));
if (result < 0) return NativeError();
bytes += result;
length -= static_cast<std::uint64_t>(result);
sent += result;
}
return 0;
}
static int SendIovecs(const Socket& socket, const GuestIovec* iov, int count, std::int64_t& sent) {
for (int i = 0; iov && i < count; ++i)
if (const int error = SendAll(socket, iov[i].base, iov[i].length, sent)) return error;
return 0;
}
int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset, std::uint64_t length,
const GuestSendfileHeaders* headers, std::int64_t* sbytes, int flags) {
(void)flags;
if (offset < 0) return Fail(22);
if (headers && (!SendfileIovecs(headers->headers, headers->headerCount) || !SendfileIovecs(headers->trailers, headers->trailerCount)))
return Fail(22);
std::vector<char> chunk;
try {
chunk.resize(0x10000);
} catch (const std::bad_alloc&) {
return Fail(12);
}
const auto read = [&](std::int64_t position, std::uint64_t remaining) {
const auto result = sceKernelPread(file, chunk.data(), static_cast<std::size_t>(std::min<std::uint64_t>(remaining, chunk.size())), position);
return result < 0 ? -(result & 0xffff) : result;
};
std::uint64_t remaining = length ? length : UINT64_MAX;
auto received = read(offset, remaining);
if (received == -9) return Fail(9);
std::int64_t sent = 0;
const auto finish = [&](int error) {
if (sbytes) *sbytes = sent;
return error ? Fail(error) : 0;
};
if (received < 0) return finish(received == -21 || received == -29 ? 22 : static_cast<int>(-received));
const auto socket = Lookup(descriptor);
if (!socket) return finish(9);
if (socket->type != 1) return finish(22);
sockaddr_storage peer{};
socklen_t size = sizeof(peer);
if (::getpeername(socket->value, reinterpret_cast<sockaddr*>(&peer), &size)) return finish(57);
if (headers)
if (const int error = SendIovecs(*socket, headers->headers, headers->headerCount, sent)) return finish(error);
while (received > 0) {
if (const int error = SendAll(*socket, chunk.data(), static_cast<std::uint64_t>(received), sent)) return finish(error);
offset += received;
remaining -= static_cast<std::uint64_t>(received);
if (remaining == 0) break;
received = read(offset, remaining);
if (received < 0) return finish(static_cast<int>(-received));
}
if (headers)
if (const int error = SendIovecs(*socket, headers->trailers, headers->trailerCount, sent)) return finish(error);
return finish(0);
}
int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t count, int timeout) {
if (timeout < -1) return Fail(22);
if (count && !descriptors) return Fail(14);

View File

@ -0,0 +1,118 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <thread>
#include <vector>
struct Iovec {
const void* base;
std::uint64_t length;
};
struct Hdtr {
const Iovec* headers;
int headerCount;
const Iovec* trailers;
int trailerCount;
};
extern "C" {
int APS5_VABI open_nid_postfix(const char*, int, int);
int APS5_VABI close_nid_postfix(int);
std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int);
int APS5_VABI socket_nid_postfix(int, int, int);
int APS5_VABI socketpair_nid_postfix(int, int, int, int*);
std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int);
int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*);
int APS5_VABI sendfile_nid_postfix(int, int, std::int64_t, std::uint64_t, const Hdtr*, std::int64_t*, int);
int* APS5_VABI __error_nid_postfix();
}
static void Require(bool value) { if (!value) std::abort(); }
static bool Fails(int result, int error) { return result == -1 && *__error_nid_postfix() == error; }
static std::string Receive(int descriptor, std::size_t length) {
std::string data(length, 0);
std::size_t done = 0;
while (done < length) {
const auto result = recv_nid_postfix(descriptor, data.data() + done, length - done, 0);
Require(result > 0);
done += static_cast<std::size_t>(result);
}
return data;
}
static std::string Content(std::size_t size) {
std::string content(size, 0);
for (std::size_t i = 0; i < size; ++i) content[i] = static_cast<char>(i * 7 % 251);
return content;
}
static int OpenWritten(const char* path, const std::string& content) {
{
std::ofstream stream(path, std::ios::binary);
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
}
const int file = open_nid_postfix(path, 0, 0);
Require(file >= 0);
return file;
}
int main() {
const std::string content = Content(200000);
const auto size = static_cast<std::int64_t>(content.size());
const int file = OpenWritten("sendfile_probe.bin", content);
int pair[2];
Require(socketpair_nid_postfix(1, 1, 0, pair) == 0);
const Iovec header[] = {{"GET", 3}, {" ", 1}};
const Iovec trailer[] = {{"END", 3}};
const Hdtr parts{header, 2, trailer, 1};
std::int64_t sent = -1;
Require(sendfile_nid_postfix(file, pair[0], 5, 10, &parts, &sent, 0) == 0 && sent == 17);
Require(Receive(pair[1], 17) == "GET " + content.substr(5, 10) + "END");
Require(lseek_nid_postfix(file, 0, 1) == 0);
std::string rest;
std::thread reader([&] { rest = Receive(pair[1], content.size() - 100); });
Require(sendfile_nid_postfix(file, pair[0], 100, 0, nullptr, &sent, 0) == 0 && sent == size - 100);
reader.join();
Require(rest == content.substr(100));
Require(sendfile_nid_postfix(file, pair[0], size - 4, 1000, nullptr, &sent, 0) == 0 && sent == 4);
Require(Receive(pair[1], 4) == content.substr(content.size() - 4));
Require(sendfile_nid_postfix(file, pair[0], size + 10, 0, &parts, &sent, 0) == 0 && sent == 7);
Require(Receive(pair[1], 7) == "GET END");
const Hdtr nullHeaders{nullptr, 5, trailer, 1};
Require(sendfile_nid_postfix(file, pair[0], 0, 3, &nullHeaders, nullptr, 0) == 0);
Require(Receive(pair[1], 6) == content.substr(0, 3) + "END");
Require(lseek_nid_postfix(file, 0, 1) == 0);
sent = 77;
Require(Fails(sendfile_nid_postfix(file, pair[0], -1, 0, nullptr, &sent, 0), 22) && sent == 77);
const std::vector<Iovec> many(1025, Iovec{"x", 1});
const Hdtr tooMany{many.data(), 1025, nullptr, 0};
Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &tooMany, &sent, 0), 22) && sent == 77);
const Hdtr negative{nullptr, 0, trailer, -1};
Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &negative, &sent, 0), 22) && sent == 77);
Require(Fails(sendfile_nid_postfix(pair[1], pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0);
sent = 77;
Require(Fails(sendfile_nid_postfix(file, file, 0, 0, nullptr, &sent, 0), 9) && sent == 0);
const int datagram = socket_nid_postfix(2, 2, 0);
Require(datagram >= 0);
sent = 77;
Require(Fails(sendfile_nid_postfix(file, datagram, 0, 0, nullptr, &sent, 0), 22) && sent == 0);
const int unconnected = socket_nid_postfix(2, 1, 0);
Require(unconnected >= 0);
sent = 77;
Require(Fails(sendfile_nid_postfix(file, unconnected, size + 10, 0, nullptr, &sent, 0), 57) && sent == 0);
const std::string large = Content(32 << 20);
const int largeFile = OpenWritten("sendfile_large.bin", large);
int on = 1;
Require(ioctl_nid_postfix(pair[0], 0x8004667e, &on) == 0);
sent = -1;
Require(Fails(sendfile_nid_postfix(largeFile, pair[0], 0, 0, nullptr, &sent, 0), 35));
Require(sent > 0 && sent < static_cast<std::int64_t>(large.size()));
Require(Receive(pair[1], static_cast<std::size_t>(sent)) == large.substr(0, static_cast<std::size_t>(sent)));
Require(close_nid_postfix(largeFile) == 0 && close_nid_postfix(file) == 0);
sent = 77;
Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, nullptr, &sent, 0), 9) && sent == 77);
Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(unconnected) == 0);
Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0);
std::filesystem::remove("sendfile_probe.bin");
std::filesystem::remove("sendfile_large.bin");
}

View File

@ -609,6 +609,7 @@ Throughout the project, every function at every stage either **does exactly what
- [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported
- [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. 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. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. 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
- [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 throws, since guest signals are not delivered to a chosen thread
- [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.
- [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix.