Repository navigation
Conversation
Every unverified point of the libSceSsl debt entry was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload against a TLS 1.2 test server presenting leaves issued by the test fixture root. The code follows the console where it differed: initial verify options 0x25 with the date checks off, bits outside 0x3F rejected, the verify callback called with the context id and the bitmask of failed checks and re-run from every later call after a negative return, sceSslRead flags 0 reading until the requested length, streams that end without close_notify reading as 0, resets reported with the libSceNet codes, receive timeouts as SSL_ERROR_WANT_POLLIN, a fatal alert ending the transport, sceSslClose as a repeatable no-op before the handshake, read and write before connect running the handshake, SSL_ERROR_INVALID_ID for unknown or deleted handles and terminated contexts, sceSslSetMinSslVersion accepting 2 to 5, an empty ALPN list rejected, long attribute names from sceSslGetNameEntryInfo and sceSslGetSerialNumber leaving the caller's length. The debt entries record the measured values and what stays unmeasured.
libSceSsl measured on a PS5, system software 13.40 (2026-10-19)Every number in this file is console output. Nothing here is inferred from an emulator, SharpProspero or KytyPS5; the only inferences are marked as such. Method
What the console sendsClientHello (144 bytes, every connection): record version 0x0301, client version 0x0303, cipher suites SNI is the host string as given ( The first Handles and pool
Initial verify options and
|
| Context | Server | Options | sceSslConnect |
|---|---|---|---|
| no CA | good | default | 0x8095F00C UNKNOWN_CA |
| root | cnmismatch | default | 0x8095F00B CN_CHECK |
| root | expired | default | 0 (accepted, data read) |
| root | notyet | default | 0 (accepted) |
| root | expired | enable 0x8 | 0x8095F00D NOT_AFTER_CHECK |
| root | expired | enable 0x10 | 0 |
| root | notyet | enable 0x10 | 0x8095F00E NOT_BEFORE_CHECK |
| root | notyet | enable 0x8 | 0 |
| root | expired | enable 0x8 then disable 0x8 | 0 |
| root | expired | disable 0x8 | 0 |
| root | expired | disable 0x10 | 0 |
| root | notyet | disable 0x10 | 0 |
| root | notyet | disable 0x8 | 0 |
| no CA | good | disable 0x2 | 0x8095F00C |
| root | good | enable 0x3F | 0 |
| root | good | enable 0x1000, disable 0x1000 (both return 0x8095F007), enable 0 (0) | 0 |
So the initial options are SERVER_VERIFY 0x1, CN_CHECK 0x4 and KNOWN_CA_CHECK 0x20 on, NOT_AFTER_CHECK 0x8 and NOT_BEFORE_CHECK 0x10 off (the 0x1/0x4/0x20 part is inferred from the failures above plus the disable results below; it was not read back, there is no getter). Whether 0x2 (CLIENT_VERIFY in SharpProspero) starts on or off was not determined.
Return codes of the option calls themselves:
| Call | Return |
|---|---|
sceSslEnableVerifyOption(c, 0x4 / 0x8 / 0x10 / 0x3F / 0) |
0 |
sceSslEnableVerifyOption(c, 0x1000), sceSslDisableVerifyOption(c, 0x40 / 0x100 / 0x1000) |
0x8095F007 INVALID_VALUE (bits outside 0x3F are rejected) |
sceSslDisableVerifyOption(c, 0x2 / 0x8 / 0x10) |
0 |
sceSslDisableVerifyOption(c, 0x1), (c, 0x4), (c, 0x20) |
no return: the process is stopped. Kernel log: mDBG: Sending signal(pid: N, tid: T, signo: 0xa0020101); ps then shows the payload in state STOP, kill(pid, SIGKILL) returns 0 and does not remove it. Reproduced 6/6 (runs 1, opt, dis-0x1, dis-0x4, dis-0x20 and the klog run), each as the first disable of that bit on a fresh connection before sceSslConnect; disabling 0x4 after enabling 0x4 on the same connection stops the process too. |
sceSsl{Enable,Disable}VerifyOption(99999, ...) |
0x8095F006 INVALID_ID |
The stop is from an elfldr payload (SceRedisServer lineage). Titles import sceSslDisableVerifyOption; whether a title process is allowed to clear 0x1/0x4/0x20 was not measured (it needs the homebrew-as-app route). Signal number 0xa0020101 is the console's debug-exception path, not a POSIX signal: a sigaction handler installed for SIGSEGV/SIGBUS/SIGILL/SIGABRT/SIGTRAP/SIGFPE/SIGSYS never ran.
Verify callback
int cb(int id, unsigned int err, void** certs, int num, void* userArg) set with sceSslSetVerifyCallback(c, cb, &arg) = 0.
| Case | Callback arguments | sceSslConnect |
Notes |
|---|---|---|---|
| root, good | id = 2 (the context id, not the connection id 0x1000001c), err = 0, num = 2, userArg as given | 0 | called once; a second sceSslConnect returns 0 without calling it again |
| root, cnmismatch, cb returns 0 | id 2, err = 0x4 | 0 (accepted, data read) | the callback's return overrides the failure |
| root, cnmismatch, cb returns 1 | err 0x4 | 0 | |
| no CA, good, cb returns 0 | id 1, err = 0x20 | 0 | |
| root, expired, 0x8 disabled, cb returns 0 | err 0 | 0 | |
| root, good, cb returns -1 | err 0 | 0x8095F0FD APPLICATION_VERIFICATION_CALLBACK_CHECK |
the callback was then called again by every following call: second sceSslConnect (-> 0x8095F0FD), sceSslRead (0x8095F0FD, 5 ms), sceSslConnect again, sceSslRead, sceSslWrite (0x8095F0FD each, 5 callbacks in total); sceSslClose 0, twice 0 |
callback set then cleared with sceSslSetVerifyCallback(c, NULL, NULL) = 0, cnmismatch |
not called | 0x8095F00B |
err is the bitmask of the verify options that failed (0x4 = CN_CHECK, 0x20 = KNOWN_CA_CHECK, 0 when the chain passed), not an SSL_ERROR_* code. The same pair comes back from sceSslGetSslError.
certs[0] is the leaf, certs[1] the root the server sent (num 2 = the chain as received). The elements are certificate handles for the context's readers: sceSslGetSerialNumber(ctx, certs[0], buf[32], &len=32) = 0 writes the serial left-aligned (10 01 for serial 0x1001, 80 a1 for the root) and leaves len at 32; sceSslGetSubjectName/sceSslGetIssuerName(ctx, cert) return name handles; sceSslGetNameEntryCount = 3; sceSslGetNameEntryInfo(ctx, name, i, oid[64], 64, value[128], 128, &valueLen) = 0 with oid = countryName, organizationName, commonName (long names) and valueLen = exact length (2, 12, 15/17/19); sceSslFreeSslCertName = 0. The first 24 bytes at a handle are three pointers (a struct with a vtable-like layout, not DER). The handles stay valid after sceSslConnect returns (re-read successfully) and are freed by sceSslDeleteConnection: the memory read back as zeros and sceSslGetSerialNumber on the stale handle crashed the payload (SIGSEGV at the handle's first pointer), so they must not be used after the delete.
Error codes by situation
Root loaded, good certificate unless stated. "read" is sceSslRead(c, buf, 64, flags).
| Situation | Call | Return |
|---|---|---|
| server sends 10 bytes, 0.6 s later 10 more, waits 3 s, close_notify | read flags 0 | 20 after 3605 ms (`piece-one |
| same | read flags 1 | 10 after 108 ms, then 10 after 498 ms: flags 1 returns what is available (MsgFlagPartialRead) |
| same, exact lengths | read 5 flags 0 -> 5 (piece) after 126 ms; read 5 flags 1 -> 5 (`-one |
); read 64 flags 2 -> 10 after 475 ms (piece-two |
| peer close_notify received | read (0 or 1) | 0, every later read 0 (never 0x8095177C SSL_ERROR_EOF) |
| peer close_notify received | write 15 bytes | 15 (the server logged APPLICATION_DATA_AFTER_CLOSE_NOTIFY) |
| server closed TCP without close_notify (FIN) | read flags 1 | 14 (`before-abrupt |
| same | write | 13, 15, 14 (accepted by the kernel) |
| same | sceSslConnect again |
0; sceSslClose 0, again 0 |
| server reset (SO_LINGER 0) | read flags 1 | 13 (`before-reset |
| same | write | 0x80410120 (SCE_NET_ERROR_EPIPE) each time |
| same | sceSslConnect again |
0; sceSslClose -> 0x80410136, second close 0x80410136 |
blocking socket, SO_RCVTIMEO = 2 s (16-byte timeval; a 4-byte int returns 0x80410116 EINVAL), silent server |
read flags 1 / 0 | 0x8095F013 WANT_POLLIN after 2001 ms each (never 0x80951710 READ_TIMEOUT); write afterwards 14, sceSslConnect 0, sceSslClose 0, read after close 0 |
non-blocking socket (SO_NBIO 0x1200 = 1), nothing pending |
read flags 0 / 1 | 0x8095F013 immediately; write 9 -> 9 |
| non-blocking handshake | sceSslConnect |
0x8095F013, poll(POLLIN) readable after 11 ms, 0x8095F013, poll readable after 9 ms, 0 (three calls); read before data 0x8095F013, after poll 10 |
| non-blocking, server never reads | write 64 KiB | 32768 (short write), then 0x8095F014 WANT_POLLOUT on each further write; poll(POLLOUT) says writable (kernel buffer) while the library still returns 0x8095F014; sceSslClose -> 0x8095F014 |
| non-blocking, cnmismatch | sceSslConnect |
0x8095F013, then 0x8095F00B; again 0x8095F00B; sceSslClose 0 |
| server sends a plaintext fatal alert 40 (handshake_failure) after the ClientHello and closes | sceSslConnect |
0x8095F010 FATAL_ALERT; sceSslConnect again -> 0 after 500 ms; read 0; write -> 0x8095170D SOCKET_CLOSED; sceSslClose 0, 0 |
| same with alert 70 (protocol_version) | identical: 0x8095F010, 0, read 0, write 0x8095170D, close 0, 0 | |
| TLS 1.3-only server (sends protocol_version alert) | sceSslConnect |
0x8095F010, read 0 |
| server sends an undecryptable application record after the handshake | read flags 1 | 17 (`before-badrecord |
| same | sceSslConnect again 0; read 0; write 14; close 0, 0 |
|
| verification failed (cnmismatch, default options) | sceSslConnect again |
0x8095F00B again (also non-blocking) |
sceSslConnect after success |
0 (no second handshake, callback not re-run) | |
sceSslClose before any connect |
0, and sceSslConnect afterwards -> 0 with data readable (close before connect is a no-op) |
|
sceSslClose after success, twice |
0, 0; then sceSslConnect 0, read 0 |
|
sceSslRead before any sceSslConnect |
read flags 0 | the handshake runs inside the read (server logged a normal handshake): 20 bytes after 3640 ms; sceSslWrite before connect 15; sceSslConnect afterwards 0 |
sceSslWrite(c, buf, 1, flags=1) |
0x8095F007 INVALID_VALUE (write accepts flags 0 only) |
sceSslGetSslError(c, &a, &b) (signature guessed as int, int*, int*) after a CN failure returned 0 with a = 0x8095F00A (INVALID_CERT) and b = 0x4 (the failed option bit).
ALPN
Server offers h2 and http/1.1 unless stated.
sceSslSetAlpn(c, s) |
Return | ClientHello ALPN names as the server decoded them | sceSslGetAlpnSelected |
|---|---|---|---|
| not called | no ALPN extension | NULL before and after | |
"h2,http/1.1" |
0 | h2, http/1.1 (two names: the string is split on commas) |
NULL before connect, "h2" after, still "h2" after sceSslClose |
"\x02h2\x08http/1.1" (wire format) |
0 | one name \x02h2\x08http/1.1 |
NULL (no match) |
"http/1.1" |
0 | http/1.1 |
"http/1.1" |
"h2,http/1.1" against a server offering http/1.1 only |
0 | "http/1.1" |
|
"h2,http/1.1" against a server without ALPN |
0 | NULL | |
"h2, http/1.1" then "spdy/3,h2" |
0, 0 | spdy/3, h2 (the second call replaces the list) |
"h2" |
"" |
0x80951773 BAD_LENGTH |
no ALPN extension | NULL |
sceSslSetAlpn(99999, "h2") |
0x8095F006 |
sceSslSetMinSslVersion
| version | Return | sceSslConnect to the TLS 1.2 server |
to the TLS 1.3-only server |
|---|---|---|---|
| -1, 0, 1 | 0x8095F007 INVALID_VALUE |
0 (the rejected call changed nothing) | |
| 2 (SSL 3.0), 3 (TLS 1.0), 4 (TLS 1.1), 5 (TLS 1.2) | 0 | 0 (version 3 took 361 ms instead of ~20 ms once; same ClientHello) | |
| 6, 7, 8, 9 | 0x8095F007 | 0 | 0x8095F010 |
| default | 0 | 0x8095F010 |
There is no TLS 1.3 value: 6 and above are rejected and the library never offers 1.3.
Handles, ordering, invalid arguments
| Call | Return |
|---|---|
sceSslClose, sceSslConnect, sceSslRead, sceSslDeleteConnection, sceSslSetAlpn, sceSslEnableVerifyOption, sceSslSetMinSslVersion, sceSslSetVerifyCallback on connection 99999 |
0x8095F006 INVALID_ID |
sceSslCreateConnection(99999, sock, host) |
0x8095F006 |
sceSslCreateConnection(ctx, -1, host), (ctx, 0, host), (ctx, 1234567, host) |
0x1000001e / 0x10000020 / 0x1000001f (the socket is not checked) |
sceSslConnect on the 1234567 one |
0x80410109 (SCE_NET_ERROR_EBADF) |
sceSslCreateConnection(ctx, sock, "") |
a handle; sceSslConnect -> 0x8095F00B (empty SNI sent, server alerted bad certificate) |
sceSslCreateConnection(ctx, sock, NULL) |
0x8095F007 INVALID_VALUE |
sceSslCreateConnection(ctx, sock, "192.168.1.176") |
handle; connect 0 (no SNI sent, matched the IP SAN) |
sceSslCreateConnection(ctx, sock, "MAC.ANYPS5.TEST") |
handle; connect 0 (case-insensitive) |
sceSslDeleteConnection without close after a successful connect |
0 (the server saw EOF without close_notify) |
second sceSslDeleteConnection, then sceSslClose, sceSslConnect on it |
0x8095F006 each |
sceSslTerm(ctx3) with a live connection |
0; then sceSslClose, sceSslConnect, sceSslDeleteConnection on that connection 0x8095F006 |
sceSslTerm(ctx3) again, sceSslTerm(99999) |
0x8095F006 |
sceSslSetVerifyCallback(c, NULL, NULL) |
0 |
Not measured
- Whether a title process survives
sceSslDisableVerifyOptionof 0x1/0x4/0x20 (needs a game-type process). SSL_ERROR_READ_TIMEOUT,SSL_ERROR_EOF,SSL_ERROR_INTERNALwere never produced; a situation that returns 0x8095F00AINVALID_CERTfromsceSslConnectwas not found (a chain problem other than CA, CN and dates was not set up).- The default of option 0x2, client certificates,
sceSslSetSslVersion,sceSslGetPeerCert,sceSslCheckRecvPending,sceSslRecv/sceSslSend,sceSslReuseConnection,sceSslLoadRootCACert(all exported, not called). - The verify callback's behaviour when it calls into the same connection, and threads.
AI-assisted: yes (Claude Code).
|
Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.
|
What
Every "unverified" and "unknown" sentence #7626 left in
docs/dev/debt/libSceSsl.mdwas measured on a PS5 on system software 13.40 with a ps5-payload-dev payload against a TLS 1.2 test server presenting certificates issued by the test fixture root (the measurement log is the first comment below; the payload, the test server and the raw console logs are attached as files by the author). The code follows the console where it differed:SERVER_VERIFY | CN_CHECK | KNOWN_CA_CHECK(0x25): the console accepts expired and not-yet-valid certificates untilNOT_AFTER_CHECK/NOT_BEFORE_CHECKis enabled. Bits outside 0x3F returnSSL_ERROR_INVALID_VALUEfromsceSslEnableVerifyOption/sceSslDisableVerifyOption.sceSslConnect,sceSslReadandsceSslWrite, each returningSSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECKwhile it refuses.sceSslReadwith flags 0 reads until the requested length or the end of the stream;MsgFlagPartialReadreturns what is available. A stream that ends without close_notify reads as 0 like a close_notify; a reset returnsSCE_NET_ERROR_ECONNRESET(0x80410136) from read and close andSCE_NET_ERROR_EPIPE(0x80410120) from write; a blocking socket's receive timeout returnsSSL_ERROR_WANT_POLLIN, soSSL_ERROR_READ_TIMEOUTis gone; a record that fails its MAC returns 0x80951D55.SSL_ERROR_FATAL_ALERTand ends the transport: later connects return 0, reads 0, writesSSL_ERROR_SOCKET_CLOSED.sceSslClosebefore the handshake is a no-op, the connection stays usable forsceSslConnect(0) andsceSslRead(0) after a close, a close_notify that would block returnsSSL_ERROR_WANT_POLLOUT.sceSslRead/sceSslWritebeforesceSslConnectrun the handshake.sceSslWritewith flags other than 0 returnsSSL_ERROR_INVALID_VALUE.SSL_ERROR_INVALID_IDfrom every connection call;sceSslTermof an unknown context too;sceSslInit(0)returnsSCE_KERNEL_ERROR_EINVAL; a null host returnsSSL_ERROR_INVALID_VALUE; a descriptor that is not a socket failssceSslConnectwithSCE_NET_ERROR_EBADF.sceSslSetMinSslVersionaccepts 2..5 only (SSL_ERROR_INVALID_VALUEotherwise; the console has no TLS 1.3 value and offers TLS 1.2 only, which this build already matches).sceSslSetAlpnsplits on commas, measured on the wire; an empty list returnsSSL_ERROR_BAD_LENGTH.sceSslGetNameEntryInfowrites long attribute names (commonName);sceSslGetSerialNumberleaves*sboLenalone when the buffer is large enough.SSL_ERROR_INVALID_CERTand what the callback gets for it, read/write after a plain verification failure, write aftersceSslClose, client certificates, the callback using its own connection). Disabling 0x1, 0x4 or 0x20 stopped the payload process through the kernel's debug exception path; the emulator keeps clearing them because titles import the call, and the entry records it.Tested
PS5, system software 13.40, ps5-payload-dev elfldr v0.26, payload built from the attached
main.c(ps5-sslprobe) with the SDK built from source on macOS; serverserver.pywith OpenSSL 4.0.3; the attached logs are the console's output.Emulator: Ubuntu 24.04 x86-64 container (GCC 13, Release,
BUILD_TESTING=ON) on 2dcc941 + this commit:guest_ssl,guest_ssl_load,guest_ssl_transport_kernel,guest_ssl_transport_net,guest_ssl_ps5_readerspass, 3 runs each, with the host CPU check of 83a268a bypassed locally for the run because the container runs under Rosetta without AVX2 (the bypass is not in the commit);python3 tools/check_conventions.py --base origin/mainpasses. Not tested: Windows and macOS, a title.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in the technical debt file.mdfiles or images added to the repository (measurement log in the first comment, payload and logs attached to this PR)r14+0x48) while the title's video-out/flip frame-pacing loop runs #6350 uses these calls on its sign-in path)