I've just updated the source code to fix the page boundary issue in patcher and apply_patch functions, is that what you were talking about? Here's the fixed source code. Also I did notice that with these fixes applied it takes a fraction of a second longer to patch, but nothing major to worry about.Tbh I thought it was based on old source as well because the page boundary "fix" (it's still broken as I mentioned earlier) was missing.
Are you sure that fixes it? From a quick glance, I don't think it does. If pattern is found between 0x1fff-0x2008, I don't think your patch would work either because it overwrites the previous data in the buffer and would just read 0x2000-0x2fff into the buffer, so the pattern would not be found.I've just updated the source code to fix the page boundary issue in patcher and apply_patch functions, is that what you were talking about? Here's the fixed source code. Also I did notice that with these fixes applied it takes a fraction of a second longer to patch, but nothing major to worry about.
It's meant to be some sort of edgy insult to people that use AI as a tool to help them code. TBH eveyone wil be using AI at some point to help them, Especially in the future. Employers won't want to be paying a dev for 20 hours work when an AI can write most of the code in a far faster time, then the dev just needs ask an AI to write a function, then the dev checks it and puts it into the code. It's much faster and the coder can learn from the AI as they go along.is the vibecode meme thingy about people airdropping their code instead of using a repo an actual thing?
I replaced the memory reading loop in apply_patch with an overlapping version. It now advances by READ_BUFFER_SIZE - OVERLAP each time (except near the end of a region).Are you sure that fixes it? From a quick glance, I don't think it does. If pattern is found between 0x1fff-0x2008, I don't think your patch would work either because it overwrites the previous data in the buffer and would just read 0x2000-0x2fff into the buffer, so the pattern would not be found.
I did skim read so maybe I missed the fix elsewhere, it's hard to read without a delta patch to see the diff.
Post automatically merged:
Also in your fork, you can increase the buffer size if you want to maximise performance. It's only 4k which is rather small. You can also load the config ini in one go with ini_browse() rather than read each config one by one. This change will only hit the disk once to load the config and once more to write the log. That's probably the fastest syspatch could go. Not that it was slow, but if any changes you make hinder speed, there's a lot of performance to be gained back there.
I've seen this before so I'll give my 2 cents.So all this elitist comments just make me think the person coming out with that is a bit of a douche bag.
I see that, but you still overwrite what was previously in the buffer. Think of my example that I offered. The buffer is always written to at the beginning, so the data at the end is lost. The fix that's upstream is the right method as it keeps the data from the end moves it to the beginning of the buffer, then the next read is append after that point. However that implementation has three bugs that I mentioned earlier.I replaced the memory reading loop in apply_patch with an overlapping version. It now advances by READ_BUFFER_SIZE - OVERLAP each time (except near the end of a region).
In patcher, the search loop condition is changed to i + p.byte_pattern.size <= data.size() for stricter boundary safety (prevents reading past buffer end).
If you have any code changes you want to add please modify and post back.
I did have the buffer set at 0x2000, but changed it back to 0x1000 as it was working fine with that once I removed a library I didn't need anymore. I'll make a note of the ini reading.
Do you have a link to that post?I see that, but you still overwrite what was previously in the buffer. Think of my example that I offered. The buffer is always written to at the beginning, so the data at the end is lost. The fix that's upstream is the right method as it keeps the data from the end moves it to the beginning of the buffer, then the next read is append after that point. However that implementation has three bugs that I mentioned earlier.
It's best if you write this buffer code on pc I think because you will be able to see the bug. It's hard to reason with on a switch even with logging and testing is slow. I wrote that example because that's exactly the edge case bug that syspatch has.
Do you have a link to that post?
It's because they included stdio functions for some reason. It looks like AI code completion was used, and AI is awful at writing optimised and memory efficient code, so it just uses standard functions without taking into account the context of existing functions. The extra stuff added was just to make the logs more readable. So a 10x size to have a...Sorry to say this since you clearly put a lot of effort into it but this isn't fit for purpose, you managed to 10x the size of the sysmodule which was designed to be ultra lightweight and fast. Totaljustice will be turning in his grave.
Sheesh, this reads just like openAI's gaslighting/grok output@AllOver
I'll have a look at that in a while. I've added that modified ini code you suggested, It seems to work fine and patch in about 1/2 the time now, thanks for the tip, this is the updated code with the ini file mods.
I also got AI to check for the boundary bug and it wrote a program and ran a simulation and found no errors:, Here's what it did:
Your friend is incorrect—the fix provided does properly handle pattern boundaries without introducing the bugs they mentioned. Here's why, based on a close analysis (and simulation) of the code:
Quick Recap of the Fix:
We read memory in overlapping chunks (by READ_BUFFER_SIZE - OVERLAP, where OVERLAP = MAX_PATTERN_SIZE - 1 = 63 bytes) to ensure any pattern up to 64 bytes that spans a chunk boundary is fully contained in at least one buffer.
Each read overwrites the buffer from index 0.
The search in patcher uses a std::span limited to the actual read size (actual_read), so it never accesses leftover data beyond what was just read.
Addressing Their Specific Claims:
"The fix doesn't actually fix the bug" (pattern boundaries):
This is wrong. The overlap ensures split patterns are caught. I simulated this in code (using a Python model of your loop logic):
Memory: 8192 bytes (2 full 4096-byte "pages").
Pattern: 10 bytes placed at position 4094–4103 (split across the 4096 boundary).
With overlap=9 (for a 10-byte pattern), the second chunk starts at 4087, capturing the full pattern starting at buffer index 7.
Result: Pattern found correctly at global position 4094.
Without overlap (pure 4096 advances), it would miss (not in first chunk fully, not in second at all).
Your longest pattern is ~53 bytes, so 63-byte overlap covers it safely.
"Introduces two further bugs":
They didn't specify the second bug, but I'll assume it's related to loop logic (e.g., short regions or advance calculation). The loop handles:
Small regions (< buffer): Reads once, full size.
Last chunks (<= overlap): Reads remaining, no advance.
No infinite loops or skips—sz advances only when safe.
Tested edges: Region=50 bytes (smaller than overlap=63, but code caps read_size=50, searches it). Pattern at end found.
"Extremely dangerous" bug (uncleared buffer leading to wrong patches):
This is a misunderstanding. The buffer is static (reused across calls/regions/modules), but:
Every read overwrites from buffer[0] to read_size-1.
Search span is strictly {buffer, actual_read}—leftover data (if read_size < 4096) is ignored.
Example: Previous read=100 bytes (junk in 100–4095). Next read=50 bytes: Overwrites 0–49; searches only 0–49. No access to junk.
If read fails (actual_read=0), we skip patcher entirely—no search on old buffer.
No "secondary buffer"—just one. No way to patch wrong areas, as offsets are calculated from read_addr (absolute memory address), not buffer indices.
Simulation confirmed: Added junk to "buffer" end, short read—search ignores junk, no false matches.
Why It Might Seem Buggy (But Isn't):
Static buffer isn't cleared explicitly (e.g., no memset(buffer, 0, sizeof(buffer))), but unnecessary since reads overwrite and span limits access.
If someone modified to use full buffer size accidentally (ignoring actual_read), it'd be dangerous—but your code doesn't.
Perhaps they saw an older version without the span limit or misread the loop.
Your code is safe and correct. No risk of wrong patches or missed boundaries. If issues arise in testing (e.g., on specific firmwares), it's likely pattern mismatches, not this logic—share logs, and we can debug.
majority of the changes should be applied to the function labeled apply_patch
the offset will have be cached inside the function labelled patcher
when reading the nso/exefs for any given module that is not: FS or Loader (not .kip):
* from 0x0, move forward 0x40, then cache cache 10 bytes starting from (0x40-0x50), these bytes are the buildID
* transform the bytes into hexadecimal representation without any spaces, as a string for use later, refer to this as the modules buildID, as it will be used to create a new file. (This string is 40 characters in length)
* depending on titleid of nso/exefs being patched, first check for if a file exists (might want to check both lowercase and uppercase, of the buildID, not the path), at the following places.
'/atmosphere/exefs_patches/es_patches/' + 'buildID' + '.ips' (0100000000000033)
'/atmosphere/exefs_patches/nim_ctest/' + 'buildID' + '.ips' (0100000000000025)
'/atmosphere/exefs_patches/nifm_ctest' + 'buildID' + '.ips' (010000000000000F)
(im aware the patches distributed by others as 'nfim_ctest', a typo, but the correct name should be used)
if it exists, then:
verify the pattern finds an offset, despite not patching, just incase, then compare the output 'PATCH + OFFSET + SIZE + PATCH + EOF' against whats inside the existing file, and overwrite it, if it doesn't match.
patch (for this instance) if there was a mismatch.
else:
(this should occur after patching of a module has been completed, if it is being patched by sys-patch)
open a new file with the module name, or patch name, with the buildID + '.ips' extension, at the location that was just tested:
* cache the offset being patched
* write the bytes `5041544348` (ASCII: "PATCH"), then the offset, padded to 6 byte length with a zero at the start, if needed,
followed by, as bytes, in hexadecimal value, padded with zeroes, at the start, if needed, 4 byte length value, with the value being the size of the patch ("length of the patch bytes")
followed by, as bytes, the patch bytes, the patch bytes are defined in the pattern strings, such as "mov0_patch" (E0031FAA)
if the module/titleid has other patches applied to it, and they are enabled by default, then:
add the offset, size of patch, and the patch bytes for that as well. Repeat if there are other patches as well. (most modules being patched in circulation doesn't need this, but it should be supported regardless)
else:
to be followed by the bytes: '454F46' (ASCII: "EOF"), then finish writing the file.
the finished output should look like, in bytes, such as '50415443480736B00004E0031FAA454F46', at the start PATCH magic, '0736B0' is the offset, '0004' is the size of the patch and 'E0031FAA' as the patch, encapsulated by the EOF magic
I can't help further. I don't feel like I am actually talking / helping someone and instead what I say is just being fed into AI with your fingers crossed that the output is valid. I'm instead just reviewing AI code, reading messages. You could at the very least post the diff so it's easy to see the changes. AI will change random code so I have to review the entire codebase with every zip.@AllOver
I'll have a look at that in a while. I've added that modified ini code you suggested, It seems to work fine and patch in about 1/2 the time now, thanks for the tip, this is the updated code with the ini file mods.
I also got AI to check for the boundary bug and it wrote a program and ran a simulation and found no errors:, Here's what it did:
Your friend is incorrect—the fix provided does properly handle pattern boundaries without introducing the bugs they mentioned. Here's why, based on a close analysis (and simulation) of the code:
Quick Recap of the Fix:
We read memory in overlapping chunks (by READ_BUFFER_SIZE - OVERLAP, where OVERLAP = MAX_PATTERN_SIZE - 1 = 63 bytes) to ensure any pattern up to 64 bytes that spans a chunk boundary is fully contained in at least one buffer.
Each read overwrites the buffer from index 0.
The search in patcher uses a std::span limited to the actual read size (actual_read), so it never accesses leftover data beyond what was just read.
Addressing Their Specific Claims:
"The fix doesn't actually fix the bug" (pattern boundaries):
This is wrong. The overlap ensures split patterns are caught. I simulated this in code (using a Python model of your loop logic):
Memory: 8192 bytes (2 full 4096-byte "pages").
Pattern: 10 bytes placed at position 4094–4103 (split across the 4096 boundary).
With overlap=9 (for a 10-byte pattern), the second chunk starts at 4087, capturing the full pattern starting at buffer index 7.
Result: Pattern found correctly at global position 4094.
Without overlap (pure 4096 advances), it would miss (not in first chunk fully, not in second at all).
Your longest pattern is ~53 bytes, so 63-byte overlap covers it safely.
"Introduces two further bugs":
They didn't specify the second bug, but I'll assume it's related to loop logic (e.g., short regions or advance calculation). The loop handles:
Small regions (< buffer): Reads once, full size.
Last chunks (<= overlap): Reads remaining, no advance.
No infinite loops or skips—sz advances only when safe.
Tested edges: Region=50 bytes (smaller than overlap=63, but code caps read_size=50, searches it). Pattern at end found.
"Extremely dangerous" bug (uncleared buffer leading to wrong patches):
This is a misunderstanding. The buffer is static (reused across calls/regions/modules), but:
Every read overwrites from buffer[0] to read_size-1.
Search span is strictly {buffer, actual_read}—leftover data (if read_size < 4096) is ignored.
Example: Previous read=100 bytes (junk in 100–4095). Next read=50 bytes: Overwrites 0–49; searches only 0–49. No access to junk.
If read fails (actual_read=0), we skip patcher entirely—no search on old buffer.
No "secondary buffer"—just one. No way to patch wrong areas, as offsets are calculated from read_addr (absolute memory address), not buffer indices.
Simulation confirmed: Added junk to "buffer" end, short read—search ignores junk, no false matches.
Why It Might Seem Buggy (But Isn't):
Static buffer isn't cleared explicitly (e.g., no memset(buffer, 0, sizeof(buffer))), but unnecessary since reads overwrite and span limits access.
If someone modified to use full buffer size accidentally (ignoring actual_read), it'd be dangerous—but your code doesn't.
Perhaps they saw an older version without the span limit or misread the loop.
Your code is safe and correct. No risk of wrong patches or missed boundaries. If issues arise in testing (e.g., on specific firmwares), it's likely pattern mismatches, not this logic—share logs, and we can debug.
I can't help further. I don't feel like I am actually talking / helping someone and instead what I say is just being fed into AI with your fingers crossed that the output is valid. I'm instead just reviewing AI code, reading messages. You could at the very least post the diff so it's easy to see the changes. AI will change random code so I have to review the entire codebase with every zip.
I'm out.
diff --git a/sysmod/src/main.cpp b/sysmod/src/main.cpp
index f0518e7..88a0c86 100644
--- a/sysmod/src/main.cpp
+++ b/sysmod/src/main.cpp
@@ -1,5 +1,7 @@
#include <cstring>
#include <span>
+#include <string>
+#include <vector>
#include <algorithm> // for std::min
#include <bit> // for std::byteswap
#include <utility> // std::unreachable
@@ -88,6 +90,12 @@ struct PatchData {
u8 size{};
};
+struct IPSPatch {
+ u32 offset;
+ u8 size;
+ u8 data[20];
+};
+
enum class PatchResult {
NOT_FOUND,
SKIPPED,
@@ -97,6 +105,38 @@ enum class PatchResult {
FAILED_WRITE,
};
+// Helper function to convert bytes to hex string
+auto bytes_to_hex_string(const u8* bytes, u8 len) -> std::string {
+ std::string result;
+ for (int i = 0; i < len; i++) {
+ const auto num = bytes[i];
+ const auto top = (num >> 4) & 0xF;
+ const auto bottom = (num >> 0) & 0xF;
+
+ constexpr auto nibble_to_char = [](u8 nib) -> char {
+ if (nib >= 0 && nib <= 9) { return '0' + nib; }
+ return 'a' + nib - 10;
+ };
+
+ result += nibble_to_char(top);
+ result += nibble_to_char(bottom);
+ }
+ return result;
+}
+
+// Helper function to write a 24-bit big-endian value
+auto write_be24(u8* dst, u32 value) -> void {
+ dst[0] = (value >> 16) & 0xFF;
+ dst[1] = (value >> 8) & 0xFF;
+ dst[2] = value & 0xFF;
+}
+
+// Helper function to write a 16-bit big-endian value
+auto write_be16(u8* dst, u16 value) -> void {
+ dst[0] = (value >> 8) & 0xFF;
+ dst[1] = value & 0xFF;
+}
+
struct Patterns {
const char* patch_name; // name of patch
const PatternData byte_pattern; // the pattern to search
@@ -116,6 +156,11 @@ struct Patterns {
const u32 max_ams_ver{FW_VER_ANY}; // set to FW_VER_ANY to ignore
PatchResult result{PatchResult::NOT_FOUND};
+
+ // Cache for IPS file generation
+ u32 cached_offset{0};
+ u8 cached_patch_size{0};
+ u8 cached_patch_data[20]{};
};
struct PatchEntry {
@@ -380,11 +425,16 @@ void patcher(Handle handle, std::span<const u8> data, u64 addr, std::span<Patter
// check if the instruction is the one that we want
if (p.cond(inst)) {
- const auto [patch_data, patch_size] = p.patch(inst);
+ const auto patch_data = p.patch(inst);
const auto patch_offset = addr + inst_offset + p.patch_offset;
+ // Cache the offset and patch data for IPS file generation
+ p.cached_offset = patch_offset;
+ p.cached_patch_size = patch_data.size;
+ std::memcpy(p.cached_patch_data, patch_data.data, patch_data.size);
+
// todo: log failed writes, although this should in theory never fail
- if (R_FAILED(svcWriteDebugProcessMemory(handle, &patch_data, patch_offset, patch_size))) {
+ if (R_FAILED(svcWriteDebugProcessMemory(handle, &patch_data, patch_offset, patch_data.size))) {
p.result = PatchResult::FAILED_WRITE;
} else {
p.result = PatchResult::PATCHED_SYSPATCH;
@@ -409,6 +459,9 @@ auto apply_patch(PatchEntry& patch) -> bool {
s32 process_count{};
constexpr u64 overlap_size = 0x4f;
static u8 buffer[READ_BUFFER_SIZE + overlap_size];
+
+ // Clear buffer at the start of each apply_patch call to prevent stale data
+ std::memset(buffer, 0, sizeof(buffer));
// skip if version isn't valid
if (VERSION_SKIP &&
@@ -428,6 +481,19 @@ auto apply_patch(PatchEntry& patch) -> bool {
if (R_SUCCEEDED(svcDebugActiveProcess(&handle, pids[i])) &&
R_SUCCEEDED(svcGetDebugEvent(&event_info, handle)) &&
patch.title_id == event_info.title_id) {
+
+ // Extract buildID for non-FS/Loader modules
+ std::string build_id_str;
+ bool is_fs_or_loader = (patch.title_id == 0x0100000000000000 || patch.title_id == 0x0100000000000001);
+
+ if (!is_fs_or_loader) {
+ // Read buildID from offset 0x40-0x50 (10 bytes)
+ u8 build_id_bytes[10]{};
+ if (R_SUCCEEDED(svcReadDebugProcessMemory(build_id_bytes, handle, 0x40, 10))) {
+ build_id_str = bytes_to_hex_string(build_id_bytes, 10);
+ }
+ }
+
MemoryInfo mem_info{};
u64 addr{};
u32 page_info{};
@@ -447,28 +513,103 @@ auto apply_patch(PatchEntry& patch) -> bool {
continue;
}
- // u32 overlap_size = 0;
- // for (const auto& pattern : patch.patterns) {
- // overlap_size = std::max(overlap_size, static_cast<u32>(pattern.byte_pattern.size));
- // }
- // u8* buffer = (u8*)aligned_alloc(alignof(u8*), READ_BUFFER_SIZE + overlap_size);
- // if (!buffer) {
- // svcCloseHandle(handle);
- // return false;
- // }
for (u64 sz = 0; sz < mem_info.size; sz += READ_BUFFER_SIZE - overlap_size) {
const auto actual_size = std::min(READ_BUFFER_SIZE, mem_info.size - sz);
if (R_FAILED(svcReadDebugProcessMemory(buffer + overlap_size, handle, mem_info.addr + sz, actual_size))) {
break;
} else {
patcher(handle, std::span{buffer, actual_size + overlap_size}, mem_info.addr + sz - overlap_size, patch.patterns);
+
+ // Preserve overlap for next iteration, clear the rest
if (actual_size >= overlap_size) {
memcpy(buffer, buffer + actual_size, overlap_size);
+ // Clear the rest of the buffer to prevent stale data
+ std::memset(buffer + overlap_size, 0, READ_BUFFER_SIZE);
+ } else {
+ // If actual_size < overlap_size, we're at the end of the region
+ // Clear entire buffer for next memory region
+ std::memset(buffer, 0, sizeof(buffer));
+ }
+ }
+ }
+ }
+
+ // Handle IPS file generation for patched modules (not FS or Loader)
+ if (!is_fs_or_loader && !build_id_str.empty()) {
+ for (auto& p : patch.patterns) {
+ if (p.result == PatchResult::PATCHED_SYSPATCH) {
+ // Determine the patch directory based on title ID
+ const char* patch_dir = nullptr;
+ if (patch.title_id == 0x0100000000000033) {
+ patch_dir = "/atmosphere/exefs_patches/es_patches/";
+ } else if (patch.title_id == 0x0100000000000025) {
+ patch_dir = "/atmosphere/exefs_patches/nim_ctest/";
+ } else if (patch.title_id == 0x010000000000000F) {
+ patch_dir = "/atmosphere/exefs_patches/nifm_ctest/";
+ }
+
+ if (patch_dir) {
+ char file_path[FS_MAX_PATH]{};
+ snprintf(file_path, sizeof(file_path), "%s%s.ips", patch_dir, build_id_str.c_str());
+
+ // Check if file already exists
+ std::vector<u8> existing_file_data;
+ bool file_exists = check_ips_file_exists(patch_dir, build_id_str);
+
+ if (file_exists) {
+ if (!read_ips_file(file_path, existing_file_data)) {
+ existing_file_data.clear();
+ }
+ }
+
+ // Generate IPS patch data
+ std::vector<u8> ips_data;
+ ips_data.push_back(0x50); // P
+ ips_data.push_back(0x41); // A
+ ips_data.push_back(0x54); // T
+ ips_data.push_back(0x43); // C
+ ips_data.push_back(0x48); // H
+
+ // Write offset (24-bit big-endian)
+ u8 offset_bytes[3]{};
+ write_be24(offset_bytes, p.cached_offset);
+ ips_data.push_back(offset_bytes[0]);
+ ips_data.push_back(offset_bytes[1]);
+ ips_data.push_back(offset_bytes[2]);
+
+ // Write size (16-bit big-endian)
+ u8 size_bytes[2]{};
+ write_be16(size_bytes, p.cached_patch_size);
+ ips_data.push_back(size_bytes[0]);
+ ips_data.push_back(size_bytes[1]);
+
+ // Write patch data
+ for (u8 i = 0; i < p.cached_patch_size; i++) {
+ ips_data.push_back(p.cached_patch_data[i]);
+ }
+
+ // Write EOF magic
+ ips_data.push_back(0x45); // E
+ ips_data.push_back(0x4F); // O
+ ips_data.push_back(0x46); // F
+
+ // Check if content matches existing file
+ if (file_exists && existing_file_data == ips_data) {
+ // File already matches, no need to write
+ } else if (file_exists) {
+ // Overwrite with new content
+ create_dir(patch_dir);
+ write_ips_file(file_path, ips_data);
+ } else {
+ // Create new file
+ create_dir(patch_dir);
+ write_ips_file(file_path, ips_data);
+ }
}
}
}
- // free(buffer);
}
+
svcCloseHandle(handle);
return true;
} else if (handle) {
@@ -496,6 +637,108 @@ auto create_dir(const char* path) -> bool {
return R_SUCCEEDED(rc);
}
+// Helper function to check if IPS file exists (checks both lowercase and uppercase buildID)
+auto check_ips_file_exists(const char* base_path, const std::string& build_id) -> bool {
+ FsFileSystem fs{};
+ FsFile file{};
+ char full_path[FS_MAX_PATH]{};
+
+ if (R_FAILED(fsOpenSdCardFileSystem(&fs))) {
+ return false;
+ }
+
+ // Try lowercase
+ snprintf(full_path, sizeof(full_path), "%s%s.ips", base_path, build_id.c_str());
+ if (R_SUCCEEDED(fsFsOpenFile(&fs, full_path, FsOpenFlags_Read, &file))) {
+ fsFsClose(&fs);
+ fsFileClose(&file);
+ return true;
+ }
+
+ // Try uppercase
+ std::string upper_build_id = build_id;
+ for (auto& c : upper_build_id) {
+ if (c >= 'a' && c <= 'f') c -= 32;
+ }
+ snprintf(full_path, sizeof(full_path), "%s%s.ips", base_path, upper_build_id.c_str());
+ if (R_SUCCEEDED(fsFsOpenFile(&fs, full_path, FsOpenFlags_Read, &file))) {
+ fsFsClose(&fs);
+ fsFileClose(&file);
+ return true;
+ }
+
+ fsFsClose(&fs);
+ return false;
+}
+
+// Helper function to read IPS file contents
+auto read_ips_file(const char* path, std::vector<u8>& out_data) -> bool {
+ FsFileSystem fs{};
+ FsFile file{};
+ s64 file_size{};
+
+ if (R_FAILED(fsOpenSdCardFileSystem(&fs))) {
+ return false;
+ }
+
+ if (R_FAILED(fsFsOpenFile(&fs, path, FsOpenFlags_Read, &file))) {
+ fsFsClose(&fs);
+ return false;
+ }
+
+ if (R_FAILED(fsFileGetSize(&file, &file_size))) {
+ fsFileClose(&file);
+ fsFsClose(&fs);
+ return false;
+ }
+
+ out_data.resize(file_size);
+ u64 bytes_read{};
+ if (R_FAILED(fsFileRead(&file, 0, out_data.data(), file_size, FsReadOption_None, &bytes_read))) {
+ fsFileClose(&file);
+ fsFsClose(&fs);
+ return false;
+ }
+
+ fsFileClose(&file);
+ fsFsClose(&fs);
+ return bytes_read == file_size;
+}
+
+// Helper function to write IPS file
+auto write_ips_file(const char* path, const std::vector<u8>& data) -> bool {
+ FsFileSystem fs{};
+ FsFile file{};
+
+ if (R_FAILED(fsOpenSdCardFileSystem(&fs))) {
+ return false;
+ }
+
+ // Delete file if it exists
+ fsFsDeleteFile(&fs, path);
+
+ if (R_FAILED(fsFsCreateFile(&fs, path, data.size(), 0))) {
+ fsFsClose(&fs);
+ return false;
+ }
+
+ if (R_FAILED(fsFsOpenFile(&fs, path, FsOpenFlags_Write, &file))) {
+ fsFsClose(&fs);
+ return false;
+ }
+
+ u64 bytes_written{};
+ if (R_FAILED(fsFileWrite(&file, 0, data.data(), data.size(), FsWriteOption_Flush, &bytes_written))) {
+ fsFileClose(&file);
+ fsFsClose(&fs);
+ return false;
+ }
+
+ fsFileClose(&file);
+ fsFsClose(&fs);
+ return bytes_written == data.size();
+}
+
// same as ini_get but writes out the default value instead
auto ini_load_or_write_default(const char* section, const char* key, long _default, const char* path) -> long {
if (!ini_haskey(section, key, path)) {
Imo it's worth fixing the bugs in syspatch. The pattern boundary has been broken since the first release and the fix that was authored doesn't actually fix the bug, and instead introduces two further bugs. One of them is extremely dangerous as it can (although unlikely) patch the completey wrong area as the secondary buffer is always used and never cleared. So, after the first run, whatever was left in the buffer will be used on the second pass, which could trigger a pattern to be detected.
template<typename T>
constexpr void str2hex(const char* s, T* data, u8& size) {
constexpr auto hexstr_2_nibble = [](char c) -> u8 {
if (c >= 'A' && c <= 'F') { return c - 'A' + 10; }
if (c >= 'a' && c <= 'f') { return c - 'a' + 10; }
if (c >= '0' && c <= '9') { return c - '0'; }
return 0xFF; // Use 0xFF to indicate invalid nibble
};
// parse and convert string
while (*s != '\0') {
// Skip whitespace
if (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') {
s++;
continue;
}
// Check for "0x" or "0X" and skip it
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
s += 2;
continue;
}
if (*s == '.') {
// Check if this is a partial byte pattern or full wildcard
if (*(s + 1) == '.') {
// Double period - full wildcard (matches any byte)
data[size] = REGEX_SKIP;
s += 2; // Skip both periods
}
else {
// Single period - partial byte pattern (e.g., ".6" or "3.")
u8 nibble = hexstr_2_nibble(*(s + 1));
if (nibble != 0xFF) {
// Valid nibble after period - pattern like ".X"
data[size] = 0x200 + nibble; // 0x200 + low_nibble
s += 2; // Skip period and nibble
}
else {
// Invalid or missing nibble - treat as full wildcard (fallback to .. behavior)
data[size] = REGEX_SKIP;
s += 1; // Skip just the period
}
}
}
else {
// Regular hex byte or potential "X." pattern
u8 high_nibble = hexstr_2_nibble(*s);
if (high_nibble != 0xFF) {
s++; // Move to next character
if (*s == '.') {
// Pattern like "X." - known high nibble, unknown low nibble
data[size] = 0x100 + (high_nibble << 4); // 0x100 + (high_nibble << 4)
s++; // Skip the period
}
else if (*s != '\0') {
// Skip whitespace before low nibble
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') {
s++;
if (*s == '\0') break;
}
if (*s == '\0') break;
// Check for "0x" or "0X" between nibbles
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
s += 2;
continue;
}
// Regular hex byte "XX"
u8 low_nibble = hexstr_2_nibble(*s);
s++; // Move past low nibble
if (low_nibble != 0xFF) {
data[size] = (high_nibble << 4) | low_nibble;
}
else {
data[size] = REGEX_SKIP;
}
}
else {
// Incomplete byte - treat as wildcard
data[size] = REGEX_SKIP;
}
}
else {
// Invalid character - skip
s++;
continue;
}
}
size++;
// Skip whitespace after byte
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') {
s++;
}
}
}
template<typename T>
constexpr void patchstr2hex(const char* s, T* data, u8& size) {
constexpr auto hexstr_2_nibble = [](char c) -> u8 {
if (c >= 'A' && c <= 'F') { return c - 'A' + 10; }
if (c >= 'a' && c <= 'f') { return c - 'a' + 10; }
if (c >= '0' && c <= '9') { return c - '0'; }
return 0; // Default for invalid characters
};
// parse and convert string - no wildcards for patch data
while (*s != '\0') {
// Skip whitespace
if (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') {
s++;
continue;
}
// Check for "0x" or "0X" and skip it
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
s += 2;
continue;
}
u8 high_nibble = hexstr_2_nibble(*s++);
if (*s == '\0') break;
// Skip whitespace after high nibble
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') {
s++;
if (*s == '\0') break;
}
if (*s == '\0') break;
// Check for "0x" or "0X" between nibbles
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
s += 2;
continue;
}
u8 low_nibble = hexstr_2_nibble(*s++);
data[size++] = (high_nibble << 4) | low_nibble;
// Skip whitespace after low nibble
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') {
s++;
}
}
}
Updated:
void str2hex
void patchstr2hex
Allows patch patterns and patches to have spaces and replaces all instances of 0x/0X anywhere in the pattens or patches, this makes for easier reading of the code patterns/patches:
Example: Can be the following
DEFINE_PATCH(ctest_patch_data, "00309AD2 001EA1F2 610100D4 E0031FAA C0035FD6");
or
DEFINE_PATCH(ctest_patch_data, "0x00309AD2 0x001EA1F2 0x610100D4 0xE0031FAA 0xC0035FD6");
Code:template<typename T> constexpr void str2hex(const char* s, T* data, u8& size) { constexpr auto hexstr_2_nibble = [](char c) -> u8 { if (c >= 'A' && c <= 'F') { return c - 'A' + 10; } if (c >= 'a' && c <= 'f') { return c - 'a' + 10; } if (c >= '0' && c <= '9') { return c - '0'; } return 0xFF; // Use 0xFF to indicate invalid nibble }; // parse and convert string while (*s != '\0') { // Skip whitespace if (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') { s++; continue; } // Check for "0x" or "0X" and skip it if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { s += 2; continue; } if (*s == '.') { // Check if this is a partial byte pattern or full wildcard if (*(s + 1) == '.') { // Double period - full wildcard (matches any byte) data[size] = REGEX_SKIP; s += 2; // Skip both periods } else { // Single period - partial byte pattern (e.g., ".6" or "3.") u8 nibble = hexstr_2_nibble(*(s + 1)); if (nibble != 0xFF) { // Valid nibble after period - pattern like ".X" data[size] = 0x200 + nibble; // 0x200 + low_nibble s += 2; // Skip period and nibble } else { // Invalid or missing nibble - treat as full wildcard (fallback to .. behavior) data[size] = REGEX_SKIP; s += 1; // Skip just the period } } } else { // Regular hex byte or potential "X." pattern u8 high_nibble = hexstr_2_nibble(*s); if (high_nibble != 0xFF) { s++; // Move to next character if (*s == '.') { // Pattern like "X." - known high nibble, unknown low nibble data[size] = 0x100 + (high_nibble << 4); // 0x100 + (high_nibble << 4) s++; // Skip the period } else if (*s != '\0') { // Skip whitespace before low nibble while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') { s++; if (*s == '\0') break; } if (*s == '\0') break; // Check for "0x" or "0X" between nibbles if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { s += 2; continue; } // Regular hex byte "XX" u8 low_nibble = hexstr_2_nibble(*s); s++; // Move past low nibble if (low_nibble != 0xFF) { data[size] = (high_nibble << 4) | low_nibble; } else { data[size] = REGEX_SKIP; } } else { // Incomplete byte - treat as wildcard data[size] = REGEX_SKIP; } } else { // Invalid character - skip s++; continue; } } size++; // Skip whitespace after byte while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') { s++; } } } template<typename T> constexpr void patchstr2hex(const char* s, T* data, u8& size) { constexpr auto hexstr_2_nibble = [](char c) -> u8 { if (c >= 'A' && c <= 'F') { return c - 'A' + 10; } if (c >= 'a' && c <= 'f') { return c - 'a' + 10; } if (c >= '0' && c <= '9') { return c - '0'; } return 0; // Default for invalid characters }; // parse and convert string - no wildcards for patch data while (*s != '\0') { // Skip whitespace if (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') { s++; continue; } // Check for "0x" or "0X" and skip it if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { s += 2; continue; } u8 high_nibble = hexstr_2_nibble(*s++); if (*s == '\0') break; // Skip whitespace after high nibble while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') { s++; if (*s == '\0') break; } if (*s == '\0') break; // Check for "0x" or "0X" between nibbles if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { s += 2; continue; } u8 low_nibble = hexstr_2_nibble(*s++); data[size++] = (high_nibble << 4) | low_nibble; // Skip whitespace after low nibble while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') { s++; } } }
@AllOver
No problem, I thought I'd already explained what functions were changed -patcher/apply_patch, I thought you'd just read those two functions and be able to see what is changed as they are reasonably small. My bad though. I should probably put the code on one of these github /bitbucket/gitlab/codeberg etc. I didn't though as I already looked at impeeza github and apart from BTH nobody else seems to be interested in adding/fixing any code. At this point though I am pretty much done with this code and I can see from the replies that nobody is really interested anyway. So thanks for all your advice. I'll take it onboard for my next project.
{ "nocntchk_19.0.0+", "0x40f9...94..40b9..0012", 2, 0, bl_cond, ret0_patch, ret0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY },
{ "es-12.0.0+", "009.....0.........9.........F.....A9F......9F..3", 10, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(12,0,0), FW_VER_ANY },
{ "es_1.0.0-8.1.1", "0x....E8.00...FF97.0300AA..00.....E0.0091..0094.7E4092.......A9", 36, 0, and_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(8,1,1) },
{ "es_9.0.0-18.1.0", "0x02.00...........00...00.....A0..D1...97.......A9", 32, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(9,0,0), MAKEHOSVERSION(18,1,0) },
{ "es_19.0.0+", "0xA1.00...........00...00.....A0..D1...97.......A9", 32, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY },
I tested it here:for me it is more about UX/design in mind.
"why fix something that isn't broken?"
"what merit is there to make this change?"
"is the change consistent with the expected behaviour?"
"does the change fit within the scope of the current function, or should it be in a different new function?"
"does what you added make sense within the scope of the program?"
example, making .ips patches is benign, on subject, it certainly conforms to the scope, but is strictly not necessary, but on paper, why not? (re-writing .ips patches however, that's a silly addition, which may cause unintended "!!FUN!!" in the future)
then i evaluate your proposed change to "str2hex" and my immediate reaction is "why?", should it not strictly do exactly what the function name is, to make strings into hex?
why all this extra stuff?
Sure there can be merit for reading patterns as bytes with every single nibble read, and transforming every pattern within sys-patch to conform to that new spec, i.e. multiplying every "." to "..", and be able to wildcard nibble instead of byte....
however, entertaining wildcarding nibbles sounds fine, but at same time, does that conform to the scope of arm instructions? as that ultimately is what we're pattern searching for, arm instructions in sequence.
where a single nibble change could yield an entirely different arm instruction, is this a wise thing to implement in something that should be as accurate as possible?
example, most of current patterns include the very cond_check in it, which is bad design (my personal opinion), as it also prevents searching for a pre-patched binary for the pattern (should i fix this? Yes. have i yet? no. why? im lazy)
example:
C++:{ "nocntchk_19.0.0+", "0x40f9...94..40b9..0012", 2, 0, bl_cond, ret0_patch, ret0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY },
where bl_cond is testing the 4th byte from offset 0x2 of the pattern, which becomes..."...94" (21.0.0+ = "6AB80394")
and bl_cond is valid at both 0x25 and 0x94 (!) and we are patching this with "E0031F2A"
the pattern cannot find "40f9e0031f2a..40b9..0012" as the pattern itself prevents that.
what we found initially before patching was "40f96ab80394..40b9..0012""
changing this, it wouldn't matter if the bl instruction we were patching was 0x25 in the 4th byte or 0x94 in the 4th byte, as that's what the bl_cond check exists for, it doesn't require ...94 or ...25 to be in the pattern, that's redundant.
the bytes being tested, and the bytes being patched, should be wildcarded inside the pattern, if it was, both results would be valid, and essentially is how sys-patch verifies if something is patched by file or not, it cannot verify if it cannot find the pattern, and it cannot find the pattern if the patterns include the "_cond" (which is the 4th byte of an arm instruction)
(using the logic above on nifm/ctest however, we're now entertaining the notion of blanking the entire length of `
00309AD2001EA1F2610100D4E0031FAAC0035FD6` just to conform to what i describe, despite we're only verifying the very first arm instruction exists, which doesn't really make sense, so that one's a lost battle in design of patterns)
edit:
* i have now redone all patterns to conform to the "rules" above.
https://github.com/borntohonk/sys-patch/commit/7dd96c185b519c44351918e759570bcc7a115bda
https://github.com/borntohonk/Switch-Ghidra-Guides/blob/master/scripts/find_patterns.py
https://github.com/borntohonk/Switch-Ghidra-Guides/blob/master/scripts/known_patterns.py
the cond wildcarding situation should in theory be addressed with this.
with nibble wildcarding
es 12.0.0 to 21.1.0 can become
C++:{ "es-12.0.0+", "009.....0.........9.........F.....A9F......9F..3", 10, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(12,0,0), FW_VER_ANY },
but in general nibble wildcarding causes way more duplicate results and incorrect offsets desired, as the search string isn't unique enough, for ES aligning it with the most firmware versions possible, without the bytes being patched, more of a hinderance.
C++:{ "es_1.0.0-8.1.1", "0x....E8.00...FF97.0300AA..00.....E0.0091..0094.7E4092.......A9", 36, 0, and_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(8,1,1) }, { "es_9.0.0-18.1.0", "0x02.00...........00...00.....A0..D1...97.......A9", 32, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(9,0,0), MAKEHOSVERSION(18,1,0) }, { "es_19.0.0+", "0xA1.00...........00...00.....A0..D1...97.......A9", 32, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY },
What exactly have you tested...?I tested it here:
tinfoil opens normally, but before finishing the download it gives an error and closes.
Is this the same version that's on your GitHub?for me it is more about UX/design in mind.
"why fix something that isn't broken?"
"what merit is there to make this change?"
"is the change consistent with the expected behaviour?"
"does the change fit within the scope of the current function, or should it be in a different new function?"
"does what you added make sense within the scope of the program?"
example, making .ips patches is benign, on subject, it certainly conforms to the scope, but is strictly not necessary, but on paper, why not? (re-writing .ips patches however, that's a silly addition, which may cause unintended "!!FUN!!" in the future)
then i evaluate your proposed change to "str2hex" and my immediate reaction is "why?", should it not strictly do exactly what the function name is, to make strings into hex?
why all this extra stuff?
Sure there can be merit for reading patterns as bytes with every single nibble read, and transforming every pattern within sys-patch to conform to that new spec, i.e. multiplying every "." to "..", and be able to wildcard nibble instead of byte....
however, entertaining wildcarding nibbles sounds fine, but at same time, does that conform to the scope of arm instructions? as that ultimately is what we're pattern searching for, arm instructions in sequence.
where a single nibble change could yield an entirely different arm instruction, is this a wise thing to implement in something that should be as accurate as possible?
example, most of current patterns include the very cond_check in it, which is bad design (my personal opinion), as it also prevents searching for a pre-patched binary for the pattern (should i fix this? Yes. have i yet? no. why? im lazy)
example:
C++:{ "nocntchk_19.0.0+", "0x40f9...94..40b9..0012", 2, 0, bl_cond, ret0_patch, ret0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY },
where bl_cond is testing the 4th byte from offset 0x2 of the pattern, which becomes..."...94" (21.0.0+ = "6AB80394")
and bl_cond is valid at both 0x25 and 0x94 (!) and we are patching this with "E0031F2A"
the pattern cannot find "40f9e0031f2a..40b9..0012" as the pattern itself prevents that.
what we found initially before patching was "40f96ab80394..40b9..0012""
changing this, it wouldn't matter if the bl instruction we were patching was 0x25 in the 4th byte or 0x94 in the 4th byte, as that's what the bl_cond check exists for, it doesn't require ...94 or ...25 to be in the pattern, that's redundant.
the bytes being tested, and the bytes being patched, should be wildcarded inside the pattern, if it was, both results would be valid, and essentially is how sys-patch verifies if something is patched by file or not, it cannot verify if it cannot find the pattern, and it cannot find the pattern if the patterns include the "_cond" (which is the 4th byte of an arm instruction)
(using the logic above on nifm/ctest however, we're now entertaining the notion of blanking the entire length of `
00309AD2001EA1F2610100D4E0031FAAC0035FD6` just to conform to what i describe, despite we're only verifying the very first arm instruction exists, which doesn't really make sense, so that one's a lost battle in design of patterns)
edit:
* i have now redone all patterns to conform to the "rules" above.
https://github.com/borntohonk/sys-patch/commit/7dd96c185b519c44351918e759570bcc7a115bda
https://github.com/borntohonk/Switch-Ghidra-Guides/blob/master/scripts/find_patterns.py
https://github.com/borntohonk/Switch-Ghidra-Guides/blob/master/scripts/known_patterns.py
the cond wildcarding situation should in theory be addressed with this.
with nibble wildcarding
es 12.0.0 to 21.1.0 can become
C++:{ "es-12.0.0+", "009.....0.........9.........F.....A9F......9F..3", 10, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(12,0,0), FW_VER_ANY },
but in general nibble wildcarding causes way more duplicate results and incorrect offsets desired, as the search string isn't unique enough, for ES aligning it with the most firmware versions possible, without the bytes being patched, more of a hinderance.
C++:{ "es_1.0.0-8.1.1", "0x....E8.00...FF97.0300AA..00.....E0.0091..0094.7E4092.......A9", 36, 0, and_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(8,1,1) }, { "es_9.0.0-18.1.0", "0x02.00...........00...00.....A0..D1...97.......A9", 32, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(9,0,0), MAKEHOSVERSION(18,1,0) }, { "es_19.0.0+", "0xA1.00...........00...00.....A0..D1...97.......A9", 32, 0, mov2_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY },
I purge the release/tag and stealth update whenever i make changes, but yes,Is this the same version that's on your GitHub?