mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 21:00:24 +02:00
json_get_str now decodes the common JSON escapes (\" \\ \/ \n \r \t \b \f) and collapses \uXXXX to '?'. Previously \" terminated the value early and \\ was copied literal, so a body containing escapes turned into garbage at the install backend. default_policy is constrained to "allow" or "deny" before being stored, so a malformed POST can't write an arbitrary string into config.json and round-trip it back out of /api/config as broken JSON. /api/manifest/<tid>, /api/pkgverify/<tid>, /api/pkgmeta/<tid> now run path_segment_safe(tid) explicitly. The lookup gate they relied on (get_title_action_info) is defense-by-coincidence — a future refactor that populates g_titles via another path would lose the check. patchdl_scan and patchdl_scan_debug_json perform a process-wide vnode swap that is only safe single-threaded. patchdl_scan_lock() is now called once right after MHD_start_daemon; subsequent calls return -1 / NULL instead of racing the worker threads. EVP_DigestFinal_ex return code is now checked. A failed final left dig uninitialized; hex_encode would have produced empty hex and a silent -2 with no diagnostic. patchdl_sha256_fd_region switches its inner sprintf to snprintf — same effect, no -Wformat-security warning.
548 lines
17 KiB
C
548 lines
17 KiB
C
#include "patchdl_scan.h"
|
|
#include "patchdl_appdb.h"
|
|
|
|
#include <ps5/kernel.h>
|
|
|
|
#include <dirent.h>
|
|
#include <limits.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/mount.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#define MAX_TITLES 512
|
|
|
|
static const char *SCAN_DIRS[] = {
|
|
"/user/app",
|
|
"/mnt/ext0/user/app",
|
|
"/mnt/ext1/user/app",
|
|
"/system_ex/app",
|
|
NULL,
|
|
};
|
|
|
|
/* ---------- param.json -------------------------------------------------- */
|
|
|
|
static int
|
|
json_str(const char *json, const char *key, char *out, size_t out_sz) {
|
|
char needle[80];
|
|
const char *p;
|
|
size_t len;
|
|
|
|
if ((size_t)snprintf(needle, sizeof(needle), "\"%s\"", key) >= sizeof(needle))
|
|
return -1;
|
|
|
|
p = strstr(json, needle);
|
|
if (!p) return -1;
|
|
p += strlen(needle);
|
|
while (*p == ' ' || *p == '\t' || *p == ':' || *p == '\n' || *p == '\r')
|
|
p++;
|
|
if (*p != '"') return -1;
|
|
p++;
|
|
for (len = 0; p[len] && p[len] != '"' && len < out_sz - 1; len++)
|
|
;
|
|
memcpy(out, p, len);
|
|
out[len] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
/* Extract the title name for the default language out of the nested
|
|
localizedParameters block. PS5 param.json has no flat titleName key. */
|
|
static void
|
|
json_title_name(const char *json, char *out, size_t out_sz) {
|
|
char lang[16] = {0};
|
|
const char *p;
|
|
|
|
if (!json_str(json, "defaultLanguage", lang, sizeof(lang)) && lang[0]) {
|
|
char anchor[24];
|
|
if ((size_t)snprintf(anchor, sizeof(anchor), "\"%s\"", lang) <
|
|
sizeof(anchor)) {
|
|
p = strstr(json, anchor);
|
|
if (p) {
|
|
const char *tn = strstr(p, "\"titleName\"");
|
|
if (tn && !json_str(tn, "titleName", out, out_sz))
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Fallback: first titleName anywhere in the file */
|
|
json_str(json, "titleName", out, out_sz);
|
|
}
|
|
|
|
static int
|
|
try_param_json(const char *dir, patchdl_title_t *t) {
|
|
char path[PATH_MAX];
|
|
FILE *fp;
|
|
char buf[8192];
|
|
size_t n;
|
|
|
|
snprintf(path, sizeof(path), "%s/sce_sys/param.json", dir);
|
|
fp = fopen(path, "r");
|
|
if (!fp) return -1;
|
|
n = fread(buf, 1, sizeof(buf) - 1, fp);
|
|
fclose(fp);
|
|
if (!n) return -1;
|
|
buf[n] = '\0';
|
|
|
|
/* PS5 keys first, then PS4 fallbacks */
|
|
if (json_str(buf, "titleId", t->title_id, sizeof(t->title_id)) &&
|
|
json_str(buf, "TITLE_ID", t->title_id, sizeof(t->title_id)))
|
|
return -1;
|
|
|
|
json_str(buf, "contentId", t->content_id, sizeof(t->content_id));
|
|
json_str(buf, "contentVersion", t->installed_version, sizeof(t->installed_version));
|
|
if (!t->content_id[0])
|
|
json_str(buf, "CONTENT_ID", t->content_id, sizeof(t->content_id));
|
|
if (!t->installed_version[0])
|
|
json_str(buf, "APP_VER", t->installed_version, sizeof(t->installed_version));
|
|
|
|
json_title_name(buf, t->name, sizeof(t->name));
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ---------- param.sfo --------------------------------------------------- */
|
|
|
|
#pragma pack(push, 1)
|
|
typedef struct {
|
|
uint32_t magic; /* 0x46535000 = "\x00PSF" */
|
|
uint32_t version;
|
|
uint32_t key_table_start;
|
|
uint32_t data_table_start;
|
|
uint32_t num_entries;
|
|
} sfo_hdr_t;
|
|
|
|
typedef struct {
|
|
uint16_t key_offset;
|
|
uint16_t data_fmt;
|
|
uint32_t data_len;
|
|
uint32_t data_max_len;
|
|
uint32_t data_offset;
|
|
} sfo_entry_t;
|
|
#pragma pack(pop)
|
|
|
|
#define SFO_MAGIC 0x46535000u
|
|
#define SFO_FMT_STR 0x0204u
|
|
|
|
static int
|
|
sfo_get(const uint8_t *buf, size_t bufsz, const char *key,
|
|
char *out, size_t out_sz) {
|
|
const sfo_hdr_t *h;
|
|
const sfo_entry_t *entries;
|
|
uint32_t i;
|
|
|
|
if (bufsz < sizeof(*h)) return -1;
|
|
h = (const sfo_hdr_t *)buf;
|
|
if (h->magic != SFO_MAGIC || h->num_entries > 256) return -1;
|
|
|
|
entries = (const sfo_entry_t *)(buf + sizeof(*h));
|
|
|
|
/* The entry table itself must fit in the bytes we actually read; a crafted
|
|
param.sfo (from a shadow-mounted game dir) could otherwise drive
|
|
entries[i] past the buffer. */
|
|
if (sizeof(*h) + (size_t)h->num_entries * sizeof(sfo_entry_t) > bufsz)
|
|
return -1;
|
|
|
|
for (i = 0; i < h->num_entries; i++) {
|
|
size_t key_off = h->key_table_start + entries[i].key_offset;
|
|
size_t val_off = h->data_table_start + entries[i].data_offset;
|
|
|
|
if (key_off >= bufsz || val_off >= bufsz) continue;
|
|
|
|
const char *k = (const char *)(buf + key_off);
|
|
size_t kmax = bufsz - key_off;
|
|
/* Require the key to be NUL-terminated within the buffer before the
|
|
strcmp, otherwise it would read past the end. */
|
|
if (strnlen(k, kmax) == kmax) continue;
|
|
if (strcmp(k, key)) continue;
|
|
if (entries[i].data_fmt != SFO_FMT_STR) return -1;
|
|
|
|
size_t len = entries[i].data_len;
|
|
if (val_off + len > bufsz) return -1;
|
|
if (len >= out_sz) len = out_sz - 1;
|
|
memcpy(out, buf + val_off, len);
|
|
out[len] = '\0';
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
try_param_sfo(const char *dir, patchdl_title_t *t) {
|
|
char path[PATH_MAX];
|
|
FILE *fp;
|
|
uint8_t buf[8192];
|
|
size_t n;
|
|
struct stat st;
|
|
|
|
snprintf(path, sizeof(path), "%s/sce_sys/param.sfo", dir);
|
|
fp = fopen(path, "rb");
|
|
if (!fp) return -1;
|
|
|
|
if (fstat(fileno(fp), &st) || (size_t)st.st_size > sizeof(buf)) {
|
|
fclose(fp);
|
|
return -1;
|
|
}
|
|
n = fread(buf, 1, (size_t)st.st_size, fp);
|
|
fclose(fp);
|
|
if (n != (size_t)st.st_size) return -1;
|
|
|
|
if (sfo_get(buf, n, "TITLE_ID", t->title_id, sizeof(t->title_id)))
|
|
return -1;
|
|
|
|
sfo_get(buf, n, "CONTENT_ID", t->content_id, sizeof(t->content_id));
|
|
sfo_get(buf, n, "APP_VER", t->installed_version, sizeof(t->installed_version));
|
|
sfo_get(buf, n, "TITLE", t->name, sizeof(t->name));
|
|
if (!t->name[0])
|
|
sfo_get(buf, n, "TITLE_00", t->name, sizeof(t->name));
|
|
return 0;
|
|
}
|
|
|
|
/* ---------- install-type classification (file-based) -------------------- */
|
|
|
|
static int
|
|
path_exists(const char *dir, const char *rel) {
|
|
char p[PATH_MAX];
|
|
struct stat st;
|
|
snprintf(p, sizeof(p), "%s/%s", dir, rel);
|
|
return stat(p, &st) == 0;
|
|
}
|
|
|
|
/* Read up to sz-1 bytes of dir/rel into buf (NUL-terminated). 0 on success. */
|
|
static int
|
|
read_small_file(const char *dir, const char *rel, char *buf, size_t sz) {
|
|
char p[PATH_MAX];
|
|
FILE *fp;
|
|
size_t n;
|
|
|
|
snprintf(p, sizeof(p), "%s/%s", dir, rel);
|
|
fp = fopen(p, "rb");
|
|
if (!fp) return -1;
|
|
n = fread(buf, 1, sz - 1, fp);
|
|
fclose(fp);
|
|
buf[n] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
/* Keep only actual games. Skip PS5 system apps (NPXS), the Payload Manager
|
|
fake app (PLDM) and ELF-loader homebrew (PSPS). */
|
|
static int
|
|
is_game_title(const char *title_id) {
|
|
if (!strncmp(title_id, "NPXS", 4)) return 0;
|
|
if (!strncmp(title_id, "PLDM", 4)) return 0;
|
|
if (!strncmp(title_id, "PSPS", 4)) return 0;
|
|
return 1;
|
|
}
|
|
|
|
/* ---------- directory scanner ------------------------------------------- */
|
|
|
|
/* Authoritative shadowmount test: ShadowMountPlus routes its images through
|
|
/mnt/shadowmnt (a pfs from /dev/lvdN there, then a nullfs onto the app dir),
|
|
so a title whose mount table references /mnt/shadowmnt is a shadowmount. The
|
|
on-disk mount.lnk marker is unreliable across reboots/remounts; the live
|
|
mount table is not. */
|
|
static int
|
|
mount_is_shadow(const char *title_id, const struct statfs *mounts, int nmounts) {
|
|
for (int i = 0; i < nmounts; i++) {
|
|
const char *from = mounts[i].f_mntfromname;
|
|
const char *on = mounts[i].f_mntonname;
|
|
if ((strstr(from, "/mnt/shadowmnt") || strstr(on, "/mnt/shadowmnt")) &&
|
|
(strstr(from, title_id) || strstr(on, title_id)))
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Classify each /user/app/<TID> (verified on fw 11.60):
|
|
* - mount table references /mnt/shadowmnt for the title -> ShadowMountPlus
|
|
* mount (authoritative; mount.lnk is only a fallback hint)
|
|
* - app.pkg (no shadow mount) -> genuine install; app.json with CDN piece
|
|
* URLs means a not-downloaded preinstall stub, local URLs a real install
|
|
* - app.json with "fake":true -> homebrew fake (skip)
|
|
*/
|
|
static int
|
|
scan_one(const char *base, const char *name, patchdl_title_t *t,
|
|
const struct statfs *mounts, int nmounts) {
|
|
char dir[PATH_MAX];
|
|
char appjson[4096];
|
|
int has_mountlnk, has_app_pkg, has_paramjson, is_shadow, is_fake = 0, is_cdn = 0;
|
|
|
|
snprintf(dir, sizeof(dir), "%s/%s", base, name);
|
|
memset(t, 0, sizeof(*t));
|
|
strncpy(t->source_path, dir, sizeof(t->source_path) - 1);
|
|
strncpy(t->title_id, name, sizeof(t->title_id) - 1);
|
|
|
|
if (!is_game_title(t->title_id))
|
|
return -1;
|
|
|
|
is_shadow = mount_is_shadow(t->title_id, mounts, nmounts);
|
|
has_mountlnk = path_exists(dir, "mount.lnk") ||
|
|
path_exists(dir, "mount_img.lnk");
|
|
has_app_pkg = path_exists(dir, "app.pkg");
|
|
has_paramjson = path_exists(dir, "sce_sys/param.json");
|
|
|
|
if (!read_small_file(dir, "app.json", appjson, sizeof(appjson))) {
|
|
if (strstr(appjson, "\"fake\"") && strstr(appjson, "true"))
|
|
is_fake = 1;
|
|
if (strstr(appjson, "http"))
|
|
is_cdn = 1; /* piece URLs point at the Sony CDN -> preinstall */
|
|
}
|
|
if (is_fake)
|
|
return -1;
|
|
|
|
/* app.pkg is the on-disk package of a genuine install; ShadowMountPlus
|
|
titles never have it (they have mounted/leftover sce_sys content). So
|
|
app.pkg is the reliable genuine-vs-shadow discriminator — independent of
|
|
whether the shadow image is currently mounted. */
|
|
if (has_app_pkg) {
|
|
t->source_type = is_cdn ? PATCHDL_SOURCE_UNKNOWN /* CDN pkg = preinstall */
|
|
: PATCHDL_SOURCE_OFFICIAL;
|
|
} else if (is_shadow || has_mountlnk || has_paramjson ||
|
|
path_exists(dir, "sce_sys/param.sfo")) {
|
|
if (try_param_json(dir, t)) /* metadata from the mounted sce_sys */
|
|
try_param_sfo(dir, t);
|
|
t->source_type = PATCHDL_SOURCE_SHADOWMOUNT;
|
|
} else {
|
|
return -1; /* empty / leftover directory */
|
|
}
|
|
|
|
if (!t->title_id[0])
|
|
strncpy(t->title_id, name, sizeof(t->title_id) - 1);
|
|
if (!t->name[0])
|
|
strncpy(t->name, t->title_id, sizeof(t->name) - 1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
already_seen(const patchdl_title_t *arr, size_t cnt, const char *title_id) {
|
|
for (size_t i = 0; i < cnt; i++)
|
|
if (!strcmp(arr[i].title_id, title_id))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
scan_base(const char *base, patchdl_title_t *arr, size_t *cnt, size_t cap,
|
|
const struct statfs *mounts, int nmounts) {
|
|
DIR *d;
|
|
struct dirent *de;
|
|
|
|
d = opendir(base);
|
|
if (!d) return;
|
|
|
|
while ((de = readdir(d))) {
|
|
if (de->d_name[0] == '.') continue;
|
|
if (*cnt >= cap) break;
|
|
if (scan_one(base, de->d_name, &arr[*cnt], mounts, nmounts) != 0)
|
|
continue;
|
|
if (already_seen(arr, *cnt, arr[*cnt].title_id))
|
|
continue; /* dedupe a title already found in an earlier base */
|
|
(*cnt)++;
|
|
}
|
|
closedir(d);
|
|
}
|
|
|
|
/* ---------- public API -------------------------------------------------- */
|
|
|
|
const char *
|
|
patchdl_source_str(patchdl_source_t src) {
|
|
switch (src) {
|
|
case PATCHDL_SOURCE_OFFICIAL: return "official";
|
|
case PATCHDL_SOURCE_EXTERNAL: return "external";
|
|
case PATCHDL_SOURCE_SHADOWMOUNT: return "shadowmount";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
/* Overlay app.db metadata (real title name, installed version, version.xml
|
|
URL) onto the scanned titles. Genuine installs have no readable param.json,
|
|
so this is the only source of their name/version. */
|
|
static void
|
|
merge_appdb(patchdl_title_t *arr, size_t cnt) {
|
|
patchdl_appinfo_t *info = NULL;
|
|
size_t n = 0;
|
|
|
|
if (patchdl_appdb_load(&info, &n))
|
|
return;
|
|
|
|
for (size_t i = 0; i < cnt; i++) {
|
|
const patchdl_appinfo_t *a = patchdl_appdb_find(info, n, arr[i].title_id);
|
|
if (!a) continue;
|
|
if (a->title_name[0])
|
|
strncpy(arr[i].name, a->title_name, sizeof(arr[i].name) - 1);
|
|
if (a->content_version[0])
|
|
strncpy(arr[i].installed_version, a->content_version,
|
|
sizeof(arr[i].installed_version) - 1);
|
|
if (a->content_id[0] && !arr[i].content_id[0])
|
|
strncpy(arr[i].content_id, a->content_id,
|
|
sizeof(arr[i].content_id) - 1);
|
|
if (a->version_file_uri[0])
|
|
strncpy(arr[i].version_file_uri, a->version_file_uri,
|
|
sizeof(arr[i].version_file_uri) - 1);
|
|
}
|
|
|
|
patchdl_appdb_free(info);
|
|
}
|
|
|
|
/* See patchdl_scan_lock — both vnode-swap entry points return -1 / NULL once
|
|
this is set so a late rescan call can't race the running MHD threads. */
|
|
static _Atomic int g_scan_locked = 0;
|
|
|
|
void
|
|
patchdl_scan_lock(void) {
|
|
atomic_store(&g_scan_locked, 1);
|
|
}
|
|
|
|
int
|
|
patchdl_scan(patchdl_title_t **titles_out, size_t *count_out) {
|
|
patchdl_title_t *arr;
|
|
size_t cnt = 0;
|
|
pid_t pid = getpid();
|
|
intptr_t saved_root = 0, root_vnode;
|
|
int using_vswap = 0;
|
|
|
|
struct statfs *mounts = NULL;
|
|
int nmounts;
|
|
|
|
if (atomic_load(&g_scan_locked)) return -1;
|
|
|
|
arr = calloc(MAX_TITLES, sizeof(*arr));
|
|
if (!arr) return -1;
|
|
|
|
/* Elevate so privileged paths (the app.db under /system_data) are
|
|
readable; same authid ftpsrv uses. No-op without kernel R/W. */
|
|
kernel_set_ucred_authid(pid, 0x4801000000000013L);
|
|
|
|
/* Global mount table for shadowmount detection. getmntinfo's buffer is
|
|
libc-managed — must NOT be freed. */
|
|
nmounts = getmntinfo(&mounts, MNT_NOWAIT);
|
|
if (nmounts < 0) { nmounts = 0; mounts = NULL; }
|
|
|
|
root_vnode = kernel_get_root_vnode();
|
|
if (root_vnode) {
|
|
saved_root = kernel_get_proc_rootdir(pid);
|
|
/* Only swap if we captured the current root, so we can always restore
|
|
it. Leaving the process rooted at the system root would make later
|
|
absolute-path writes land in the wrong place. */
|
|
if (saved_root) {
|
|
kernel_set_proc_rootdir(pid, root_vnode);
|
|
using_vswap = 1;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; SCAN_DIRS[i]; i++)
|
|
scan_base(SCAN_DIRS[i], arr, &cnt, MAX_TITLES, mounts, nmounts);
|
|
|
|
merge_appdb(arr, cnt);
|
|
|
|
if (using_vswap)
|
|
kernel_set_proc_rootdir(pid, saved_root);
|
|
|
|
*titles_out = arr;
|
|
*count_out = cnt;
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
patchdl_scan_free(patchdl_title_t *titles, size_t count) {
|
|
(void)count;
|
|
free(titles);
|
|
}
|
|
|
|
/* ---------- diagnostic dump --------------------------------------------- */
|
|
|
|
static char *
|
|
dbg_append(char *buf, size_t *len, size_t *cap, const char *s) {
|
|
size_t sl = strlen(s);
|
|
if (*len + sl + 1 > *cap) {
|
|
size_t nc = (*cap + sl + 1) * 2;
|
|
char *p = realloc(buf, nc);
|
|
if (!p) return buf;
|
|
buf = p;
|
|
*cap = nc;
|
|
}
|
|
memcpy(buf + *len, s, sl + 1);
|
|
*len += sl;
|
|
return buf;
|
|
}
|
|
|
|
char *
|
|
patchdl_scan_debug_json(void) {
|
|
char *buf = NULL;
|
|
size_t len = 0, cap = 0;
|
|
char tmp[2048];
|
|
pid_t pid = getpid();
|
|
intptr_t saved_root = 0, root_vnode;
|
|
|
|
if (atomic_load(&g_scan_locked)) return NULL;
|
|
int using_vswap = 0;
|
|
struct statfs *mounts = NULL;
|
|
int nmounts;
|
|
|
|
/* getmntinfo returns the global mount table — no root-vnode swap needed.
|
|
Must NOT free the returned buffer (libc-managed). */
|
|
nmounts = getmntinfo(&mounts, MNT_NOWAIT);
|
|
if (nmounts < 0) { nmounts = 0; mounts = NULL; }
|
|
|
|
buf = dbg_append(buf, &len, &cap, "{\"mounts\":[");
|
|
for (int i = 0; i < nmounts; i++) {
|
|
snprintf(tmp, sizeof(tmp),
|
|
"%s{\"from\":\"%s\",\"on\":\"%s\",\"type\":\"%s\",\"flags\":\"0x%lx\"}",
|
|
i ? "," : "",
|
|
mounts[i].f_mntfromname, mounts[i].f_mntonname,
|
|
mounts[i].f_fstypename, (unsigned long)mounts[i].f_flags);
|
|
buf = dbg_append(buf, &len, &cap, tmp);
|
|
}
|
|
|
|
buf = dbg_append(buf, &len, &cap, "],\"bases\":[");
|
|
|
|
root_vnode = kernel_get_root_vnode();
|
|
if (root_vnode) {
|
|
saved_root = kernel_get_proc_rootdir(pid);
|
|
/* Only swap if we can restore it afterwards (see patchdl_scan). */
|
|
if (saved_root) {
|
|
kernel_set_proc_rootdir(pid, root_vnode);
|
|
using_vswap = 1;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; SCAN_DIRS[i]; i++) {
|
|
DIR *d = opendir(SCAN_DIRS[i]);
|
|
snprintf(tmp, sizeof(tmp), "%s{\"base\":\"%s\",\"exists\":%s,\"entries\":[",
|
|
i ? "," : "", SCAN_DIRS[i], d ? "true" : "false");
|
|
buf = dbg_append(buf, &len, &cap, tmp);
|
|
if (d) {
|
|
struct dirent *de;
|
|
int first = 1;
|
|
while ((de = readdir(d))) {
|
|
char dir[PATH_MAX];
|
|
if (de->d_name[0] == '.') continue;
|
|
snprintf(dir, sizeof(dir), "%s/%s", SCAN_DIRS[i], de->d_name);
|
|
snprintf(tmp, sizeof(tmp),
|
|
"%s{\"name\":\"%s\",\"pj\":%s,\"pkg\":%s,\"lnk\":%s}",
|
|
first ? "" : ",", de->d_name,
|
|
path_exists(dir, "sce_sys/param.json") ? "true" : "false",
|
|
path_exists(dir, "app.pkg") ? "true" : "false",
|
|
(path_exists(dir, "mount.lnk") ||
|
|
path_exists(dir, "mount_img.lnk")) ? "true" : "false");
|
|
buf = dbg_append(buf, &len, &cap, tmp);
|
|
first = 0;
|
|
}
|
|
closedir(d);
|
|
}
|
|
buf = dbg_append(buf, &len, &cap, "]}");
|
|
}
|
|
|
|
if (using_vswap)
|
|
kernel_set_proc_rootdir(pid, saved_root);
|
|
|
|
buf = dbg_append(buf, &len, &cap, "]}");
|
|
return buf;
|
|
}
|