Files

628 lines
28 KiB
C

/* SPDX-License-Identifier: GPL-3.0-or-later
* Exact-firmware installer with optional live identity discovery. Publication is disabled until the
* independently reviewed native exception barrier and package both declare readiness. No allocation
* is released after any attempted executor-vtable publication.
*/
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
/* SDK ships sha256.h without libmd. Use its already installed static libcrypto
* primitive API, retaining its supported SHA256 context ABI. No TLS/network. */
#define OPENSSL_API_COMPAT 0x10100000L
#include <openssl/sha.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <ps5/kernel.h>
#include "cc_executor_guard.h"
#include "cc_bootstrap_stub.h"
#include "cc_bootstrap_observe.h"
#include "remote_trace.h"
#ifndef ND_CC_PAYLOAD_HEADER
#define ND_CC_PAYLOAD_HEADER "../build/cc-install-payload.h"
#endif
#include ND_CC_PAYLOAD_HEADER
#ifndef ND_CC_AUTODETECT_IDENTITY
#define ND_CC_AUTODETECT_IDENTITY 0
#endif
#ifndef ND_CC_ENABLE_PUBLICATION
#define ND_CC_ENABLE_PUBLICATION 0
#endif
#if ND_CC_PACKAGE_SCHEMA != 1
#error Unsupported CC package schema
#endif
#if ND_CC_ENABLE_PUBLICATION && !ND_CC_CPP_BARRIER_READY
#error Publication requires the reviewed native exception barrier
#endif
static const volatile unsigned publication_enabled = ND_CC_ENABLE_PUBLICATION;
#ifndef ND_CC_CONFIGURE_HTTP_AUDIO
#define ND_CC_CONFIGURE_HTTP_AUDIO 0
#endif
#if ND_CC_CONFIGURE_HTTP_AUDIO != 0 && ND_CC_CONFIGURE_HTTP_AUDIO != 1
#error ND_CC_CONFIGURE_HTTP_AUDIO must be exactly 0 or 1
#endif
static const unsigned configure_http_audio = ND_CC_CONFIGURE_HTTP_AUDIO;
static const volatile unsigned exception_barrier_ready = ND_CC_CPP_BARRIER_READY;
#define ND_MAX_IMAGE (ND_TRACE_MAX_INITIALIZED_MAPPING - ND_STUB_SCRIPT_OFFSET)
#define ND_MAX_MAPPING ND_TRACE_MAX_INITIALIZED_MAPPING
static const char source_url[] = "navidrome://cc-local-entry";
_Static_assert(sizeof(struct nd_bootstrap_state) < ND_STUB_SOURCE_URL_OFFSET - ND_STUB_STATE_OFFSET,
"state/source URL overlap");
_Static_assert(ND_STUB_STATE_OFFSET == 0x4000 && ND_STUB_SCRIPT_OFFSET == 0x8000,
"stub fixed layout");
struct file_proof {
const char *path, *expected;
size_t size, bytes;
int accepted, error;
char digest[65];
};
struct install_report {
const char *stage, *failure;
time_t started, finished;
pid_t target_pid;
struct nd_cc_identity before, after;
struct nd_cc_libc_identity libc_before, libc_after;
struct nd_cc_feature0_identity feature_before, feature_after;
struct nd_bootstrap_observation observation;
struct file_proof files[3];
uint64_t mapping, clone, observed_vtable;
size_t mapping_size, script_mapping_size;
int allocation, copied, protections, guards_match, native_code_before, native_code_after;
int publication_attempted, publication_confirmed;
int rollback_attempted, rollback_confirmed, unmap_attempted, unmapped;
int close_result, recovery_retained, report_error;
};
static int digest_matches(const void *data, size_t size, const char *expected) {
if (strlen(expected) != 64)
return 0;
unsigned char digest[32];
char hex[65];
SHA256_CTX ctx;
if (!SHA256_Init(&ctx) || !SHA256_Update(&ctx, data, size) || !SHA256_Final(digest, &ctx))
return 0;
for (size_t i = 0; i < 32; ++i)
snprintf(hex + i * 2, 3, "%02x", digest[i]);
return !strcmp(hex, expected);
}
static int hash_file(struct file_proof *p) {
FILE *f = fopen(p->path, "rb");
if (!f) {
p->error = errno ? errno : EIO;
return -1;
}
struct stat before, after;
unsigned char buffer[16384], digest[32];
SHA256_CTX ctx;
if (!SHA256_Init(&ctx)) {
p->error = EIO;
goto done;
}
if (fstat(fileno(f), &before) || !S_ISREG(before.st_mode) || before.st_size != (off_t)p->size) {
p->error = ESTALE;
goto done;
}
while (p->bytes < p->size) {
size_t count = p->size - p->bytes;
if (count > sizeof(buffer))
count = sizeof(buffer);
const size_t n = fread(buffer, 1, count, f);
if (!n) {
p->error = ferror(f) && errno ? errno : EIO;
goto done;
}
if (!SHA256_Update(&ctx, buffer, n)) {
p->error = EIO;
goto done;
}
p->bytes += n;
}
if (fgetc(f) != EOF || ferror(f) || fstat(fileno(f), &after) || before.st_dev != after.st_dev ||
before.st_ino != after.st_ino || before.st_size != after.st_size ||
before.st_mtime != after.st_mtime) {
p->error = ESTALE;
goto done;
}
if (!SHA256_Final(digest, &ctx)) {
p->error = EIO;
goto done;
}
for (size_t i = 0; i < 32; ++i)
snprintf(p->digest + i * 2, 3, "%02x", digest[i]);
p->accepted = !strcmp(p->digest, p->expected);
if (!p->accepted)
p->error = ESTALE;
done:
if (fclose(f) && !p->error)
p->error = errno ? errno : EIO;
return p->error ? -1 : 0;
}
static int package_identity(const struct nd_cc_identity *i) {
if (!(i->complete && i->stable && i->credentials_restored && i->backend == 1 &&
i->initial_pid > 0 && i->initial_pid == i->final_pid &&
i->firmware == ND_CC_EXPECTED_FIRMWARE))
return 0;
#if ND_CC_AUTODETECT_IDENTITY
/* Full pointer/type/module guard remains authoritative. No wildcard read
* address: these values were discovered while this exact PID is stopped. */
return i->native_executor && i->runtime && i->app_instance_id && i->js_thread;
#else
return i->initial_pid == ND_CC_EXPECTED_PID && i->native_executor == ND_CC_EXPECTED_EXECUTOR &&
i->runtime == ND_CC_EXPECTED_RUNTIME &&
i->app_instance_id == ND_CC_EXPECTED_APP_INSTANCE_ID &&
i->js_thread == ND_CC_EXPECTED_JS_THREAD;
#endif
}
static int same_identity(const struct nd_cc_identity *a, const struct nd_cc_identity *b) {
if (!package_identity(a) || !package_identity(b))
return 0;
if (a->initial_pid != b->initial_pid || a->native_executor != b->native_executor ||
a->runtime != b->runtime || a->app_instance_id != b->app_instance_id ||
a->js_thread != b->js_thread)
return 0;
if (a->application != b->application || a->context != b->context || a->data != b->data ||
a->bridge != b->bridge || a->managed_executor != b->managed_executor ||
a->native_vtable != b->native_vtable ||
a->runtime_vtable_candidate != b->runtime_vtable_candidate ||
strcmp(a->application_name, b->application_name) ||
strcmp(a->bundle_path, b->bundle_path) ||
memcmp(a->native_methods, b->native_methods, sizeof(a->native_methods)))
return 0;
for (size_t n = 0; n < 4; ++n)
if (!a->modules[n].guard_accepted || !b->modules[n].guard_accepted ||
!a->modules[n].identity_unchanged || !b->modules[n].identity_unchanged ||
a->modules[n].base != b->modules[n].base ||
a->modules[n].handle != b->modules[n].handle)
return 0;
return 1;
}
static int write_and_verify(struct nd_trace *t, uint64_t address, const void *data, size_t size) {
const unsigned char *src = data;
unsigned char copy[4096];
for (size_t pos = 0; pos < size;) {
size_t n = size - pos;
if (n > sizeof(copy))
n = sizeof(copy);
if (nd_trace_write(t, address + pos, src + pos, n) ||
nd_trace_read(t, address + pos, copy, n) || memcmp(copy, src + pos, n))
return -1;
pos += n;
}
return 0;
}
static int native_entry_bytes(struct nd_trace *t, const struct nd_cc_identity *i) {
/* Captured native-extensions.manifest.json, code at actual VAs
* 0x806e3cf10, 0x806e381f0, 0x806e25af0. Relocate only after module guard. */
static const struct {
uint64_t offset;
unsigned char bytes[16];
} expected[] = {
{0x30f10,
{0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x81,
0xec}},
{0x2c1f0,
{0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83,
0xec}},
{0x19af0,
{0x48, 0x85, 0xff, 0x0f, 0x85, 0x17, 0x01, 0x00, 0x00, 0xc3, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc,
0xcc}},
};
for (size_t n = 0; n < sizeof(expected) / sizeof(expected[0]); ++n) {
unsigned char raw[16];
if (nd_trace_read(t, i->modules[3].base + expected[n].offset, raw, sizeof(raw)) ||
memcmp(raw, expected[n].bytes, sizeof(raw)))
return 0;
}
return 1;
}
static void identity_json(FILE *f, const struct nd_cc_identity *i) {
fprintf(f,
"{\"complete\":%s,\"stable\":%s,\"credentialsRestored\":%s,\"pid\":%d,\"finalPid\":%d,"
"\"stage\":\"%s\",\"failure\":\"%s\",\"executor\":\"0x%" PRIxPTR
"\",\"vtable\":\"0x%" PRIxPTR "\","
"\"runtime\":\"0x%" PRIxPTR "\",\"appInstanceId\":\"0x%" PRIx64 "\",\"backend\":%d,"
"\"name\":\"%s\",\"bundle\":\"%s\",\"records\":%zu,\"bytesRequested\":%zu,"
"\"application\":\"0x%" PRIxPTR "\",\"context\":\"0x%" PRIxPTR
"\",\"data\":\"0x%" PRIxPTR "\","
"\"bridge\":\"0x%" PRIxPTR "\",\"managedExecutor\":\"0x%" PRIxPTR
"\",\"jsThread\":\"0x%" PRIxPTR "\","
"\"modules\":[",
i->complete ? "true" : "false", i->stable ? "true" : "false",
i->credentials_restored ? "true" : "false", i->initial_pid, i->final_pid,
i->stage ? i->stage : "", i->failure ? i->failure : "", i->native_executor,
i->native_vtable, i->runtime, i->app_instance_id, i->backend,
i->complete ? i->application_name : "", i->complete ? i->bundle_path : "", i->records,
i->bytes_requested, i->application, i->context, i->data, i->bridge, i->managed_executor,
i->js_thread);
for (size_t n = 0; n < 4; ++n) {
const struct nd_cc_guard_module *m = &i->modules[n];
if (n)
fputc(',', f);
fprintf(f,
"{\"name\":\"%s\",\"base\":\"0x%" PRIxPTR
"\",\"handle\":%u,\"guardAccepted\":%s,\"identityUnchanged\":%s}",
m->name ? m->name : "", m->base, m->handle, m->guard_accepted ? "true" : "false",
m->identity_unchanged ? "true" : "false");
}
fputs("]}", f);
}
static void libc_json(FILE *f, const struct nd_cc_libc_identity *i) {
fprintf(f,
"{\"accepted\":%s,\"failure\":\"%s\",\"base\":\"0x%" PRIxPTR "\",\"handle\":%u,"
"\"guardAccepted\":%s,\"identityUnchanged\":%s,\"bytesRequested\":%zu,"
"\"beginCatch\":\"0x%" PRIx64 "\",\"endCatch\":\"0x%" PRIx64
"\",\"personality\":\"0x%" PRIx64 "\","
"\"addDynamicFde\":\"0x%" PRIx64 "\",\"iterateFdeCache\":\"0x%" PRIx64 "\"}",
i->accepted ? "true" : "false", i->failure ? i->failure : "", i->module.base,
i->module.handle, i->module.guard_accepted ? "true" : "false",
i->module.identity_unchanged ? "true" : "false", i->bytes_requested, i->cxa_begin_catch,
i->cxa_end_catch, i->personality, i->add_dynamic_fde, i->iterate_fde_cache);
}
int main(void) {
static struct install_report r;
r.stage = "package";
r.started = time(NULL);
r.close_result = 0;
const char *id = ND_CC_PACKAGE_ID;
if (!*id || strlen(id) > 48)
return 3;
for (const char *p = id; *p; ++p)
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
*p == '_' || *p == '-'))
return 3;
char report_path[192];
snprintf(report_path, sizeof(report_path), "/data/navidrome-cc-install-%s-%jd-%d.json", id,
(intmax_t)r.started, getpid());
const int fd = open(report_path, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd < 0)
return 3;
FILE *out = fdopen(fd, "w");
if (!out) {
close(fd);
return 3;
}
struct nd_trace *trace = NULL;
struct nd_bootstrap_state state = {0};
struct file_proof feature_file = {
.path = "/system/common/lib/libSceMediaFrameworkInterface.sprx",
.size = 127728,
.expected = "82819bc39eb313f49e25ec56a66e96caa3deecbc820ed4de0e8f07e1a08c0683"};
struct file_proof feature_file_after = feature_file;
struct file_proof media_file = {
.path = "/system_ex/common_ex/lib/libReactNative.Modules.Vsh.Media.sprx",
.size = 36260,
.expected = "cc899b57f5e83c351c18d0911003a88dd8dfcf1d01329b99c436fe784e2e1a39"};
struct file_proof media_file_after = media_file;
if (configure_http_audio && (hash_file(&feature_file) || hash_file(&media_file))) {
r.failure = "feature0_installed_self_hash";
goto report;
}
if (!publication_enabled || !exception_barrier_ready) {
r.failure = "publication_disabled_pending_native_exception_barrier";
goto report;
}
if (kernel_get_fw_version() != ND_CC_EXPECTED_FIRMWARE ||
ND_CC_EXPECTED_FIRMWARE != UINT32_C(0x13200006)) {
r.failure = "firmware_mismatch";
goto report;
}
if (!ND_CC_STUB_CODE_BYTES || ND_CC_STUB_CODE_BYTES > ND_STUB_PAGE ||
ND_CC_STUB_CODE_BYTES > sizeof(nd_cc_stub_image) ||
sizeof(nd_cc_stub_image) > ND_STUB_SCRIPT_OFFSET || sizeof(nd_cc_hbc_image) < 128 ||
sizeof(nd_cc_hbc_image) > ND_MAX_IMAGE ||
!digest_matches(nd_cc_stub_image, sizeof(nd_cc_stub_image), ND_CC_STUB_SHA256) ||
!digest_matches(nd_cc_hbc_image, sizeof(nd_cc_hbc_image), ND_CC_HBC_SHA256)) {
r.failure = "package_image_size_or_hash_mismatch";
goto report;
}
/* This profile is tied to the captured Hermes bytecode version 89. */
const unsigned char hbc_prefix[12] = {0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03,
0x19, 0x1f, 0x59, 0, 0, 0};
uint32_t hbc_file_length = 0;
memcpy(&hbc_file_length, nd_cc_hbc_image + 32, sizeof(hbc_file_length));
if (sizeof(nd_cc_hbc_image) < 128 || memcmp(nd_cc_hbc_image, hbc_prefix, sizeof(hbc_prefix)) ||
hbc_file_length != sizeof(nd_cc_hbc_image)) {
r.failure = "package_hbc_header_mismatch";
goto report;
}
r.script_mapping_size =
(sizeof(nd_cc_hbc_image) + ND_STUB_PAGE - 1) & ~(size_t)(ND_STUB_PAGE - 1);
r.mapping_size = ND_STUB_SCRIPT_OFFSET + r.script_mapping_size;
if (r.mapping_size > ND_MAX_MAPPING) {
r.failure = "package_exceeds_mapping_initialization_budget";
goto report;
}
r.files[0] = (struct file_proof){
.path = "/system_ex/rnps/apps/NPXS40003/application.ps.bundle",
.size = 6494240,
.expected = "952d0615f36a1db9a1fa23f5c5dd190dcb2f53f6b8827cdddc9340d4561114f7"};
r.files[1] = (struct file_proof){
.path = "/system_ex/rnps/apps/NPXS40141/base_dll.ps.bundle",
.size = 1026448,
.expected = "33d67df1d95b834015d2c8ae4750d70cb54656954afda3b4227f147cac7b1b40"};
r.files[2] = (struct file_proof){
.path = "/system_ex/rnps/apps/NPXS40141/base_dll.ps.bundle.hbc",
.size = 839040,
.expected = "e3a29881ce96604cbd06898d16bb93d199a99f4f62711954a823ab1cd5a22210"};
r.stage = "installed_bundle_hashes_before_attach";
for (size_t i = 0; i < 3; ++i)
if (hash_file(&r.files[i])) {
r.failure = "installed_bundle_hash_mismatch";
goto report;
}
r.target_pid = ND_CC_EXPECTED_PID;
#if ND_CC_AUTODETECT_IDENTITY
r.stage = "discover_shellui";
if (nd_cc_guard_find_shellui(&r.target_pid)) {
r.failure = "shellui_discovery_failed";
goto report;
}
#endif
r.stage = "trace_attach";
trace = nd_trace_begin(r.target_pid);
if (!nd_trace_ready(trace)) {
r.failure = "trace_not_ready";
goto cleanup;
}
r.stage = "stopped_identity_guard";
if (nd_cc_guard_capture(r.target_pid, &r.before) || !package_identity(&r.before)) {
r.failure = "stopped_identity_differs_from_package";
goto cleanup;
}
r.native_code_before = native_entry_bytes(trace, &r.before);
if (!r.native_code_before) {
r.failure = "native_entry_bytes_mismatch";
goto cleanup;
}
r.stage = "stopped_libc_eh_guard";
if (nd_cc_guard_libc(r.target_pid, &r.libc_before)) {
r.failure = "libc_eh_guard_failed";
goto cleanup;
}
if (configure_http_audio) {
r.stage = "stopped_feature0_guard";
if (nd_cc_guard_feature0(r.target_pid, &r.feature_before)) {
r.failure = "feature0_guard_failed";
goto cleanup;
}
}
r.stage = "allocate_rw";
if (r.mapping_size > ND_MAX_MAPPING || nd_trace_map(trace, r.mapping_size, &r.mapping)) {
r.failure = "mapping_not_confirmed";
goto cleanup;
}
r.allocation = 1;
r.stage = "initialize_private_mapping_pages";
if (nd_trace_initialize_mapping(trace, r.mapping, r.mapping_size)) {
r.failure = "mapping_initialization_not_confirmed";
goto cleanup;
}
const uint64_t state_address = r.mapping + ND_STUB_STATE_OFFSET;
r.clone = state_address + offsetof(struct nd_bootstrap_state, cloned_table) + 16;
state.magic = ND_STUB_MAGIC;
state.executor = r.before.native_executor;
state.runtime = r.before.runtime;
state.original_vtable = r.before.native_vtable;
state.cloned_vtable = r.clone;
state.original_call = r.before.native_methods[5];
/* Original native method avoids the exported wrapper's seventh offset arg. */
state.evaluate = r.before.native_methods[4];
state.free_string = r.before.modules[3].base + 0x19af0;
state.script = r.mapping + ND_STUB_SCRIPT_OFFSET;
state.script_bytes = sizeof(nd_cc_hbc_image);
state.source_url = r.mapping + ND_STUB_SOURCE_URL_OFFSET;
state.app_instance_id = r.before.app_instance_id;
state.cxa_begin_catch = r.libc_before.cxa_begin_catch;
state.cxa_end_catch = r.libc_before.cxa_end_catch;
state.personality = r.libc_before.personality;
state.add_dynamic_fde = r.libc_before.add_dynamic_fde;
state.iterate_fde_cache = r.libc_before.iterate_fde_cache;
state.feature0_result = INT32_MIN;
state.feature0_gate_result = INT32_MIN;
if (configure_http_audio) {
state.feature0_setter = r.feature_before.setter;
state.feature0_enabled = 1;
state.feature0_get_service = r.feature_before.get_service;
state.feature0_get_info = r.feature_before.get_info;
state.feature0_gate_enabled = 1;
}
state.cloned_table[0] = 0;
state.cloned_table[1] = r.before.modules[3].base + 0x953b8;
memcpy(state.cloned_table + 2, r.before.native_methods, sizeof(r.before.native_methods));
state.cloned_table[2 + 5] = r.mapping;
r.stage = "copy_readback";
if (write_and_verify(trace, r.mapping, nd_cc_stub_image, ND_CC_STUB_CODE_BYTES) ||
write_and_verify(trace, state_address, &state, sizeof(state)) ||
write_and_verify(trace, state.source_url, source_url, sizeof(source_url)) ||
write_and_verify(trace, state.script, nd_cc_hbc_image, sizeof(nd_cc_hbc_image))) {
r.failure = "image_copy_or_readback_failed";
goto cleanup;
}
r.copied = 1;
r.stage = "protect_code_rx_script_r";
if (nd_trace_protect(trace, r.mapping, ND_STUB_PAGE, PROT_READ | PROT_EXEC) ||
nd_trace_protect(trace, state.script, r.script_mapping_size, PROT_READ)) {
r.failure = "image_protection_failed";
goto cleanup;
}
r.protections = 1;
r.stage = "identity_recheck_before_publication";
if (nd_cc_guard_capture(r.target_pid, &r.after) || !same_identity(&r.before, &r.after)) {
r.failure = "identity_changed_before_publication";
goto cleanup;
}
r.native_code_after = native_entry_bytes(trace, &r.after);
if (!r.native_code_after) {
r.failure = "native_entry_bytes_changed";
goto cleanup;
}
if (nd_cc_guard_libc(r.target_pid, &r.libc_after) ||
r.libc_after.module.base != r.libc_before.module.base ||
r.libc_after.module.handle != r.libc_before.module.handle) {
r.failure = "libc_eh_guard_changed";
goto cleanup;
}
if (configure_http_audio &&
(nd_cc_guard_feature0(r.target_pid, &r.feature_after) ||
r.feature_before.module.base != r.feature_after.module.base ||
r.feature_before.module.handle != r.feature_after.module.handle ||
r.feature_before.setter != r.feature_after.setter ||
r.feature_before.get_service != r.feature_after.get_service ||
r.feature_before.get_info != r.feature_after.get_info || hash_file(&feature_file_after) ||
!feature_file.accepted || !feature_file_after.accepted ||
strcmp(feature_file.digest, feature_file_after.digest) || hash_file(&media_file_after) ||
!media_file.accepted || !media_file_after.accepted ||
strcmp(media_file.digest, media_file_after.digest))) {
r.failure = "feature0_guard_changed";
goto cleanup;
}
r.guards_match = 1;
if (nd_trace_read(trace, state.executor, &r.observed_vtable, 8) ||
r.observed_vtable != state.original_vtable) {
r.failure = "vtable_changed_before_publication";
goto cleanup;
}
r.stage = "publish_vtable";
r.publication_attempted = 1; /* From here allocation is retained forever. */
if (nd_trace_write(trace, state.executor, &r.clone, 8) ||
nd_trace_read(trace, state.executor, &r.observed_vtable, 8) ||
r.observed_vtable != r.clone) {
r.failure = "publication_write_or_readback_failed";
goto rollback;
}
r.publication_confirmed = 1;
goto cleanup;
rollback:
r.rollback_attempted = 1;
if (!nd_trace_read(trace, state.executor, &r.observed_vtable, 8)) {
if (r.observed_vtable == state.original_vtable)
r.rollback_confirmed = 1;
else if (r.observed_vtable == r.clone &&
!nd_trace_write(trace, state.executor, &state.original_vtable, 8) &&
!nd_trace_read(trace, state.executor, &r.observed_vtable, 8) &&
r.observed_vtable == state.original_vtable)
r.rollback_confirmed = 1;
}
if (!r.rollback_confirmed) {
r.recovery_retained = 1;
nd_trace_retain(trace, "publication rollback unconfirmed; keep tracer and mapping alive");
goto report;
}
cleanup:
if (trace) {
if (r.allocation && !r.publication_attempted && nd_trace_ready(trace)) {
r.unmap_attempted = 1;
r.unmapped = nd_trace_unmap(trace, r.mapping, r.mapping_size) == 0;
}
r.close_result = nd_trace_close(trace);
if (r.close_result)
r.recovery_retained = 1;
}
if (r.publication_confirmed && !r.close_result && !r.recovery_retained) {
r.stage = "postpublication_read_only_observation";
nd_bootstrap_observe(r.target_pid, r.mapping, ND_CC_STUB_CODE_BYTES, &state,
&r.observation);
if (!r.observation.complete)
r.failure = "postpublication_observation_failed";
else if (!strcmp(r.observation.status, "native_exception"))
r.failure = "native_exception_observed";
else if (!strcmp(r.observation.status, "native_barrier_rejected"))
r.failure = "native_barrier_rejected";
else if (!strcmp(r.observation.status, "vtable_restore_unconfirmed") ||
!strcmp(r.observation.status, "vtable_unexpected"))
r.failure = "postpublication_vtable_unconfirmed";
}
report:
r.finished = time(NULL);
fprintf(out, "{\"identityMode\":\"%s\",\"detectedPid\":%d,",
ND_CC_AUTODETECT_IDENTITY ? "automatic-runtime" : "captured-session", r.target_pid);
fprintf(
out,
"\"schema\":1,\"packageId\":\"%s\",\"packageSha256\":\"%s\","
"\"publicationEnabled\":%s,\"exceptionBarrierReady\":%s,\"stage\":\"%s\",\"failure\":\"%"
"s\","
"\"startedUnix\":%jd,\"finishedUnix\":%jd,\"mapping\":\"0x%" PRIx64
"\",\"mappingBytes\":%zu,"
"\"cloneVtable\":\"0x%" PRIx64 "\",\"copiedAndReadback\":%s,\"protectionsConfirmed\":%s,"
"\"guardsMatch\":%s,\"nativeCodeBefore\":%s,\"nativeCodeAfter\":%s,"
"\"publicationAttempted\":%s,\"publicationConfirmed\":%s,"
"\"rollbackAttempted\":%s,\"rollbackConfirmed\":%s,\"unmapAttempted\":%s,\"unmapped\":%s,"
"\"allocationRetained\":%s,\"recoveryRetained\":%s,\"closeResult\":%d,\"installedFiles\":[",
id, ND_CC_PACKAGE_SHA256, publication_enabled ? "true" : "false",
exception_barrier_ready ? "true" : "false", r.stage, r.failure ? r.failure : "",
(intmax_t)r.started, (intmax_t)r.finished, r.mapping, r.mapping_size, r.clone,
r.copied ? "true" : "false", r.protections ? "true" : "false",
r.guards_match ? "true" : "false", r.native_code_before ? "true" : "false",
r.native_code_after ? "true" : "false", r.publication_attempted ? "true" : "false",
r.publication_confirmed ? "true" : "false", r.rollback_attempted ? "true" : "false",
r.rollback_confirmed ? "true" : "false", r.unmap_attempted ? "true" : "false",
r.unmapped ? "true" : "false", r.allocation && !r.unmapped ? "true" : "false",
r.recovery_retained ? "true" : "false", r.close_result);
for (size_t i = 0; i < 3; ++i) {
const struct file_proof *p = &r.files[i];
if (i)
fputc(',', out);
fprintf(
out, "{\"path\":\"%s\",\"bytes\":%zu,\"sha256\":\"%s\",\"accepted\":%s,\"error\":%d}",
p->path ? p->path : "", p->bytes, p->digest, p->accepted ? "true" : "false", p->error);
}
fputs("],\"identityBefore\":", out);
identity_json(out, &r.before);
fputs(",\"identityAfter\":", out);
identity_json(out, &r.after);
fprintf(
out,
",\"feature0Guard\":{\"beforeAccepted\":%d,\"afterAccepted\":%d,\"setter\":\"0x%" PRIx64
"\",\"resolved\":\"0x%" PRIx64 "\",\"guardMask\":%u,\"readError\":%d,\"failure\":\"%s\"}",
r.feature_before.accepted, r.feature_after.accepted, r.feature_before.setter,
r.feature_before.resolved, r.feature_before.entry_guard_mask, r.feature_before.read_error,
r.feature_before.failure ? r.feature_before.failure
: r.feature_after.failure ? r.feature_after.failure
: "");
fprintf(out,
",\"feature0Files\":{\"beforeAccepted\":%d,\"afterAccepted\":%d,\"beforeBytes\":%zu,"
"\"afterBytes\":%zu,\"beforeError\":%d,\"afterError\":%d,\"beforeSha256\":\"%s\","
"\"afterSha256\":\"%s\"}",
feature_file.accepted, feature_file_after.accepted, feature_file.bytes,
feature_file_after.bytes, feature_file.error, feature_file_after.error,
feature_file.digest, feature_file_after.digest);
fputs(",\"libcBefore\":", out);
libc_json(out, &r.libc_before);
fputs(",\"libcAfter\":", out);
libc_json(out, &r.libc_after);
fputs(",\"trace\":", out);
nd_trace_json(out, trace);
fputs(",\"observation\":", out);
nd_bootstrap_observation_json(out, &r.observation);
fprintf(out,
",\"status\":\"%s\",\"notice\":\"Installed file hashes are provenance, not a read of "
"live HBC. Publication does not prove JS executed. Published memory remains until "
"ShellUI exits.\"}\n",
r.recovery_retained ? "operator_recovery_required"
: r.observation.attempted ? r.observation.status
: r.publication_confirmed && !r.close_result ? "published_pending_execution"
: "not_published");
r.report_error = ferror(out);
if (fclose(out))
r.report_error = 1;
if (r.recovery_retained) {
/* Exiting a still-attached tracer can terminate the tracee. Retain
* this payload; the report explicitly requires operator recovery. */
for (;;)
sleep(1);
}
nd_trace_destroy(trace);
return r.report_error || r.failure || !r.publication_confirmed ? 2 : 0;
}