|
| 1 | +# rffickle Integration for Modal Client (rfmodal) |
| 2 | + |
| 3 | +This is a security-enhanced fork of the Modal client library that integrates `rffickle` for safe pickle deserialization. |
| 4 | + |
| 5 | +## What's Changed |
| 6 | + |
| 7 | +This fork adds support for using `rffickle` (Roboflow's fork of `fickle`) to safely deserialize pickled data from untrusted Modal functions. This prevents pickle-based Remote Code Execution (RCE) attacks when running untrusted user code in Modal sandboxes. |
| 8 | + |
| 9 | +## Security Problem Addressed |
| 10 | + |
| 11 | +When untrusted code runs in a Modal sandbox and returns malicious pickled objects, those objects can execute arbitrary code on the client machine during deserialization. This fork mitigates that risk by using `rffickle`'s firewall to block dangerous pickle operations. |
| 12 | + |
| 13 | +## Implementation |
| 14 | + |
| 15 | +The changes are minimal and focused on four files: |
| 16 | + |
| 17 | +1. **modal/_serialization.py**: Modified `deserialize()` function to optionally use `rffickle.DefaultFirewall` |
| 18 | +2. **modal/app.py**: Added `use_firewall` parameter to `@app.function()` and `@app.cls()` decorators |
| 19 | +3. **modal/_functions.py**: Store and pass the firewall flag through function execution |
| 20 | +4. **modal/_utils/function_utils.py**: Process results with the firewall option |
| 21 | + |
| 22 | +### Key Design Decisions |
| 23 | + |
| 24 | +- **No fallback to unsafe pickle**: If `use_firewall=True` is set but `rffickle` is not installed, the function will fail rather than falling back to unsafe deserialization |
| 25 | +- **Per-function configuration**: Each function can individually opt-in to safe deserialization |
| 26 | +- **Client-side only**: Firewall only applies to client-side (local) deserialization, not server-side |
| 27 | +- **Backward compatible**: Existing code continues to work without changes (defaults to `use_firewall=False`) |
| 28 | + |
| 29 | +## Usage |
| 30 | + |
| 31 | +### Per-Function Configuration |
| 32 | + |
| 33 | +When defining functions in your app: |
| 34 | + |
| 35 | +```python |
| 36 | +import modal |
| 37 | + |
| 38 | +app = modal.App() |
| 39 | + |
| 40 | +# Trusted function - regular pickle deserialization (default) |
| 41 | +@app.function() |
| 42 | +def process_internal_data(data): |
| 43 | + """Processes trusted internal data with full pickle support.""" |
| 44 | + return complex_computation(data) |
| 45 | + |
| 46 | +# Untrusted function - rffickle firewall enabled |
| 47 | +@app.function(use_firewall=True) |
| 48 | +def run_user_code(code: str): |
| 49 | + """Safely runs untrusted user code. |
| 50 | + |
| 51 | + Even if the user code returns malicious pickled objects, |
| 52 | + they will be blocked during deserialization on the client. |
| 53 | + """ |
| 54 | + exec(code) |
| 55 | + return result |
| 56 | + |
| 57 | +# For class-based functions |
| 58 | +@app.cls(use_firewall=True) |
| 59 | +class UntrustedExecutor: |
| 60 | + @modal.method() |
| 61 | + def execute(self, code: str): |
| 62 | + """Execute untrusted code in a sandboxed environment.""" |
| 63 | + exec(code) |
| 64 | + return result |
| 65 | +``` |
| 66 | + |
| 67 | +### Looking Up Remote Functions/Classes |
| 68 | + |
| 69 | +When using `Cls.from_name()` or `Function.from_name()` to look up deployed functions: |
| 70 | + |
| 71 | +```python |
| 72 | +import modal |
| 73 | + |
| 74 | +# Look up a class with firewall protection enabled |
| 75 | +CustomBlockExecutor = modal.Cls.from_name( |
| 76 | + "inference-custom-blocks", |
| 77 | + "CustomBlockExecutor", |
| 78 | + use_firewall=True # Enable safe deserialization |
| 79 | +) |
| 80 | + |
| 81 | +# Now when you call methods on this class, results will be |
| 82 | +# deserialized using rffickle to prevent pickle-based attacks |
| 83 | +executor = CustomBlockExecutor() |
| 84 | +result = executor.run_user_code.remote(untrusted_code) |
| 85 | + |
| 86 | +# For functions: |
| 87 | +untrusted_func = modal.Function.from_name( |
| 88 | + "untrusted-app", |
| 89 | + "process_user_input", |
| 90 | + use_firewall=True |
| 91 | +) |
| 92 | +result = untrusted_func.remote(user_data) |
| 93 | +``` |
| 94 | + |
| 95 | +**Important**: When using `from_name()`, you must explicitly set `use_firewall=True` because the client doesn't know if the deployed function was originally configured with firewall protection. |
| 96 | + |
| 97 | +## Installation |
| 98 | + |
| 99 | +```bash |
| 100 | +# Install the modified modal client (includes rffickle automatically) |
| 101 | +pip install -e /path/to/modal-client |
| 102 | + |
| 103 | +# Or if published to PyPI: |
| 104 | +pip install rfmodal |
| 105 | +``` |
| 106 | + |
| 107 | +The `rffickle` dependency is automatically installed with `rfmodal`. |
| 108 | + |
| 109 | +## Testing |
| 110 | + |
| 111 | +Run the test suite to verify the implementation: |
| 112 | +```bash |
| 113 | +python test_implementation.py # Verifies all code changes are in place |
| 114 | +python test_firewall_direct.py # Tests the serialization module directly |
| 115 | +``` |
| 116 | + |
| 117 | +## Status |
| 118 | + |
| 119 | +✅ **Implementation Complete**: All necessary changes have been made to support per-function firewall configuration. |
| 120 | + |
| 121 | +### What's Working: |
| 122 | +- Per-function `use_firewall` parameter on decorators |
| 123 | +- Firewall flag properly passed through function execution pipeline |
| 124 | +- Safe deserialization using `rffickle` when enabled |
| 125 | +- No performance impact on functions that don't use the firewall |
| 126 | +- Proper error handling (no unsafe fallback) |
| 127 | + |
| 128 | +### Next Steps for Production: |
| 129 | +1. Deploy `rfmodal` package to PyPI |
| 130 | +2. Update Inference codebase to use `use_firewall=True` for custom Python blocks |
| 131 | +3. Add monitoring/logging for blocked pickle operations |
| 132 | +4. Consider adding validation that `rffickle` is installed when `use_firewall=True` is used |
0 commit comments