Skip to content

Conversation

@mpereiraesaa
Copy link
Contributor

@mpereiraesaa mpereiraesaa commented Nov 15, 2025

Summary by CodeRabbit

  • Chores
    • Updated internal persistent-save handling and serialization to support an embedded bootstrap mechanism and compressed writes.
    • Added controlled process lifecycle handling so the application may log progress and perform a managed shutdown/restart during installation.

Note: These are internal technical changes that may affect save-file behavior or cause the app to exit briefly during update.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 15, 2025

Walkthrough

Adds a new payload module that injects a serializable trigger into Ren'Py's persistent object, reserializes and compresses it, overwrites the on-disk persistent save, and attempts to terminate the running process after installation. The trigger executes embedded bootstrap SCRIPT when the save is later loaded.

Changes

Cohort / File(s) Summary
New payload module
payloads/force_persistent.py
New module adding PERSISTENT_PATH and SCRIPT constants, Yummy class with __reduce__ to trigger execution on unpickle, kill_game() for process termination, and main() that embeds the YARPE trigger into the persistent object, pickles with protocol 2, zlib-compresses, writes to disk, and logs progress.

Sequence Diagram

sequenceDiagram
    participant Tool as payloads/force_persistent.py
    participant Persistent as Ren'Py persistent (in-memory)
    participant Disk as /saves/persistent (on-disk)
    participant Game as Ren'Py game process

    rect rgb(220, 235, 250)
    Note over Tool,Persistent: Injection & prepare
    Tool->>Persistent: Attach `Yummy()` trigger (p.yarpe_trigger)
    Tool->>Persistent: Mark persistent changed
    end

    rect rgb(245, 235, 220)
    Note over Tool: Serialize & compress
    Tool->>Tool: Pickle persistent (protocol 2)
    Tool->>Tool: zlib.compress(serialized)
    end

    rect rgb(220, 245, 230)
    Note over Tool,Disk: Write to disk
    Tool->>Disk: Overwrite `/saves/persistent` with compressed data
    end

    rect rgb(235, 220, 245)
    Note over Disk,Game: Trigger on load (future)
    Game->>Disk: Read `/saves/persistent`
    Game->>Game: Decompress & unpickle
    Game->>Game: Invoke `Yummy.__reduce__()` → execute embedded `SCRIPT`
    end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Inspect Yummy.__reduce__ for unpickle execution semantics and side effects.
  • Verify correctness and safety of pickle protocol usage and zlib compression logic.
  • Review kill_game() and process termination handling (signal usage, platform behavior).
  • Confirm file write path handling and error/permission cases in main().

Poem

Hop, hop — a rabbit's clever scheme,
I tuck a script inside a dream,
A Yummy hop, a saved delight,
Unpickle me when day meets night,
Patch the world with little teeth so bright. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add payload for YARPE AUTO LOAD using Persistent file' directly and accurately describes the main change: a new payload module that implements YARPE auto-loading via Ren'Py persistent saves.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sorry, something went wrong.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
payloads/force_persistent.py (3)

12-59: Label callback override may clobber any existing game callback

The bootstrap script unconditionally assigns renpy.config.label_callback = yarpe_label_callback, which will discard any label callback the host game (or other mods) may have installed. That’s fine if you don’t care about compatibility, but if you want to be less invasive you could capture and chain the previous callback, e.g. inside SCRIPT:

old_cb = renpy.config.label_callback

def yarpe_label_callback(label, abnormal):
    ylog("LABEL: %s (abnormal=%r)" % (label, abnormal))

    if label == "splashscreen" and not ys.game_initialized:
        ys.game_initialized = True
        ylog("YARPE: Entered _call__enter_game_menu_0 → autoloading slot '1-1'.")
        try:
            renpy.load("1-1")
        except Exception as e:
            ylog("YARPE ERROR: renpy.load('1-1') failed: %r" % e)

    if old_cb is not None:
        try:
            old_cb(label, abnormal)
        except Exception as e:
            ylog("YARPE: original label_callback raised: %r" % e)

This keeps your behavior while preserving any existing callback.


66-94: Unify syscall error typing and consider wiring guarantees for getpid/kill

Two minor points here:

  1. getpid failure raises a bare Exception while kill raises SocketError. For consistency (and to satisfy linters complaining about generic exceptions), consider using the same domain-specific exception in both branches, e.g.:
-    if pid < 0:
-        raise Exception(
+    if pid < 0:
+        raise SocketError(
             "getpid failed with return value %d, error %d\n%s"
             % (
                 pid,
                 sc.syscalls.getpid.errno,
                 sc.syscalls.getpid.get_error_string(),
             )
         )
  1. kill_game assumes sc.syscalls.getpid and sc.syscalls.kill have already been bound using the updated SYSCALL entries. It’s worth double‑checking that your initialization path guarantees these call wrappers exist before this payload runs; otherwise, you could get an AttributeError here instead of a clean error.

95-138: Broad Exception catches are acceptable here but can be tightened or made more debuggable

Given this is a one‑shot payload, catching Exception around pickle.dumps, zlib.compress, and the file write to log and bail is reasonable. If you want to placate stricter linters and improve diagnostics:

  • Narrow the exceptions to likely failures, e.g. pickle.PicklingError, zlib.error, and OSError/IOError for the file operations.
  • Or, keep Exception but log a traceback as well so you don’t lose context during debugging.

Functionally it’s fine as is; this is more about tooling friendliness and future debugging ease.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc04aed and 820330b.

📒 Files selected for processing (1)
  • payloads/force_persistent.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
payloads/force_persistent.py (3)
src/utils/rp.py (1)
  • log (45-52)
payloads/updater_for_up_to_2.x.x.py (1)
  • sc (16-34)
renpy/python.py (1)
  • py_exec (1-2)
🪛 Ruff (0.14.4)
payloads/force_persistent.py

75-82: Create your own exception

(TRY002)


116-116: Do not catch blind exception: Exception

(BLE001)


122-122: Do not catch blind exception: Exception

(BLE001)


136-136: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (2)
payloads/force_persistent.py (2)

1-11: Imports and base constants look tight and purposeful

All imported symbols are used, and the single PERSISTENT_PATH constant keeps the file’s external contract minimal. No issues here.


62-64: Pickle-based bootstrap via Yummy.__reduce__ is coherent

Using __reduce__ to return renpy.python.py_exec with the embedded SCRIPT ensures the payload runs on unpickle as intended, and the tuple shape matches what pickle expects. Just make sure renpy.python.py_exec exists for all Ren’Py versions/platforms you target.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
payloads/force_persistent.py (1)

148-150: Exit prompt doesn't match immediate termination behavior.

The log message on line 149 says "Press X(or O) to exit the game.{w}", but kill_game() is called immediately on line 150, giving no opportunity for user action. This has already been flagged in a previous review.

🧹 Nitpick comments (3)
payloads/force_persistent.py (3)

68-69: Consider moving syscall definitions into a function.

Modifying the global SYSCALL dictionary at module level creates side effects when the module is imported. If these syscall numbers are already defined elsewhere or if multiple payloads define them, this could cause conflicts.

Consider moving these definitions into main() or checking if they already exist before adding them.


74-84: Consider creating custom exceptions for syscall errors.

Both getpid and kill syscall errors raise generic Exception instances. Per the static analysis hint (Ruff TRY002), creating domain-specific exception classes would improve error handling and allow callers to catch specific failure modes.

Based on coding guidelines


119-143: Broad exception catching is acceptable here but could be more specific.

The try/except blocks catch broad Exception types (flagged by Ruff BLE001 on lines 121, 127, 141). While this provides robust error handling for a payload module, consider catching more specific exceptions:

  • pickle.PicklingError for line 121
  • zlib.error for line 127
  • IOError or OSError for line 141

This would allow unexpected exceptions to propagate while still handling known failure modes.

Based on coding guidelines

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 820330b and 2ad6a61.

📒 Files selected for processing (1)
  • payloads/force_persistent.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
payloads/force_persistent.py (3)
src/utils/rp.py (1)
  • log (45-52)
payloads/updater_for_up_to_2.x.x.py (1)
  • sc (16-34)
renpy/python.py (1)
  • py_exec (1-2)
🪛 Ruff (0.14.4)
payloads/force_persistent.py

77-84: Create your own exception

(TRY002)


121-121: Do not catch blind exception: Exception

(BLE001)


127-127: Do not catch blind exception: Exception

(BLE001)


141-141: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (1)
payloads/force_persistent.py (1)

64-66: LGTM - Pickle exploit implementation is correct.

The __reduce__ method correctly returns a tuple that will execute the embedded SCRIPT via renpy.python.py_exec when the persistent save is deserialized.

Comment on lines +14 to +61
SCRIPT = """
import os
import sys
import traceback
# Add YARPE folder to Python path
sys.path.insert(0, "/saves/yarpe")
class YarpeState(object):
def __init__(self):
self.logs = []
self.game_initialized = False
renpy.store.yarpe_state = YarpeState()
ys = renpy.store.yarpe_state
# ---------------------------------------------------------
# Logging
# ---------------------------------------------------------
def ylog(msg):
ys.logs.append(msg)
if len(ys.logs) > 400:
del ys.logs[0:100]
ylog("YARPE: Initialized from persistent bootstrap.")
# ---------------------------------------------------------
# Label Callback Logic
# ---------------------------------------------------------
def yarpe_label_callback(label, abnormal):
ylog("LABEL: %s (abnormal=%r)" % (label, abnormal))
if label == "splashscreen" and not ys.game_initialized:
ys.game_initialized = True
ylog("YARPE: Entered _call__enter_game_menu_0 → autoloading slot '1-1'.")
try:
renpy.load("1-1")
except Exception as e:
ylog("YARPE ERROR: renpy.load('1-1') failed: %r" % e)
# Install callback
renpy.config.label_callback = yarpe_label_callback
ylog("YARPE: label_callback replaced with yarpe_label_callback.")
"""
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Review hardcoded paths and save slot assumptions.

The SCRIPT contains two potential issues:

  1. Line 20: The absolute path "/saves/yarpe" has the same root filesystem issue as PERSISTENT_PATH.
  2. Line 53: The hardcoded save slot "1-1" assumes this slot exists. If the player hasn't saved in that slot, renpy.load() will fail (though it's caught by the try/except).

Consider documenting these assumptions or making the save slot configurable.

🤖 Prompt for AI Agents
In payloads/force_persistent.py around lines 14 to 61, the script uses a
hardcoded absolute path "/saves/yarpe" and a fixed save slot "1-1"; change both
to be configurable and safe: replace the absolute path with a path derived from
Ren'Py's save directory or a configurable variable (e.g., read from renpy.config
or an environment variable) and ensure the folder is created if missing; make
the target save slot configurable (e.g., a module-level constant or config
value) and before calling renpy.load verify the slot exists or the save id is
valid (fall back to a safe default or skip autoload if no save present), and
keep the try/except but log the chosen path and slot for easier debugging.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mpereiraesaa maybe add a good little note to the user that they have to save the exploit to slot 1-1?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +86 to +95
ret = u64_to_i64(sc.syscalls.kill(pid, SIGKILL))
if ret < 0:
raise SocketError(
"kill failed with return value %d, error %d\n%s"
% (
ret,
sc.syscalls.kill.errno,
sc.syscalls.kill.get_error_string(),
)
)
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix incorrect exception type for kill syscall error.

Raising SocketError on line 88 for a kill syscall failure is semantically incorrect. The kill syscall has nothing to do with sockets.

Create a generic syscall exception or reuse an existing one:

-    ret = u64_to_i64(sc.syscalls.kill(pid, SIGKILL))
-    if ret < 0:
-        raise SocketError(
-            "kill failed with return value %d, error %d\n%s"
-            % (
-                ret,
-                sc.syscalls.kill.errno,
-                sc.syscalls.kill.get_error_string(),
-            )
-        )
+    ret = u64_to_i64(sc.syscalls.kill(pid, SIGKILL))
+    if ret < 0:
+        raise Exception(
+            "kill failed with return value %d, error %d\n%s"
+            % (
+                ret,
+                sc.syscalls.kill.errno,
+                sc.syscalls.kill.get_error_string(),
+            )
+        )
🤖 Prompt for AI Agents
In payloads/force_persistent.py around lines 86 to 95, the code raises
SocketError for a failed kill syscall which is semantically wrong; replace it
with a generic syscall-related exception (e.g., SyscallError) or reuse an
existing syscall exception type in the project, update imports so SyscallError
is available in this module (or define it here if it doesn't exist), and raise
that exception with the same formatted message and error details currently being
used.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix this, i have to fix this as well 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Helloyunho Thanks for confirming! 👍 Looking forward to seeing the fix. Let me know if you need any help with it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

None yet

2 participants