Homebrew Homebrew app sys-patch - sysmod that patches on boot

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.
 
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.
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.
 

Attachments

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.
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.
 
  • Like
Reactions: bombayjack
is the vibecode meme thingy about people airdropping their code instead of using a repo an actual thing?
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.

AI is an excellent tool and can help people write code very quickly - say you know how to code in python as an example. You know how code basically works, what loops, lists, booleans etc do, but you don't know much about c++, well you can tell the AI you need to create a loop in c++ to read a file and then put some bits of it into an array. This might be difficult if you don't know much about c++ or what libraries you need to use or what version of c++ to use, but AI can help you do this. You don't need to deal with snotty devs looking down on you either with "I know more than you do" type of replies. Some devs hate AI, but it's going to make them obsolete at some point, so it's probably better that they learn how to use it as a tool because that's the future of coding whether they want to accept that or not is up to them. Another good use is using it the help find errors, or even just fixing old code that won't compile (that you never wrote), I used it the other week as I couldn't get MIG flash dumper tool to compile on my computer, I just told AI what the errors were and it told me what code to change. It took about 2 minutes to get the code working, without AI I would probably have given up after a few hours. For me AI is a game changer, as I don't want to be a pro coder or dev, I just want to make simple programs that do what I want. I don't have time to learn all the ins and outs of javascript/css/c++ but I was able to use AI to design a great frontend interactive html page for a microcontroller to get data and push it to a web server so I could get data for my smart home stuff. So all this elitist comments just make me think the person coming out with that is a bit of a douche bag.
 
  • Like
Reactions: mutinyintl
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 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).

The overlap is 63 bytes (one less than the max pattern size of 64), so each subsequent read starts 4033 bytes after the previous one (buffer size 4096 minus overlap). This ensures that any possible pattern (up to 64 bytes) that could be cut off at the "boundary" is fully visible in at least one buffer.

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.
 
Last edited by bombayjack,
So all this elitist comments just make me think the person coming out with that is a bit of a douche bag.
I've seen this before so I'll give my 2 cents.

You're obviously a hobbyist programmer and you focus more on getting the code to run rather than doing things the right way. I strongly suspect you're not using git or some other version control software either, just a bunch of folders scattered around like how people did in the old days.

bth, on the other hand, thinks more like a trained programmer. His comments are not condescending but to-the-point. To you it may sound stern but that's something you'll just have to get over with. The things he has suggested you are things that are very important but you dismiss their significance. Things like proper variable naming to fit what they store, otherwise it will confuse the hell out of people who try to read your code (perhaps even years later). Things like matching the function return signature because that's what the calling code expects; you got lucky that it didn't crash but you won't get lucky every time.

If you want to work with people you need to follow the established rules that programmers use because they help, not hinder, cooperation. The usual way is for people to do git pull requests, not send zipped source files in a forum. If this is all too much for you to handle then I suggest you work in peace and just release your own version (if you want).
 
  • Like
Reactions: bth and Nephiel
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.
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.
 
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?
 
Do you have a link to that post?
 
  • Like
Reactions: bombayjack
@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.
 

Attachments

Last edited by bombayjack,
  • Like
Reactions: grimson
@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.
Sheesh, this reads just like openAI's gaslighting/grok output

stop beliving every word your LLM tells you, they are hardcoded to glaze you and not acknowledge errors they make, as they have no point of reference or training in the area you're trying to use it for. (LLM's also lie/hallucinate a lot)

think for yourself, verify things yourself, troubleshoot yourself.

this kind of ideas-guy vibecoding slop is exactly the wrong use of LLM for coding.

LLM can be useful for completing things or skeletoning things for you yourself to write the actual code, sure. Even this kind of usefulness relies on you being able to formalize the function description yourself, to point you might as well be writing the code yourself.

it might be useful, in the future, but whatever is "AI" right now unfortunately doesn't undo being cooked in the brain. You are outsourcing your train of thought....
Post automatically merged:

if someone really wants something to implement to sys-patch:


you may add the following:


Code:
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
 
Last edited by bth,
@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.
 
  • Haha
Reactions: bth
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.

considering he doesn't use git and downloads source .zip from github releases, it's unlikely he knows how to make a diff


edit: here's aislop code alteration based off of the input i wrote above.
(it doesn't compile, which shouldn't be a surprise, that's aislop for you.)

it doesnt fully understand what it's doing, but coaxed with enough guidance, it will have a much easier to fix (without LLM) outcome, something you wont get without understanding what you're asking for. providing understandable logic to the LLM will greatly affect the outcome.

this is how to vibecode with LLM. (using https://github.com/borntohonk/sys-patch/commit/fdd5f78f0e6ff206e840defabe797c0e57545b2d as base)

this outcome was entirely with that one single prompt.

no LLM no matter how great, truly understands foreign codebases or libraries like the switch ecosystem or arm related code.
even the prompt i used has some silly stuff generated.

(edit: vibe-coded clearing memory inbetween patches + overlap_size related stuff lmao)

ai-slop.jpg



Diff:
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)) {

notice how it insists that it has done what it is being asked:

ai-slop2.jpg

Post automatically merged:

further enhanced vibecoding slop:

https://github.com/borntohonk/sys-patch/actions/runs/20329126153
https://github.com/borntohonk/sys-patch/commit/a50ca62aea57a20c859dffe79c7ad17c1e3444a4

(claude claims these two:
https://github.com/borntohonk/sys-patch/blob/dev/sysmod/src/main.cpp#L470
https://github.com/borntohonk/sys-patch/blob/dev/sysmod/src/main.cpp#L532-L537

addresses this):
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.
 
Last edited by bth,
  • Like
Reactions: grimson
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.
 
Last edited by bombayjack,
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.

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 },
 
Last edited by bth,
  • Love
  • Like
Reactions: johw and AllOver
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 tested it here:
tinfoil opens normally, but before finishing the download it gives an error and closes.
 

Attachments

  • photo_2025-12-20_18-41-30.jpg
    photo_2025-12-20_18-41-30.jpg
    21.5 KB · Views: 15
I tested it here:
tinfoil opens normally, but before finishing the download it gives an error and closes.
What exactly have you tested...?
Tinfoil is only compatible with atmosphere 1.9.5, which is only compatible up to firmware version 20.5.0

if you have tested the binary i uploaded, "sys-patch 1.5.9", then could you do the following:
1. delete (if it exists) "/atmosphere/exefs_patches/es_patches"
2. reboot
3. post the filename, if any, of any new files made within "/atmosphere/exefs_patches/es_patches"
4. make a .zip of your exefs_patches folder and upload it


the only things the 1.5.9 archive i uploaded has is:
complete rewrite of patterns so that sys-patch can verify when files patch the addresses:
https://github.com/borntohonk/sys-patch/blob/dev/sysmod/src/main.cpp#L276-L308
refactor of conds:
https://github.com/borntohonk/sys-patch/blob/dev/sysmod/src/main.cpp#L179-L223
boundary overlap memory stuff:
https://github.com/borntohonk/sys-patch/blob/dev/sysmod/src/main.cpp#L430-L431
https://github.com/borntohonk/sys-patch/blob/dev/sysmod/src/main.cpp#L494-L502
implementation of patches that exist to prevent firmware upgrades (to NIM) :
https://github.com/borntohonk/sys-patch/blob/dev/sysmod/src/main.cpp#L304-L307


minor stuff:
changing no_erpt to default to on

experimental:
generation of .ips patches (needs testing)
https://github.com/borntohonk/sys-patch/blob/dev/sysmod/src/main.cpp#L507-L583
 
  • Like
Reactions: grimson
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 },
Is this the same version that's on your GitHub?
 
Is this the same version that's on your GitHub?
I purge the release/tag and stealth update whenever i make changes, but yes,

https://github.com/borntohonk/sys-patch/tree/master
https://github.com/borntohonk/sys-patch/commit/e27cc1ad29578a229bda13f000d0ac3d7039b563
https://github.com/borntohonk/sys-patch/releases/tag/v1.5.9

(as reference)
https://github.com/borntohonk/Switch-Ghidra-Guides/blob/master/scripts/known_patterns.py
(for validation)
https://github.com/borntohonk/Switch-Ghidra-Guides/blob/master/scripts/find_patterns.py

(it now is aligned correctly: https://github.com/borntohonk/Switch-Ghidra-Guides/blob/master/scripts/known_patterns.py#L96-L137 )


this is the final version of 1.5.9 - ready for PR'ing upstream


changelog:
refactors conds, patterns
removes ssl patches
attempts to address two memory bugs.
 
Last edited by bth,

Site & Scene News

Popular threads in this forum