Update to psfree-1.5rc1
900 Port in process: DO NOT USE IT.
This commit is contained in:
+459
-83
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2023 anonymous
|
||||
/* Copyright (C) 2023-2025 anonymous
|
||||
|
||||
This file is part of PSFree.
|
||||
|
||||
@@ -15,46 +15,82 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Int } from './int64.mjs';
|
||||
import { Int, lohi_from_one } from './int64.mjs';
|
||||
import { get_view_vector } from './memtools.mjs';
|
||||
import { Addr, mem } from './mem.mjs';
|
||||
import { Addr } from './mem.mjs';
|
||||
|
||||
import {
|
||||
read64,
|
||||
write64,
|
||||
} from './rw.mjs';
|
||||
|
||||
import * as o from './offset.mjs';
|
||||
import * as config from '/config.mjs';
|
||||
|
||||
// put the sycall names that you want to use here
|
||||
export const syscall_map = new Map(Object.entries({
|
||||
'close': 6,
|
||||
'read' : 3,
|
||||
'write' : 4,
|
||||
'open' : 5,
|
||||
'close' : 6,
|
||||
'getpid' : 20,
|
||||
'setuid' : 23,
|
||||
'getuid' : 24,
|
||||
'mprotect': 74,
|
||||
'accept' : 30,
|
||||
'pipe' : 42,
|
||||
'ioctl' : 54,
|
||||
'munmap' : 73,
|
||||
'mprotect' : 74,
|
||||
'fcntl' : 92,
|
||||
'socket' : 97,
|
||||
'connect' : 98,
|
||||
'bind' : 104,
|
||||
'setsockopt' : 105,
|
||||
'listen' : 106,
|
||||
'getsockopt' : 118,
|
||||
'fchmod' : 124,
|
||||
'socketpair' : 135,
|
||||
'fstat' : 189,
|
||||
'getdirentries' : 196,
|
||||
'__sysctl' : 202,
|
||||
'mlock' : 203,
|
||||
'clock_gettime' : 232,
|
||||
'nanosleep' : 240,
|
||||
'sched_yield' : 331,
|
||||
'kqueue' : 362,
|
||||
'kevent' : 363,
|
||||
'rtprio_thread' : 466,
|
||||
'mmap' : 477,
|
||||
// for JIT shared memory
|
||||
'ftruncate' : 480,
|
||||
'shm_open' : 482,
|
||||
'cpuset_getaffinity' : 487,
|
||||
'cpuset_setaffinity' : 488,
|
||||
'jitshm_create' : 533,
|
||||
'jitshm_alias' : 534,
|
||||
'evf_create' : 538,
|
||||
'evf_delete' : 539,
|
||||
'evf_set' : 544,
|
||||
'evf_clear' : 545,
|
||||
'set_vm_container' : 559,
|
||||
'dmem_container' : 586,
|
||||
'dynlib_dlsym' : 591,
|
||||
'dynlib_get_list' : 592,
|
||||
'dynlib_get_info' : 593,
|
||||
'dynlib_load_prx' : 594,
|
||||
'randomized_path' : 602,
|
||||
'budget_get_ptype' : 610,
|
||||
'thr_suspend_ucontext' : 632,
|
||||
'thr_resume_ucontext' : 633,
|
||||
'blockpool_open' : 653,
|
||||
'blockpool_map' : 654,
|
||||
'blockpool_unmap' : 655,
|
||||
'blockpool_batch' : 657,
|
||||
// syscall 661 is unimplemented so free for use. a kernel exploit will
|
||||
// install "kexec" here
|
||||
'aio_submit' : 661,
|
||||
'kexec' : 661,
|
||||
'aio_multi_delete' : 662,
|
||||
'aio_multi_wait' : 663,
|
||||
'aio_multi_poll' : 664,
|
||||
'aio_multi_cancel' : 666,
|
||||
'aio_submit_cmd' : 669,
|
||||
'blockpool_move' : 673,
|
||||
}));
|
||||
|
||||
// Extra space to allow a ROP chain to push temporary values. It must pop all
|
||||
// of it before reaching a "ret" instruction, else the instruction will pop one
|
||||
// of the temporaries as its return address.
|
||||
//
|
||||
// Also space for additional frames when we call a function since we do not
|
||||
// pivot the call to another stack (the called function's stack pointer is
|
||||
// pointing to our ROP stack as well).
|
||||
const upper_pad = 0x10000;
|
||||
// maximum size of the ROP stack
|
||||
const stack_size = 0x10000;
|
||||
const total_size = upper_pad + stack_size;
|
||||
|
||||
const argument_pops = [
|
||||
'pop rdi; ret',
|
||||
'pop rsi; ret',
|
||||
@@ -64,49 +100,190 @@ const argument_pops = [
|
||||
'pop r9; ret',
|
||||
];
|
||||
|
||||
// implementations are expected to have these gadgets:
|
||||
// * libSceLibcInternal:
|
||||
// * __errno - FreeBSD's function to get the location of errno
|
||||
// * setcontext - what we call Sony's own version of _Ux86_64_setcontext
|
||||
// * getcontext - what we call Sony's own version of _Ux86_64_getcontext
|
||||
// * anywhere:
|
||||
// * the gadgets at argument_pops
|
||||
// * ret
|
||||
//
|
||||
// setcontext/getcontext naming came from this project:
|
||||
// https://github.com/libunwind/libunwind
|
||||
//
|
||||
// setcontext(context *ctx):
|
||||
// mov rax, qword [rdi + 0x38]
|
||||
// sub rax, 0x10 ; 16
|
||||
// mov qword [rdi + 0x38], rax
|
||||
// mov rbx, qword [rdi + 0x20]
|
||||
// mov qword [rax], rbx
|
||||
// mov rbx, qword [rdi + 0x80]
|
||||
// mov qword [rax + 8], rbx
|
||||
// mov rax, qword [rdi]
|
||||
// mov rbx, qword [rdi + 8]
|
||||
// mov rcx, qword [rdi + 0x10]
|
||||
// mov rdx, qword [rdi + 0x18]
|
||||
// mov rsi, qword [rdi + 0x28]
|
||||
// mov rbp, qword [rdi + 0x30]
|
||||
// mov r8, qword [rdi + 0x40]
|
||||
// mov r9, qword [rdi + 0x48]
|
||||
// mov r10, qword [rdi + 0x50]
|
||||
// mov r11, qword [rdi + 0x58]
|
||||
// mov r12, qword [rdi + 0x60]
|
||||
// mov r13, qword [rdi + 0x68]
|
||||
// mov r14, qword [rdi + 0x70]
|
||||
// mov r15, qword [rdi + 0x78]
|
||||
// cmp qword [rdi + 0xb0], 0x20001
|
||||
// jne done
|
||||
// cmp qword [rdi + 0xb8], 0x10002
|
||||
// jne done
|
||||
// fxrstor [rdi + 0xc0]
|
||||
// done:
|
||||
// mov rsp, qword [rdi + 0x38]
|
||||
// pop rdi
|
||||
// ret
|
||||
//
|
||||
// getcontext(context *ctx):
|
||||
// mov qword [rdi], rax
|
||||
// mov qword [rdi + 8], rbx
|
||||
// mov qword [rdi + 0x10], rcx
|
||||
// mov qword [rdi + 0x18], rdx
|
||||
// mov qword [rdi + 0x20], rdi
|
||||
// mov qword [rdi + 0x28], rsi
|
||||
// mov qword [rdi + 0x30], rbp
|
||||
// mov qword [rdi + 0x38], rsp
|
||||
// add qword [rdi + 0x38], 8
|
||||
// mov qword [rdi + 0x40], r8
|
||||
// mov qword [rdi + 0x48], r9
|
||||
// mov qword [rdi + 0x50], r10
|
||||
// mov qword [rdi + 0x58], r11
|
||||
// mov qword [rdi + 0x60], r12
|
||||
// mov qword [rdi + 0x68], r13
|
||||
// mov qword [rdi + 0x70], r14
|
||||
// mov qword [rdi + 0x78], r15
|
||||
// mov rsi, qword [rsp]
|
||||
// mov qword [rdi + 0x80], rsi
|
||||
// fxsave [rdi + 0xc0]
|
||||
// mov qword [rdi + 0xb0], 0x20001
|
||||
// mov qword [rdi + 0xb8], 0x10002
|
||||
// xor eax, eax
|
||||
// ret
|
||||
|
||||
// ROP chain manager base class
|
||||
//
|
||||
// Args:
|
||||
// stack_size: the size of the stack
|
||||
// upper_pad: the amount of extra space above stack
|
||||
export class ChainBase {
|
||||
constructor() {
|
||||
this.is_stale = false;
|
||||
constructor(stack_size=0x1000, upper_pad=0x10000) {
|
||||
this._is_dirty = false;
|
||||
this.position = 0;
|
||||
this._return_value = new Uint8Array(8);
|
||||
this.retval_addr = get_view_vector(this._return_value);
|
||||
|
||||
const stack_buffer = new ArrayBuffer(total_size);
|
||||
this.stack_buffer = stack_buffer;
|
||||
this.stack = new Uint8Array(stack_buffer, upper_pad, stack_size);
|
||||
this.stack_addr = get_view_vector(this.stack);
|
||||
const return_value = new Uint32Array(4);
|
||||
this._return_value = return_value;
|
||||
this.retval_addr = get_view_vector(return_value);
|
||||
|
||||
const errno = new Uint32Array(1);
|
||||
this._errno = errno;
|
||||
this.errno_addr = get_view_vector(errno);
|
||||
|
||||
const full_stack_size = upper_pad + stack_size;
|
||||
const stack_buffer = new ArrayBuffer(full_stack_size);
|
||||
const stack = new DataView(stack_buffer, upper_pad);
|
||||
this.stack = stack;
|
||||
this.stack_addr = get_view_vector(stack);
|
||||
this.stack_size = stack_size;
|
||||
this.full_stack_size = full_stack_size;
|
||||
}
|
||||
|
||||
check_stale() {
|
||||
if (this.is_stale) {
|
||||
throw Error('chain already ran, clean it first');
|
||||
}
|
||||
this.is_stale = true;
|
||||
// use this if you want to write a new ROP chain but don't want to allocate
|
||||
// a new instance
|
||||
empty() {
|
||||
this.position = 0;
|
||||
}
|
||||
|
||||
check_is_empty() {
|
||||
if (this.position === 0) {
|
||||
throw Error('chain is empty');
|
||||
}
|
||||
// flag indicating whether .run() was ever called with this chain
|
||||
get is_dirty() {
|
||||
return this._is_dirty;
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.position = 0;
|
||||
this.is_stale = false;
|
||||
this._is_dirty = false;
|
||||
}
|
||||
|
||||
dirty() {
|
||||
this._is_dirty = true;
|
||||
}
|
||||
|
||||
check_allow_run() {
|
||||
if (this.position === 0) {
|
||||
throw Error('chain is empty');
|
||||
}
|
||||
if (this.is_dirty) {
|
||||
throw Error('chain already ran, clean it first');
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.empty();
|
||||
this.clean();
|
||||
}
|
||||
|
||||
get retval_int() {
|
||||
return this._return_value[0] | 0;
|
||||
}
|
||||
|
||||
get retval() {
|
||||
return new Int(this._return_value[0], this._return_value[1]);
|
||||
}
|
||||
|
||||
// return value as a pointer
|
||||
get retval_ptr() {
|
||||
return new Addr(this._return_value[0], this._return_value[1]);
|
||||
}
|
||||
|
||||
set retval(value) {
|
||||
const values = lohi_from_one(value);
|
||||
const retval = this._return_value;
|
||||
retval[0] = values[0];
|
||||
retval[1] = values[1];
|
||||
}
|
||||
|
||||
get retval_all() {
|
||||
const retval = this._return_value;
|
||||
return [new Int(retval[0], retval[1]), new Int(retval[2], retval[3])];
|
||||
}
|
||||
|
||||
set retval_all(values) {
|
||||
const [a, b] = [lohi_from_one(values[0]), lohi_from_one(values[1])];
|
||||
const retval = this._return_value;
|
||||
retval[0] = a[0];
|
||||
retval[1] = a[1];
|
||||
retval[2] = b[0];
|
||||
retval[3] = b[1];
|
||||
}
|
||||
|
||||
get errno() {
|
||||
return this._errno[0];
|
||||
}
|
||||
|
||||
set errno(value) {
|
||||
this._errno[0] = value;
|
||||
}
|
||||
|
||||
// this will raise an error if the value is not an Int
|
||||
push_value(value) {
|
||||
if (this.position >= stack_size) {
|
||||
const position = this.position;
|
||||
if (position >= this.stack_size) {
|
||||
throw Error(`no more space on the stack, pushed value: ${value}`);
|
||||
}
|
||||
write64(this.stack, this.position, value);
|
||||
this.position += 8;
|
||||
}
|
||||
|
||||
// converts value to Int first
|
||||
push_constant(value) {
|
||||
this.push_value(new Int(value));
|
||||
const values = lohi_from_one(value);
|
||||
const stack = this.stack;
|
||||
stack.setUint32(position, values[0], true);
|
||||
stack.setUint32(position + 4, values[1], true);
|
||||
|
||||
this.position += 8;
|
||||
}
|
||||
|
||||
get_gadget(insn_str) {
|
||||
@@ -125,14 +302,13 @@ export class ChainBase {
|
||||
push_call(func_addr, ...args) {
|
||||
if (args.length > 6) {
|
||||
throw TypeError(
|
||||
'call() does not support functions that have more than 6'
|
||||
+ ' arguments'
|
||||
);
|
||||
'push_call() does not support functions that have more than 6'
|
||||
+ ' arguments');
|
||||
}
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
this.push_gadget(argument_pops[i]);
|
||||
this.push_constant(args[i]);
|
||||
this.push_value(args[i]);
|
||||
}
|
||||
|
||||
// The address of our buffer seems to be always aligned to 8 bytes.
|
||||
@@ -143,7 +319,11 @@ export class ChainBase {
|
||||
this.push_gadget('ret');
|
||||
}
|
||||
|
||||
this.push_value(func_addr);
|
||||
if (typeof func_addr === 'string') {
|
||||
this.push_gadget(func_addr);
|
||||
} else {
|
||||
this.push_value(func_addr);
|
||||
}
|
||||
}
|
||||
|
||||
push_syscall(syscall_name, ...args) {
|
||||
@@ -164,43 +344,239 @@ export class ChainBase {
|
||||
this.push_call(syscall_addr, ...args);
|
||||
}
|
||||
|
||||
// ROP chain to retrieve rax
|
||||
push_get_retval() {
|
||||
throw Error('push_get_retval() not implemented');
|
||||
}
|
||||
|
||||
// Firmware specific method to launch a ROP chain
|
||||
//
|
||||
// Implementations must call check_stale() and check_is_empty() before
|
||||
// trying to launch the chain.
|
||||
run() {
|
||||
throw Error('run() not implemented');
|
||||
}
|
||||
|
||||
get return_value() {
|
||||
return read64(this._return_value, 0);
|
||||
}
|
||||
|
||||
// Sets needed class properties
|
||||
//
|
||||
// Args:
|
||||
// gadgets:
|
||||
// A Map-like object mapping instruction strings (e.g "pop rax; ret")
|
||||
// A Map-like object mapping instruction strings (e.g. "pop rax; ret")
|
||||
// to their addresses in memory.
|
||||
// syscall_array:
|
||||
// An array whose indices correspond to syscall numbers. Maps syscall
|
||||
// numbers to their addresses in memory. Defaults to an empty Array.
|
||||
//
|
||||
// Raises:
|
||||
// Error:
|
||||
// For missing bare minimum gadgets
|
||||
static init_class(gadgets, syscall_array=[]) {
|
||||
for (const insn of argument_pops) {
|
||||
if (!gadgets.has(insn)) {
|
||||
throw Error(`gadget map must contain this gadget: ${insn}`);
|
||||
}
|
||||
}
|
||||
this.prototype.gadgets = gadgets;
|
||||
this.prototype.syscall_array = syscall_array;
|
||||
}
|
||||
|
||||
// START: implementation-dependent parts
|
||||
//
|
||||
// the user doesn't need to implement all of these. just the ones they need
|
||||
|
||||
// Firmware specific method to launch a ROP chain
|
||||
//
|
||||
// Proper implementations will check if .position is nonzero before
|
||||
// running. Implementations can optionally check .is_dirty to enforce
|
||||
// single-run gadget sequences
|
||||
run() {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
|
||||
// anything you need to do before the ROP chain jumps back to JavaScript
|
||||
push_end() {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
|
||||
push_get_errno() {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
|
||||
push_clear_errno() {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
|
||||
// get the rax register
|
||||
push_get_retval() {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
|
||||
// get the rax and rdx registers
|
||||
push_get_retval_all() {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
|
||||
// END: implementation-dependent parts
|
||||
|
||||
// note that later firmwares (starting around > 5.00?), the browser doesn't
|
||||
// have a JIT compiler. we programmed in a way that tries to make the
|
||||
// resulting bytecode be optimal
|
||||
//
|
||||
// we intentionally have an incomplete set (there's no function to get a
|
||||
// full 128-bit result). we only implemented what we think are the common
|
||||
// cases. the user will have to implement those other functions if they
|
||||
// need it
|
||||
|
||||
do_call(...args) {
|
||||
if (this.position) {
|
||||
throw Error('chain not empty');
|
||||
}
|
||||
try {
|
||||
this.push_call(...args);
|
||||
this.push_get_retval();
|
||||
this.push_get_errno();
|
||||
this.push_end();
|
||||
this.run();
|
||||
} finally {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
call_void(...args) {
|
||||
this.do_call(...args);
|
||||
}
|
||||
|
||||
call_int(...args) {
|
||||
this.do_call(...args);
|
||||
// x | 0 will always be a signed integer
|
||||
return this._return_value[0] | 0;
|
||||
}
|
||||
|
||||
call(...args) {
|
||||
this.do_call(...args);
|
||||
const retval = this._return_value;
|
||||
return new Int(retval[0], retval[1]);
|
||||
}
|
||||
|
||||
do_syscall(...args) {
|
||||
if (this.position) {
|
||||
throw Error('chain not empty');
|
||||
}
|
||||
try {
|
||||
this.push_syscall(...args);
|
||||
this.push_get_retval();
|
||||
this.push_get_errno();
|
||||
this.push_end();
|
||||
this.run();
|
||||
} finally {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
syscall_void(...args) {
|
||||
this.do_syscall(...args);
|
||||
}
|
||||
|
||||
syscall_int(...args) {
|
||||
this.do_syscall(...args);
|
||||
// x | 0 will always be a signed integer
|
||||
return this._return_value[0] | 0;
|
||||
}
|
||||
|
||||
syscall(...args) {
|
||||
this.do_syscall(...args);
|
||||
const retval = this._return_value;
|
||||
return new Int(retval[0], retval[1]);
|
||||
}
|
||||
|
||||
syscall_ptr(...args) {
|
||||
this.do_syscall(...args);
|
||||
const retval = this._return_value;
|
||||
return new Addr(retval[0], retval[1]);
|
||||
}
|
||||
|
||||
// syscall variants that throw an error on errno
|
||||
|
||||
do_syscall_clear_errno(...args) {
|
||||
if (this.position) {
|
||||
throw Error('chain not empty');
|
||||
}
|
||||
try {
|
||||
this.push_clear_errno();
|
||||
this.push_syscall(...args);
|
||||
this.push_get_retval();
|
||||
this.push_get_errno();
|
||||
this.push_end();
|
||||
this.run();
|
||||
} finally {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
sysi(...args) {
|
||||
const errno = this._errno;
|
||||
this.do_syscall_clear_errno(...args);
|
||||
|
||||
const err = errno[0];
|
||||
if (err !== 0) {
|
||||
throw Error(`syscall(${args[0]}) errno: ${err}`);
|
||||
}
|
||||
|
||||
// x | 0 will always be a signed integer
|
||||
return this._return_value[0] | 0;
|
||||
}
|
||||
|
||||
sys(...args) {
|
||||
const errno = this._errno;
|
||||
this.do_syscall_clear_errno(...args);
|
||||
|
||||
const err = errno[0];
|
||||
if (err !== 0) {
|
||||
throw Error(`syscall(${args[0]}) errno: ${err}`);
|
||||
}
|
||||
|
||||
const retval = this._return_value;
|
||||
return new Int(retval[0], retval[1]);
|
||||
}
|
||||
|
||||
sysp(...args) {
|
||||
const errno = this._errno;
|
||||
this.do_syscall_clear_errno(...args);
|
||||
|
||||
const err = errno[0];
|
||||
if (err !== 0) {
|
||||
throw Error(`syscall(${args[0]}) errno: ${err}`);
|
||||
}
|
||||
|
||||
const retval = this._return_value;
|
||||
return new Addr(retval[0], retval[1]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function get_gadget(map, insn_str) {
|
||||
const addr = map.get(insn_str);
|
||||
if (addr === undefined) {
|
||||
throw Error(`gadget not found: ${insn_str}`);
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
function load_fw_specific(version) {
|
||||
if (version & 0x10000) {
|
||||
throw RangeError('ps5 not supported yet');
|
||||
}
|
||||
|
||||
const value = version & 0xffff;
|
||||
// we don't want to bother with very old firmwares that don't support
|
||||
// ECMAScript 2015. 6.xx WebKit poisons the pointer fields of some types
|
||||
// which can be annoying to deal with
|
||||
if (value < 0x700) {
|
||||
throw RangeError("PS4 firmwares < 7.00 isn't supported");
|
||||
}
|
||||
|
||||
if (0x800 <= value && value <= 0x900) {
|
||||
return import('/rop/900.mjs');
|
||||
}
|
||||
|
||||
throw RangeError('firmware not supported');
|
||||
}
|
||||
|
||||
export let gadgets = null;
|
||||
export let libwebkit_base = null;
|
||||
export let libkernel_base = null;
|
||||
export let libc_base = null;
|
||||
export let init_gadget_map = null;
|
||||
export let Chain = null;
|
||||
|
||||
export async function init() {
|
||||
const module = await load_fw_specific(config.target);
|
||||
Chain = module.Chain;
|
||||
module.init(Chain);
|
||||
({
|
||||
gadgets,
|
||||
libwebkit_base,
|
||||
libkernel_base,
|
||||
libc_base,
|
||||
init_gadget_map,
|
||||
} = module);
|
||||
}
|
||||
|
||||
+86
-151
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2023 anonymous
|
||||
/* Copyright (C) 2023-2025 anonymous
|
||||
|
||||
This file is part of PSFree.
|
||||
|
||||
@@ -15,184 +15,119 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
function check_range(x) {
|
||||
return (-0x80000000 <= x) && (x <= 0xffffffff);
|
||||
// cache some constants
|
||||
const isInteger = Number.isInteger;
|
||||
|
||||
function check_not_in_range(x) {
|
||||
return !(isInteger(x) && -0x80000000 <= x && x <= 0xffffffff);
|
||||
}
|
||||
|
||||
function unhexlify(hexstr) {
|
||||
if (hexstr.substring(0, 2) === "0x") {
|
||||
hexstr = hexstr.substring(2);
|
||||
}
|
||||
if (hexstr.length % 2 === 1) {
|
||||
hexstr = '0' + hexstr;
|
||||
}
|
||||
if (hexstr.length % 2 === 1) {
|
||||
throw TypeError("Invalid hex string");
|
||||
// use this if you want to support objects convertible to Int but only need
|
||||
// their low/high bits. creating a Int is slower compared to just using this
|
||||
// function
|
||||
export function lohi_from_one(low) {
|
||||
if (low instanceof Int) {
|
||||
return low._u32.slice();
|
||||
}
|
||||
|
||||
let bytes = new Uint8Array(hexstr.length / 2);
|
||||
for (let i = 0; i < hexstr.length; i += 2) {
|
||||
let new_i = hexstr.length - 2 - i;
|
||||
let substr = hexstr.slice(new_i, new_i + 2);
|
||||
bytes[i / 2] = parseInt(substr, 16);
|
||||
if (check_not_in_range(low)) {
|
||||
throw TypeError(`low not a 32-bit integer: ${low}`);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Decorator for Int instance operations. Takes care
|
||||
// of converting arguments to Int instances if required.
|
||||
function operation(f, nargs) {
|
||||
return function () {
|
||||
if (arguments.length !== nargs)
|
||||
throw Error("Not enough arguments for function " + f.name);
|
||||
let new_args = [];
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
if (!(arguments[i] instanceof Int)) {
|
||||
new_args[i] = new Int(arguments[i]);
|
||||
} else {
|
||||
new_args[i] = arguments[i];
|
||||
}
|
||||
}
|
||||
return f.apply(this, new_args);
|
||||
};
|
||||
return [low >>> 0, low < 0 ? -1 >>> 0 : 0];
|
||||
}
|
||||
|
||||
// immutable 64-bit integer
|
||||
export class Int {
|
||||
constructor(low, high) {
|
||||
let buffer = new Uint32Array(2);
|
||||
let bytes = new Uint8Array(buffer.buffer);
|
||||
|
||||
if (arguments.length > 2) {
|
||||
throw TypeError('Int takes at most 2 args');
|
||||
}
|
||||
if (arguments.length === 0) {
|
||||
throw TypeError('Int takes at min 1 args');
|
||||
}
|
||||
let is_one = false;
|
||||
if (arguments.length === 1) {
|
||||
is_one = true;
|
||||
if (high === undefined) {
|
||||
this._u32 = new Uint32Array(lohi_from_one(low));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_one) {
|
||||
if (typeof (low) !== 'number'
|
||||
&& typeof (high) !== 'number') {
|
||||
throw TypeError('low/high must be numbers');
|
||||
}
|
||||
if (check_not_in_range(low)) {
|
||||
throw TypeError(`low not a 32-bit integer: ${low}`);
|
||||
}
|
||||
|
||||
if (typeof low === 'number') {
|
||||
if (!check_range(low)) {
|
||||
throw TypeError('low not a valid value: ' + low);
|
||||
}
|
||||
if (is_one) {
|
||||
high = 0;
|
||||
if (low < 0) {
|
||||
high = -1;
|
||||
}
|
||||
} else {
|
||||
if (!check_range(high)) {
|
||||
throw TypeError('high not a valid value: ' + high);
|
||||
}
|
||||
}
|
||||
buffer[0] = low;
|
||||
buffer[1] = high;
|
||||
} else if (typeof low === 'string') {
|
||||
bytes.set(unhexlify(low));
|
||||
} else if (typeof low === 'object') {
|
||||
if (low instanceof Int) {
|
||||
bytes.set(low.bytes);
|
||||
} else {
|
||||
if (low.length !== 8)
|
||||
throw TypeError("Array must have exactly 8 elements.");
|
||||
bytes.set(low);
|
||||
}
|
||||
} else {
|
||||
throw TypeError('Int does not support your object for conversion');
|
||||
if (check_not_in_range(high)) {
|
||||
throw TypeError(`high not a 32-bit integer: ${high}`);
|
||||
}
|
||||
|
||||
this.buffer = buffer;
|
||||
this.bytes = bytes;
|
||||
|
||||
this.eq = operation(function eq(b) {
|
||||
const a = this;
|
||||
return a.low() === b.low() && a.high() === b.high();
|
||||
}, 1);
|
||||
|
||||
this.neg = operation(function neg() {
|
||||
let type = this.constructor;
|
||||
|
||||
let low = ~this.low();
|
||||
let high = ~this.high();
|
||||
|
||||
let res = (new Int(low, high)).add(1);
|
||||
|
||||
return new type(res);
|
||||
}, 0);
|
||||
|
||||
this.add = operation(function add(b) {
|
||||
let type = this.constructor;
|
||||
|
||||
let low = this.low();
|
||||
let high = this.high();
|
||||
|
||||
low += b.low();
|
||||
let carry = 0;
|
||||
if (low > 0xffffffff) {
|
||||
carry = 1;
|
||||
}
|
||||
high += carry + b.high();
|
||||
|
||||
low &= 0xffffffff;
|
||||
high &= 0xffffffff;
|
||||
|
||||
return new type(low, high);
|
||||
}, 1);
|
||||
|
||||
this.sub = operation(function sub(b) {
|
||||
let type = this.constructor;
|
||||
|
||||
b = b.neg();
|
||||
|
||||
let low = this.low();
|
||||
let high = this.high();
|
||||
|
||||
low += b.low();
|
||||
let carry = 0;
|
||||
if (low > 0xffffffff) {
|
||||
carry = 1;
|
||||
}
|
||||
high += carry + b.high();
|
||||
|
||||
low &= 0xffffffff;
|
||||
high &= 0xffffffff;
|
||||
|
||||
return new type(low, high);
|
||||
}, 1);
|
||||
this._u32 = new Uint32Array([low, high]);
|
||||
}
|
||||
|
||||
low() {
|
||||
return this.buffer[0];
|
||||
get lo() {
|
||||
return this._u32[0];
|
||||
}
|
||||
|
||||
high() {
|
||||
return this.buffer[1];
|
||||
get hi() {
|
||||
return this._u32[1];
|
||||
}
|
||||
|
||||
toString(is_pretty) {
|
||||
// return low/high as signed integers
|
||||
|
||||
get bot() {
|
||||
return this._u32[0] | 0;
|
||||
}
|
||||
|
||||
get top() {
|
||||
return this._u32[1] | 0;
|
||||
}
|
||||
|
||||
neg() {
|
||||
const u32 = this._u32;
|
||||
const low = (~u32[0] >>> 0) + 1;
|
||||
return new this.constructor(
|
||||
low >>> 0,
|
||||
((~u32[1] >>> 0) + (low > 0xffffffff)) >>> 0,
|
||||
);
|
||||
}
|
||||
|
||||
eq(b) {
|
||||
const values = lohi_from_one(b);
|
||||
const u32 = this._u32;
|
||||
return (
|
||||
u32[0] === values[0]
|
||||
&& u32[1] === values[1]
|
||||
);
|
||||
}
|
||||
|
||||
ne(b) {
|
||||
return !this.eq(b);
|
||||
}
|
||||
|
||||
add(b) {
|
||||
const values = lohi_from_one(b);
|
||||
const u32 = this._u32;
|
||||
const low = u32[0] + values[0];
|
||||
return new this.constructor(
|
||||
low >>> 0,
|
||||
(u32[1] + values[1] + (low > 0xffffffff)) >>> 0,
|
||||
);
|
||||
}
|
||||
|
||||
sub(b) {
|
||||
const values = lohi_from_one(b);
|
||||
const u32 = this._u32;
|
||||
const low = u32[0] + (~values[0] >>> 0) + 1;
|
||||
return new this.constructor(
|
||||
low >>> 0,
|
||||
(u32[1] + (~values[1] >>> 0) + (low > 0xffffffff)) >>> 0,
|
||||
);
|
||||
}
|
||||
|
||||
toString(is_pretty=false) {
|
||||
if (!is_pretty) {
|
||||
let low = this.low().toString(16).padStart(8, '0');
|
||||
let high = this.high().toString(16).padStart(8, '0');
|
||||
const low = this.lo.toString(16).padStart(8, '0');
|
||||
const high = this.hi.toString(16).padStart(8, '0');
|
||||
return '0x' + high + low;
|
||||
}
|
||||
let high = this.high().toString(16).padStart(8, '0');
|
||||
let high = this.hi.toString(16).padStart(8, '0');
|
||||
high = high.substring(0, 4) + '_' + high.substring(4);
|
||||
|
||||
let low = this.low().toString(16).padStart(8, '0');
|
||||
let low = this.lo.toString(16).padStart(8, '0');
|
||||
low = low.substring(0, 4) + '_' + low.substring(4);
|
||||
|
||||
return '0x' + high + '_' + low;
|
||||
}
|
||||
}
|
||||
|
||||
Int.Zero = new Int(0);
|
||||
Int.One = new Int(1);
|
||||
|
||||
+325
-123
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2023-2024 anonymous
|
||||
/* Copyright (C) 2023-2025 anonymous
|
||||
|
||||
This file is part of PSFree.
|
||||
|
||||
@@ -15,214 +15,416 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Int } from './int64.mjs';
|
||||
import {
|
||||
read16,
|
||||
read32,
|
||||
read64,
|
||||
write16,
|
||||
write32,
|
||||
write64,
|
||||
} from './rw.mjs';
|
||||
import * as o from './offset.mjs';
|
||||
import { Int, lohi_from_one } from './int64.mjs';
|
||||
import { view_m_vector, view_m_length } from './offset.mjs';
|
||||
|
||||
export let mem = null;
|
||||
|
||||
// cache some constants
|
||||
const off_vector = view_m_vector / 4;
|
||||
const off_vector2 = (view_m_vector + 4) / 4;
|
||||
const isInteger = Number.isInteger;
|
||||
|
||||
function init_module(memory) {
|
||||
mem = memory;
|
||||
}
|
||||
|
||||
function add_and_set_addr(mem, offset, base_lo, base_hi) {
|
||||
const values = lohi_from_one(offset);
|
||||
const main = mem._main;
|
||||
|
||||
const low = base_lo + values[0];
|
||||
|
||||
// no need to use ">>> 0" to convert to unsigned here
|
||||
main[off_vector] = low;
|
||||
main[off_vector2] = base_hi + values[1] + (low > 0xffffffff);
|
||||
}
|
||||
|
||||
export class Addr extends Int {
|
||||
read8(offset) {
|
||||
const addr = this.add(offset);
|
||||
return mem.read8(addr);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
return m.read8_at(offset);
|
||||
}
|
||||
|
||||
read16(offset) {
|
||||
const addr = this.add(offset);
|
||||
return mem.read16(addr);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
return m.read16_at(offset);
|
||||
}
|
||||
|
||||
read32(offset) {
|
||||
const addr = this.add(offset);
|
||||
return mem.read32(addr);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
return m.read32_at(offset);
|
||||
}
|
||||
|
||||
read64(offset) {
|
||||
const addr = this.add(offset);
|
||||
return mem.read64(addr);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
return m.read64_at(offset);
|
||||
}
|
||||
|
||||
// returns a pointer instead of an Int
|
||||
readp(offset) {
|
||||
const addr = this.add(offset);
|
||||
return mem.readp(addr);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
return m.readp_at(offset);
|
||||
}
|
||||
|
||||
write8(offset, value) {
|
||||
const addr = this.add(offset);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
mem.write8(addr, value);
|
||||
m.write8_at(offset, value);
|
||||
}
|
||||
|
||||
write16(offset, value) {
|
||||
const addr = this.add(offset);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
mem.write16(addr, value);
|
||||
m.write16_at(offset, value);
|
||||
}
|
||||
|
||||
write32(offset, value) {
|
||||
const addr = this.add(offset);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
mem.write32(addr, value);
|
||||
m.write32_at(offset, value);
|
||||
}
|
||||
|
||||
write64(offset, value) {
|
||||
const addr = this.add(offset);
|
||||
const m = mem;
|
||||
if (isInteger(offset) && 0 <= offset && offset <= 0xffffffff) {
|
||||
m._set_addr_direct(this);
|
||||
} else {
|
||||
add_and_set_addr(m, offset, this.lo, this.hi);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
mem.write64(addr, value);
|
||||
m.write64_at(offset, value);
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryBase {
|
||||
_addrof(obj) {
|
||||
if (typeof obj !== 'object'
|
||||
&& typeof obj !== 'function'
|
||||
) {
|
||||
throw TypeError('addrof argument not a JS object');
|
||||
// expected:
|
||||
// * main - Uint32Array whose m_vector points to worker
|
||||
// * worker - DataView
|
||||
//
|
||||
// addrof()/fakeobj() expectations:
|
||||
// * obj - has a "addr" property and a 0 index.
|
||||
// * addr_addr - Int, the address of the slot of obj.addr
|
||||
// * fake_addr - Int, the address of the slot of obj[0]
|
||||
//
|
||||
// a valid example for "obj" is "{addr: null, 0: 0}". note that this example
|
||||
// has [0] be 0 so that the butterfly's indexing type is ArrayWithInt32. this
|
||||
// prevents the garbage collector from incorrectly treating the slot's value as
|
||||
// a JSObject and then crash
|
||||
//
|
||||
// the relative read/write methods expect the offset to be a unsigned 32-bit
|
||||
// integer
|
||||
export class Memory {
|
||||
constructor(main, worker, obj, addr_addr, fake_addr) {
|
||||
this._main = main;
|
||||
this._worker = worker;
|
||||
this._obj = obj;
|
||||
this._addr_low = addr_addr.lo;
|
||||
this._addr_high = addr_addr.hi;
|
||||
this._fake_low = fake_addr.lo;
|
||||
this._fake_high = fake_addr.hi;
|
||||
|
||||
main[view_m_length / 4] = 0xffffffff;
|
||||
|
||||
init_module(this);
|
||||
|
||||
const off_mvec = view_m_vector;
|
||||
// use this to create WastefulTypedArrays to avoid a GC crash
|
||||
const buf = new ArrayBuffer(0);
|
||||
|
||||
const src = new Uint8Array(buf);
|
||||
const sset = new Uint32Array(buf);
|
||||
const sset_p = this.addrof(sset);
|
||||
sset_p.write64(off_mvec, this.addrof(src).add(off_mvec));
|
||||
sset_p.write32(view_m_length, 3);
|
||||
this._cpysrc = src;
|
||||
this._src_setter = sset;
|
||||
|
||||
const dst = new Uint8Array(buf);
|
||||
const dset = new Uint32Array(buf);
|
||||
const dset_p = this.addrof(dset);
|
||||
dset_p.write64(off_mvec, this.addrof(dst).add(off_mvec));
|
||||
dset_p.write32(view_m_length, 3);
|
||||
dset[2] = 0xffffffff;
|
||||
this._cpydst = dst;
|
||||
this._dst_setter = dset;
|
||||
}
|
||||
|
||||
// dst and src may overlap
|
||||
cpy(dst, src, len) {
|
||||
if (!(isInteger(len) && 0 <= len && len <= 0xffffffff)) {
|
||||
throw TypeError('len not a unsigned 32-bit integer');
|
||||
}
|
||||
this.worker.a = obj;
|
||||
write64(this.main, o.view_m_vector, this.butterfly.sub(0x10));
|
||||
let res = read64(this.worker, 0);
|
||||
write64(this.main, o.view_m_vector, this._current_addr);
|
||||
|
||||
const dvals = lohi_from_one(dst);
|
||||
const svals = lohi_from_one(src);
|
||||
const dset = this._dst_setter;
|
||||
const sset = this._src_setter;
|
||||
|
||||
dset[0] = dvals[0];
|
||||
dset[1] = dvals[1];
|
||||
sset[0] = svals[0];
|
||||
sset[1] = svals[1];
|
||||
sset[2] = len;
|
||||
|
||||
this._cpydst.set(this._cpysrc);
|
||||
}
|
||||
|
||||
// allocate Garbage Collector managed memory. returns [address_of_memory,
|
||||
// backer]. backer is the JSCell that is keeping the returned memory alive,
|
||||
// you can drop it once you have another GC object reference the address.
|
||||
// the backer is an implementation detail. don't use it to mutate the
|
||||
// memory
|
||||
gc_alloc(size) {
|
||||
if (!isInteger(size)) {
|
||||
throw TypeError('size not a integer');
|
||||
}
|
||||
if (size < 0) {
|
||||
throw RangeError('size is negative');
|
||||
}
|
||||
|
||||
const fastLimit = 1000;
|
||||
size = (size + 7 & ~7) >> 3;
|
||||
if (size > fastLimit) {
|
||||
throw RangeError('size is too large');
|
||||
}
|
||||
|
||||
const backer = new Float64Array(size);
|
||||
return [mem.addrof(backer).readp(view_m_vector), backer];
|
||||
}
|
||||
|
||||
fakeobj(addr) {
|
||||
const values = lohi_from_one(addr);
|
||||
const worker = this._worker;
|
||||
const main = this._main;
|
||||
|
||||
main[off_vector] = this._fake_low;
|
||||
main[off_vector2] = this._fake_high;
|
||||
worker.setUint32(0, values[0], true);
|
||||
worker.setUint32(4, values[1], true);
|
||||
return this._obj[0];
|
||||
}
|
||||
|
||||
addrof(object) {
|
||||
// typeof considers null as a object. blacklist it as it isn't a
|
||||
// JSObject
|
||||
if (object === null
|
||||
|| (typeof object !== 'object' && typeof object !== 'function')
|
||||
) {
|
||||
throw TypeError('argument not a JS object');
|
||||
}
|
||||
|
||||
const obj = this._obj;
|
||||
const worker = this._worker;
|
||||
const main = this._main;
|
||||
|
||||
obj.addr = object;
|
||||
|
||||
main[off_vector] = this._addr_low;
|
||||
main[off_vector2] = this._addr_high;
|
||||
|
||||
const res = new Addr(
|
||||
worker.getUint32(0, true),
|
||||
worker.getUint32(4, true),
|
||||
);
|
||||
obj.addr = null;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
addrof(obj) {
|
||||
return new Addr(this._addrof(obj));
|
||||
// expects addr to be a Int
|
||||
_set_addr_direct(addr) {
|
||||
const main = this._main;
|
||||
main[off_vector] = addr.lo;
|
||||
main[off_vector2] = addr.hi;
|
||||
}
|
||||
|
||||
set_addr(addr) {
|
||||
if (!(addr instanceof Int)) {
|
||||
throw TypeError('addr must be an Int');
|
||||
}
|
||||
this._current_addr = addr;
|
||||
write64(this.main, o.view_m_vector, this._current_addr);
|
||||
const values = lohi_from_one(addr);
|
||||
const main = this._main;
|
||||
main[off_vector] = values[0];
|
||||
main[off_vector2] = values[1];
|
||||
}
|
||||
|
||||
get_addr() {
|
||||
return this._current_addr;
|
||||
}
|
||||
|
||||
// write0() is for when you want to write to address 0. You can't use for
|
||||
// example: "mem.write32(Int.Zero, 0)", since you can't set by index the
|
||||
// view when it isDetached(). isDetached() == true when m_mode >=
|
||||
// WastefulTypedArray and m_vector == 0.
|
||||
//
|
||||
// Functions like write32() will index mem.worker via write() from rw.mjs.
|
||||
//
|
||||
// size is the number of bits to read/write.
|
||||
//
|
||||
// The constraint is 0 <= offset + 1 < 2**32.
|
||||
//
|
||||
// PS4 firmwares >= 9.00 and any PS5 version can write to address 0
|
||||
// directly. All firmwares (PS4 and PS5) can read address 0 directly.
|
||||
//
|
||||
// See setIndex() from
|
||||
// WebKit/Source/JavaScriptCore/runtime/JSGenericTypedArrayView.h at PS4
|
||||
// 8.03 for more information. Affected firmwares will get this error:
|
||||
//
|
||||
// TypeError: Underlying ArrayBuffer has been detached from the view
|
||||
write0(size, offset, value) {
|
||||
const i = offset + 1;
|
||||
if (i >= 2**32 || i < 0) {
|
||||
throw RangeError(`read0() invalid offset: ${offset}`);
|
||||
}
|
||||
|
||||
this.set_addr(new Int(-1));
|
||||
|
||||
switch (size) {
|
||||
case 8: {
|
||||
this.worker[i] = value;
|
||||
}
|
||||
case 16: {
|
||||
write16(this.worker, i, value);
|
||||
}
|
||||
case 32: {
|
||||
write32(this.worker, i, value);
|
||||
}
|
||||
case 64: {
|
||||
write64(this.worker, i, value);
|
||||
}
|
||||
default: {
|
||||
throw RangeError(`write0() invalid size: ${size}`);
|
||||
}
|
||||
}
|
||||
const main = this._main;
|
||||
return new Addr(main[off_vector], main[off_vector2]);
|
||||
}
|
||||
|
||||
read8(addr) {
|
||||
this.set_addr(addr);
|
||||
return this.worker[0];
|
||||
return this._worker.getUint8(0);
|
||||
}
|
||||
|
||||
read16(addr) {
|
||||
this.set_addr(addr);
|
||||
return read16(this.worker, 0);
|
||||
return this._worker.getUint16(0, true);
|
||||
}
|
||||
|
||||
read32(addr) {
|
||||
this.set_addr(addr);
|
||||
return read32(this.worker, 0);
|
||||
return this._worker.getUint32(0, true);
|
||||
}
|
||||
|
||||
read64(addr) {
|
||||
this.set_addr(addr);
|
||||
return read64(this.worker, 0);
|
||||
const worker = this._worker;
|
||||
return new Int(worker.getUint32(0, true), worker.getUint32(4, true));
|
||||
}
|
||||
|
||||
// returns a pointer instead of an Int
|
||||
readp(addr) {
|
||||
return new Addr(this.read64(addr));
|
||||
this.set_addr(addr);
|
||||
const worker = this._worker;
|
||||
return new Addr(worker.getUint32(0, true), worker.getUint32(4, true));
|
||||
}
|
||||
|
||||
read8_at(offset) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
return this._worker.getUint8(offset);
|
||||
}
|
||||
|
||||
read16_at(offset) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
return this._worker.getUint16(offset, true);
|
||||
}
|
||||
|
||||
read32_at(offset) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
return this._worker.getUint32(offset, true);
|
||||
}
|
||||
|
||||
read64_at(offset) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
const worker = this._worker;
|
||||
return new Int(
|
||||
worker.getUint32(offset, true),
|
||||
worker.getUint32(offset + 4, true),
|
||||
);
|
||||
}
|
||||
|
||||
readp_at(offset) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
const worker = this._worker;
|
||||
return new Addr(
|
||||
worker.getUint32(offset, true),
|
||||
worker.getUint32(offset + 4, true),
|
||||
);
|
||||
}
|
||||
|
||||
write8(addr, value) {
|
||||
this.set_addr(addr);
|
||||
this.worker[0] = value;
|
||||
this._worker.setUint8(0, value);
|
||||
}
|
||||
|
||||
write16(addr, value) {
|
||||
this.set_addr(addr);
|
||||
write16(this.worker, 0, value);
|
||||
this._worker.setUint16(0, value, true);
|
||||
}
|
||||
|
||||
write32(addr, value) {
|
||||
this.set_addr(addr);
|
||||
write32(this.worker, 0, value);
|
||||
this._worker.setUint32(0, value, true);
|
||||
}
|
||||
|
||||
write64(addr, value) {
|
||||
const values = lohi_from_one(value);
|
||||
this.set_addr(addr);
|
||||
write64(this.worker, 0, value);
|
||||
}
|
||||
}
|
||||
|
||||
export class Memory extends MemoryBase {
|
||||
constructor(main, worker) {
|
||||
super();
|
||||
|
||||
this.main = main;
|
||||
this.worker = worker;
|
||||
|
||||
// The initial creation of the "a" property will change the butterfly
|
||||
// address. Do it now so we can cache it for addrof().
|
||||
worker.a = 0; // dummy value, we just want to create the "a" property
|
||||
this.butterfly = read64(main, o.js_butterfly);
|
||||
|
||||
write32(main, o.view_m_length, 0xffffffff);
|
||||
|
||||
this._current_addr = Int.Zero;
|
||||
|
||||
init_module(this);
|
||||
const worker = this._worker;
|
||||
worker.setUint32(0, values[0], true);
|
||||
worker.setUint32(4, values[1], true);
|
||||
}
|
||||
|
||||
write8_at(offset, value) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
this._worker.setUint8(offset, value);
|
||||
}
|
||||
|
||||
write16_at(offset, value) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
this._worker.setUint16(offset, value, true);
|
||||
}
|
||||
|
||||
write32_at(offset, value) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
this._worker.setUint32(offset, value, true);
|
||||
}
|
||||
|
||||
write64_at(offset, value) {
|
||||
if (!isInteger(offset)) {
|
||||
throw TypeError('offset not a integer');
|
||||
}
|
||||
const values = lohi_from_one(value);
|
||||
const worker = this._worker;
|
||||
worker.setUint32(offset, values[0], true);
|
||||
worker.setUint32(offset + 4, values[1], true);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-29
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2023-2024 anonymous
|
||||
/* Copyright (C) 2023-2025 anonymous
|
||||
|
||||
This file is part of PSFree.
|
||||
|
||||
@@ -18,13 +18,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
// This module are for utilities that depend on running the exploit first
|
||||
|
||||
import { Int } from './int64.mjs';
|
||||
import { Addr, mem } from './mem.mjs';
|
||||
import { mem } from './mem.mjs';
|
||||
import { align } from './utils.mjs';
|
||||
import { KB } from './constants.mjs';
|
||||
import { read32 } from './rw.mjs';
|
||||
import { page_size } from './offset.mjs';
|
||||
import { BufferView } from './rw.mjs';
|
||||
import { View1 } from './view.mjs';
|
||||
|
||||
import * as rw from './rw.mjs';
|
||||
import * as o from './offset.mjs';
|
||||
import * as off from './offset.mjs';
|
||||
|
||||
// creates an ArrayBuffer whose contents is copied from addr
|
||||
export function make_buffer(addr, size) {
|
||||
@@ -53,11 +53,11 @@ export function make_buffer(addr, size) {
|
||||
const u_addr = mem.addrof(u);
|
||||
|
||||
// we won't change the butterfly and m_mode so we won't save those
|
||||
const old_addr = u_addr.read64(o.view_m_vector);
|
||||
const old_size = u_addr.read32(o.view_m_length);
|
||||
const old_addr = u_addr.read64(off.view_m_vector);
|
||||
const old_size = u_addr.read32(off.view_m_length);
|
||||
|
||||
u_addr.write64(o.view_m_vector, addr);
|
||||
u_addr.write32(o.view_m_length, size);
|
||||
u_addr.write64(off.view_m_vector, addr);
|
||||
u_addr.write32(off.view_m_length, size);
|
||||
|
||||
const copy = new Uint8Array(u.length);
|
||||
copy.set(u);
|
||||
@@ -76,8 +76,8 @@ export function make_buffer(addr, size) {
|
||||
const res = copy.buffer;
|
||||
|
||||
// restore
|
||||
u_addr.write64(o.view_m_vector, old_addr);
|
||||
u_addr.write32(o.view_m_length, old_size);
|
||||
u_addr.write64(off.view_m_vector, old_addr);
|
||||
u_addr.write32(off.view_m_length, old_size);
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -87,8 +87,8 @@ function check_magic_at(p, is_text) {
|
||||
// byte sequence that is very likely to appear at offset 0 of a .text
|
||||
// segment
|
||||
const text_magic = [
|
||||
new Int([0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56]),
|
||||
new Int([0x41, 0x55, 0x41, 0x54, 0x53, 0x50, 0x48, 0x8d]),
|
||||
new Int(0xe5894855, 0x56415741),
|
||||
new Int(0x54415541, 0x8d485053),
|
||||
];
|
||||
|
||||
// the .data "magic" is just a portion of the PT_SCE_MODULE_PARAM segment
|
||||
@@ -137,8 +137,6 @@ function check_magic_at(p, is_text) {
|
||||
// addr.read8(-1);
|
||||
//
|
||||
export function find_base(addr, is_text, is_back) {
|
||||
// ps4 page size
|
||||
const page_size = 16 * KB;
|
||||
// align to page size
|
||||
addr = align(addr, page_size);
|
||||
const offset = (is_back ? -1 : 1) * page_size;
|
||||
@@ -146,7 +144,7 @@ export function find_base(addr, is_text, is_back) {
|
||||
if (check_magic_at(addr, is_text)) {
|
||||
break;
|
||||
}
|
||||
addr = addr.add(offset)
|
||||
addr = addr.add(offset);
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
@@ -156,27 +154,27 @@ export function get_view_vector(view) {
|
||||
if (!ArrayBuffer.isView(view)) {
|
||||
throw TypeError(`object not a JSC::JSArrayBufferView: ${view}`);
|
||||
}
|
||||
return mem.addrof(view).readp(o.view_m_vector);
|
||||
return mem.addrof(view).readp(off.view_m_vector);
|
||||
}
|
||||
|
||||
export function resolve_import(import_addr) {
|
||||
if (import_addr.read16(0) !== 0x25ff) {
|
||||
throw Error(
|
||||
`instruction at ${import_addr} is not of the form: jmp qword`
|
||||
+ ' [rip + X]'
|
||||
);
|
||||
+ ' [rip + X]');
|
||||
}
|
||||
// module_function_import:
|
||||
// jmp qword [rip + X]
|
||||
// ff 25 xx xx xx xx // signed 32-bit displacement
|
||||
const disp = import_addr.read32(2);
|
||||
// sign extend
|
||||
const offset = new Int(disp, disp >> 31);
|
||||
// assume disp and offset are 32-bit integers
|
||||
// x | 0 will always be a signed integer
|
||||
const offset = (disp | 0) + 6;
|
||||
// The rIP value used by "jmp [rip + X]" instructions is actually the rIP
|
||||
// of the next instruction. This means that the actual address used is
|
||||
// [rip + X + sizeof(jmp_insn)], where sizeof(jmp_insn) is the size of the
|
||||
// jump instruction, which is 6 in this case.
|
||||
const function_addr = import_addr.readp(offset.add(6));
|
||||
const function_addr = import_addr.readp(offset);
|
||||
|
||||
return function_addr;
|
||||
}
|
||||
@@ -186,8 +184,9 @@ export function init_syscall_array(
|
||||
libkernel_web_base,
|
||||
max_search_size,
|
||||
) {
|
||||
if (typeof max_search_size !== 'number') {
|
||||
throw TypeError(`max_search_size is not a number: ${max_search_size}`);
|
||||
if (!Number.isInteger(max_search_size)) {
|
||||
throw TypeError(
|
||||
`max_search_size is not a integer: ${max_search_size}`);
|
||||
}
|
||||
if (max_search_size < 0) {
|
||||
throw Error(`max_search_size is less than 0: ${max_search_size}`);
|
||||
@@ -197,7 +196,7 @@ export function init_syscall_array(
|
||||
libkernel_web_base,
|
||||
max_search_size,
|
||||
);
|
||||
const kbuf = new Uint8Array(libkernel_web_buffer);
|
||||
const kbuf = new BufferView(libkernel_web_buffer);
|
||||
|
||||
// Search 'rdlo' string from libkernel_web's .rodata section to gain an
|
||||
// upper bound on the size of the .text section.
|
||||
@@ -217,8 +216,7 @@ export function init_syscall_array(
|
||||
if (!found) {
|
||||
throw Error(
|
||||
'"rdlo" string not found in libkernel_web, base address:'
|
||||
+ ` ${libkernel_web_base}`
|
||||
);
|
||||
+ ` ${libkernel_web_base}`);
|
||||
}
|
||||
|
||||
// search for the instruction sequence:
|
||||
@@ -236,10 +234,23 @@ export function init_syscall_array(
|
||||
&& kbuf[i + 10] === 0x0f
|
||||
&& kbuf[i + 11] === 0x05
|
||||
) {
|
||||
const syscall_num = read32(kbuf, i + 3);
|
||||
const syscall_num = kbuf.read32(i + 3);
|
||||
syscall_array[syscall_num] = libkernel_web_base.add(i);
|
||||
// skip the sequence
|
||||
i += 11;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create a char array like in the C language
|
||||
//
|
||||
// string to view since it's easier to get the address of the buffer this way
|
||||
export function cstr(str) {
|
||||
str += '\0';
|
||||
return View1.from(str, c => c.codePointAt(0));
|
||||
}
|
||||
|
||||
// we are re-exporting this since users that want to use cstr() usually want
|
||||
// jstr() as well. they are likely working with functions that take/return
|
||||
// strings
|
||||
export { jstr } from './utils.mjs';
|
||||
|
||||
+28
-1
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2023 anonymous
|
||||
/* Copyright (C) 2023-2025 anonymous
|
||||
|
||||
This file is part of PSFree.
|
||||
|
||||
@@ -15,8 +15,16 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
// WebKit offsets start
|
||||
|
||||
// offsets for JSC::JSObject
|
||||
export const js_cell = 0;
|
||||
export const js_butterfly = 0x8;
|
||||
// start of the array of inline properties (JSValues)
|
||||
export const js_inline_prop = 0x10;
|
||||
|
||||
// sizeof JSC::JSObject
|
||||
export const size_jsobj = js_inline_prop;
|
||||
|
||||
// offsets for JSC::JSArrayBufferView
|
||||
export const view_m_vector = 0x10;
|
||||
@@ -33,3 +41,22 @@ export const strimpl_inline_str = 0x14;
|
||||
|
||||
// sizeof WTF::StringImpl
|
||||
export const size_strimpl = 0x18;
|
||||
|
||||
// offsets for WebCore::JSHTMLTextAreaElement, subclass of JSObject
|
||||
|
||||
// offset to m_wrapped, pointer to a DOM object
|
||||
// for this class, it's a WebCore::HTMLTextAreaElement pointer
|
||||
export const jsta_impl = 0x18;
|
||||
|
||||
// sizeof WebCore::JSHTMLTextAreaElement
|
||||
export const size_jsta = 0x20;
|
||||
|
||||
// WebKit offsets end
|
||||
|
||||
export const KB = 1024;
|
||||
export const MB = KB * KB;
|
||||
export const GB = KB * KB * KB;
|
||||
export const page_size = 16 * KB; // page size on the ps4
|
||||
|
||||
// size of the buffer used by setcontext/getcontext (see module/chain.mjs)
|
||||
export const context_size = 0xc8;
|
||||
|
||||
+52
-17
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2023 anonymous
|
||||
/* Copyright (C) 2023-2025 anonymous
|
||||
|
||||
This file is part of PSFree.
|
||||
|
||||
@@ -15,7 +15,54 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Int } from './int64.mjs';
|
||||
import { Int, lohi_from_one } from './int64.mjs';
|
||||
|
||||
// DataView's accessors are constant time and are faster when doing multi-byte
|
||||
// accesses but the single-byte accessors are slightly slower compared to just
|
||||
// indexing the Uint8Array
|
||||
//
|
||||
// to get the best of both worlds, BufferView uses a DataView for multi-byte
|
||||
// accesses and a Uint8Array for single-byte
|
||||
//
|
||||
// instances of BufferView will their have m_mode set to WastefulTypedArray
|
||||
// since we use the .buffer getter to create a DataView
|
||||
export class BufferView extends Uint8Array {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this._dview = new DataView(this.buffer, this.byteOffset);
|
||||
}
|
||||
|
||||
read16(offset) {
|
||||
return this._dview.getUint16(offset, true);
|
||||
}
|
||||
|
||||
read32(offset) {
|
||||
return this._dview.getUint32(offset, true);
|
||||
}
|
||||
|
||||
read64(offset) {
|
||||
return new Int(
|
||||
this._dview.getUint32(offset, true),
|
||||
this._dview.getUint32(offset + 4, true),
|
||||
);
|
||||
}
|
||||
|
||||
write16(offset, value) {
|
||||
this._dview.setUint16(offset, value, true);
|
||||
}
|
||||
|
||||
write32(offset, value) {
|
||||
this._dview.setUint32(offset, value, true);
|
||||
}
|
||||
|
||||
write64(offset, value) {
|
||||
const values = lohi_from_one(value);
|
||||
this._dview.setUint32(offset, values[0], true);
|
||||
this._dview.setUint32(offset + 4, values[1], true);
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING: These functions are now deprecated. use BufferView instead.
|
||||
|
||||
// view.buffer is the underlying ArrayBuffer of a TypedArray, but since we will
|
||||
// be corrupting the m_vector of our target views later, the ArrayBuffer's
|
||||
@@ -58,11 +105,7 @@ export function read32(u8_view, offset) {
|
||||
}
|
||||
|
||||
export function read64(u8_view, offset) {
|
||||
let res = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
res.push(u8_view[offset + i]);
|
||||
}
|
||||
return new Int(res);
|
||||
return new Int(read32(u8_view, offset), read32(u8_view, offset + 4));
|
||||
}
|
||||
|
||||
// for writes less than 8 bytes
|
||||
@@ -85,8 +128,8 @@ export function write64(u8_view, offset, value) {
|
||||
throw TypeError('write64 value must be an Int');
|
||||
}
|
||||
|
||||
let low = value.low();
|
||||
let high = value.high();
|
||||
let low = value.lo;
|
||||
let high = value.hi;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
u8_view[offset + i] = (low >>> i*8) & 0xff;
|
||||
@@ -95,11 +138,3 @@ export function write64(u8_view, offset, value) {
|
||||
u8_view[offset + 4 + i] = (high >>> i*8) & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
export function sread64(str, offset) {
|
||||
let res = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
res.push(str.charCodeAt(offset + i));
|
||||
}
|
||||
return new Int(res);
|
||||
}
|
||||
|
||||
+121
-24
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2023 anonymous
|
||||
/* Copyright (C) 2023-2025 anonymous
|
||||
|
||||
This file is part of PSFree.
|
||||
|
||||
@@ -17,32 +17,24 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Int } from './int64.mjs';
|
||||
|
||||
export function die(msg) {
|
||||
alert(msg);
|
||||
undefinedFunction();
|
||||
export class DieError extends Error {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export function debug_log(msg) {
|
||||
let textNode = document.createTextNode(msg);
|
||||
let node = document.createElement("p").appendChild(textNode);
|
||||
export function die(msg='') {
|
||||
throw new DieError(msg);
|
||||
}
|
||||
|
||||
document.body.appendChild(node);
|
||||
document.body.appendChild(document.createElement("br"));
|
||||
const console = document.getElementById('console');
|
||||
export function log(msg='') {
|
||||
console.append(msg + '\n');
|
||||
}
|
||||
|
||||
export function clear_log() {
|
||||
document.body.innerHTML = null;
|
||||
}
|
||||
|
||||
export function str2array(str, length, offset) {
|
||||
if (offset === undefined) {
|
||||
offset = 0;
|
||||
}
|
||||
let a = new Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
a[i] = str.charCodeAt(i + offset);
|
||||
}
|
||||
return a;
|
||||
console.innerHTML = null;
|
||||
}
|
||||
|
||||
// alignment must be 32 bits and is a power of 2
|
||||
@@ -52,8 +44,8 @@ export function align(a, alignment) {
|
||||
}
|
||||
const mask = -alignment & 0xffffffff;
|
||||
let type = a.constructor;
|
||||
let low = a.low() & mask;
|
||||
return new type(low, a.high());
|
||||
let low = a.lo & mask;
|
||||
return new type(low, a.hi);
|
||||
}
|
||||
|
||||
export async function send(url, buffer, file_name, onload=() => {}) {
|
||||
@@ -65,7 +57,7 @@ export async function send(url, buffer, file_name, onload=() => {}) {
|
||||
const form = new FormData();
|
||||
form.append('upload', file);
|
||||
|
||||
debug_log('send');
|
||||
log('send');
|
||||
const response = await fetch(url, {method: 'POST', body: form});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -73,3 +65,108 @@ export async function send(url, buffer, file_name, onload=() => {}) {
|
||||
}
|
||||
onload();
|
||||
}
|
||||
|
||||
// mostly used to yield to the GC. marking is concurrent but collection isn't
|
||||
//
|
||||
// yielding also lets the DOM update. which is useful since we use the DOM for
|
||||
// logging and we loop when waiting for a collection to occur
|
||||
export function sleep(ms=0) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function hex(number) {
|
||||
return '0x' + number.toString(16);
|
||||
}
|
||||
|
||||
// no "0x" prefix
|
||||
export function hex_np(number) {
|
||||
return number.toString(16);
|
||||
}
|
||||
|
||||
// expects a byte array
|
||||
export function hexdump(view) {
|
||||
const num_16 = view.length & ~15;
|
||||
const residue = view.length - num_16;
|
||||
const max_off_len = hex_np(((view.length + 7) & ~7) - 1).length;
|
||||
|
||||
function chr(i) {
|
||||
if (0x20 <= i && i <= 0x7e) {
|
||||
return String.fromCodePoint(i);
|
||||
}
|
||||
return '.';
|
||||
}
|
||||
|
||||
function to_hex(view, offset, length) {
|
||||
return (
|
||||
[...view.slice(offset, offset + length)]
|
||||
.map(e => hex_np(e).padStart(2, '0'))
|
||||
.join(' ')
|
||||
);
|
||||
}
|
||||
|
||||
let bytes = [];
|
||||
for (let i = 0; i < num_16; i += 16) {
|
||||
const long1 = to_hex(view, i, 8);
|
||||
const long2 = to_hex(view, i + 8, 8);
|
||||
|
||||
let print = '';
|
||||
for (let j = 0; j < 16; j++) {
|
||||
print += chr(view[j]);
|
||||
}
|
||||
|
||||
bytes.push([`${long1} ${long2}`, print]);
|
||||
}
|
||||
|
||||
if (residue) {
|
||||
const small = residue <= 8;
|
||||
const long1_len = small ? residue : 8;
|
||||
|
||||
let long1 = to_hex(view, num_16, long1_len);
|
||||
if (small) {
|
||||
for (let i = 0; i < 8 - residue; i++) {
|
||||
long1 += ' xx';
|
||||
}
|
||||
}
|
||||
|
||||
const long2 = (() => {
|
||||
if (small) {
|
||||
return Array(8).fill('xx').join(' ');
|
||||
}
|
||||
|
||||
let res = to_hex(view, num_16 + 8, residue - 8);
|
||||
for (let i = 0; i < 16 - residue; i++) {
|
||||
res += ' xx';
|
||||
}
|
||||
|
||||
return res;
|
||||
})();
|
||||
|
||||
let print = '';
|
||||
for (let i = 0; i < residue; i++) {
|
||||
print += chr(view[num_16 + i]);
|
||||
}
|
||||
for (let i = 0; i < 16 - residue; i++) {
|
||||
print += ' ';
|
||||
}
|
||||
|
||||
bytes.push([`${long1} ${long2}`, print]);
|
||||
}
|
||||
|
||||
for (const [pos, [val, print]] of bytes.entries()) {
|
||||
const off = hex_np(pos * 16).padStart(max_off_len, '0');
|
||||
log(`${off} | ${val} |${print}|`);
|
||||
}
|
||||
}
|
||||
|
||||
// make a JavaScript string
|
||||
export function jstr(buffer) {
|
||||
let res = '';
|
||||
for (const item of buffer) {
|
||||
if (item === 0) {
|
||||
break;
|
||||
}
|
||||
res += String.fromCodePoint(item);
|
||||
}
|
||||
// convert to primitive string
|
||||
return String(res);
|
||||
}
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
/* Copyright (C) 2025 anonymous
|
||||
|
||||
This file is part of PSFree.
|
||||
|
||||
PSFree is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
PSFree is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Int, lohi_from_one } from '/module/int64.mjs';
|
||||
import { Addr } from '/module/mem.mjs';
|
||||
import { BufferView } from '/module/rw.mjs';
|
||||
|
||||
import * as config from '/config.mjs';
|
||||
import * as mt from '/module/memtools.mjs';
|
||||
|
||||
// View constructors will always get the buffer property in order to make sure
|
||||
// that the JSArrayBufferView is a WastefulTypedArray. m_vector may change if
|
||||
// m_mode < WastefulTypedArray. This is to make caching the m_view field
|
||||
// possible. Users don't have to worry if the m_view they got from addr() is
|
||||
// possibly stale.
|
||||
//
|
||||
// see possiblySharedBuffer() from
|
||||
// WebKit/Source/JavaScriptCore/runtime/JSArrayBufferViewInlines.h
|
||||
// at PS4 8.03
|
||||
//
|
||||
// Subclasses of TypedArray are still implemented as a JSArrayBufferView, so
|
||||
// get_view_vector() still works on them.
|
||||
|
||||
function ViewMixin(superclass) {
|
||||
const res = class extends superclass {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.buffer;
|
||||
}
|
||||
|
||||
get addr() {
|
||||
let res = this._addr_cache;
|
||||
if (res !== undefined) {
|
||||
return res;
|
||||
}
|
||||
res = mt.get_view_vector(this);
|
||||
this._addr_cache = res;
|
||||
return res;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.byteLength;
|
||||
}
|
||||
|
||||
addr_at(index) {
|
||||
const size = this.BYTES_PER_ELEMENT;
|
||||
return this.addr.add(index * size);
|
||||
}
|
||||
|
||||
sget(index) {
|
||||
return this[index] | 0;
|
||||
}
|
||||
};
|
||||
|
||||
// workaround for known affected versions: ps4 [6.00, 10.00)
|
||||
//
|
||||
// see from() and of() from
|
||||
// WebKit/Source/JavaScriptCore/builtins/TypedArrayConstructor.js at PS4
|
||||
// 8.0x
|
||||
//
|
||||
// @getByIdDirectPrivate(this, "allocateTypedArray") will fail when "this"
|
||||
// isn't one of the built-in TypedArrays. this is a violation of the
|
||||
// ECMAScript spec at that time
|
||||
//
|
||||
// TODO assumes ps4, support ps5 as well
|
||||
// FIXME define the from/of workaround functions once
|
||||
if (0x600 <= config.target && config.target < 0x1000) {
|
||||
res.from = function from(...args) {
|
||||
const base = this.__proto__;
|
||||
return new this(base.from(...args).buffer);
|
||||
};
|
||||
|
||||
res.of = function of(...args) {
|
||||
const base = this.__proto__;
|
||||
return new this(base.of(...args).buffer);
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export class View1 extends ViewMixin(Uint8Array) {}
|
||||
export class View2 extends ViewMixin(Uint16Array) {}
|
||||
export class View4 extends ViewMixin(Uint32Array) {}
|
||||
|
||||
export class Buffer extends BufferView {
|
||||
get addr() {
|
||||
let res = this._addr_cache;
|
||||
if (res !== undefined) {
|
||||
return res;
|
||||
}
|
||||
res = mt.get_view_vector(this);
|
||||
this._addr_cache = res;
|
||||
return res;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.byteLength;
|
||||
}
|
||||
|
||||
addr_at(index) {
|
||||
return this.addr.add(index);
|
||||
}
|
||||
}
|
||||
// see from() and of() comment above
|
||||
if (0x600 <= config.target && config.target < 0x1000) {
|
||||
Buffer.from = function from(...args) {
|
||||
const base = this.__proto__;
|
||||
return new this(base.from(...args).buffer);
|
||||
};
|
||||
Buffer.of = function of(...args) {
|
||||
const base = this.__proto__;
|
||||
return new this(base.of(...args).buffer);
|
||||
};
|
||||
}
|
||||
|
||||
const VariableMixin = superclass => class extends superclass {
|
||||
constructor(value=0) {
|
||||
// unlike the View classes, we don't allow number coercion. we
|
||||
// explicitly allow floats unlike Int
|
||||
if (typeof value !== 'number') {
|
||||
throw TypeError('value not a number');
|
||||
}
|
||||
super([value]);
|
||||
}
|
||||
|
||||
addr_at(...args) {
|
||||
throw TypeError('unimplemented method');
|
||||
}
|
||||
|
||||
[Symbol.toPrimitive](hint) {
|
||||
return this[0];
|
||||
}
|
||||
|
||||
toString(...args) {
|
||||
return this[0].toString(...args);
|
||||
}
|
||||
};
|
||||
|
||||
export class Byte extends VariableMixin(View1) {}
|
||||
export class Short extends VariableMixin(View2) {}
|
||||
// Int was already taken by int64.mjs
|
||||
export class Word extends VariableMixin(View4) {}
|
||||
|
||||
export class LongArray {
|
||||
constructor(length) {
|
||||
this.buffer = new DataView(new ArrayBuffer(length * 8));
|
||||
}
|
||||
|
||||
get addr() {
|
||||
return mt.get_view_vector(this.buffer);
|
||||
}
|
||||
|
||||
addr_at(index) {
|
||||
return this.addr.add(index * 8);
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.buffer.length / 8;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.buffer.byteLength;
|
||||
}
|
||||
|
||||
get byteLength() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
get(index) {
|
||||
const buffer = this.buffer;
|
||||
const base = index * 8;
|
||||
return new Int(
|
||||
buffer.getUint32(base, true),
|
||||
buffer.getUint32(base + 4, true),
|
||||
);
|
||||
}
|
||||
|
||||
set(index, value) {
|
||||
const buffer = this.buffer;
|
||||
const base = index * 8;
|
||||
const values = lohi_from_one(value);
|
||||
|
||||
buffer.setUint32(base, values[0], true);
|
||||
buffer.setUint32(base + 4, values[1], true);
|
||||
}
|
||||
}
|
||||
|
||||
// mutable Int (we are explicitly using Int's private fields)
|
||||
const Word64Mixin = superclass => class extends superclass {
|
||||
constructor(...args) {
|
||||
if (!args.length) {
|
||||
return super(0);
|
||||
}
|
||||
super(...args);
|
||||
}
|
||||
|
||||
get addr() {
|
||||
// assume this is safe to cache
|
||||
return mt.get_view_vector(this._u32);
|
||||
}
|
||||
|
||||
get length() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
get byteLength() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
// no setters for top and bot since low/high can accept negative integers
|
||||
|
||||
get lo() {
|
||||
return super.lo;
|
||||
}
|
||||
|
||||
set lo(value) {
|
||||
this._u32[0] = value;
|
||||
}
|
||||
|
||||
get hi() {
|
||||
return super.hi;
|
||||
}
|
||||
|
||||
set hi(value) {
|
||||
this._u32[1] = value;
|
||||
}
|
||||
|
||||
set(value) {
|
||||
const buffer = this._u32;
|
||||
const values = lohi_from_one(value);
|
||||
|
||||
buffer[0] = values[0];
|
||||
buffer[1] = values[1];
|
||||
}
|
||||
};
|
||||
|
||||
export class Long extends Word64Mixin(Int) {
|
||||
as_addr() {
|
||||
return new Addr(this);
|
||||
}
|
||||
}
|
||||
export class Pointer extends Word64Mixin(Addr) {}
|
||||
Reference in New Issue
Block a user