Skip to content

Commit ca028e0

Browse files
committed
Propagate use_firewall
1 parent bc17318 commit ca028e0

File tree

4 files changed

+44
-19
lines changed

4 files changed

+44
-19
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ This changelog documents user-facing updates (features, enhancements, fixes, and
66

77
<!-- NEW CONTENT GENERATED BELOW. PLEASE PRESERVE THIS COMMENT. -->
88

9+
### 1.2.6 (2025-09-01)
10+
11+
**Bug fixes:**
12+
- Fixed critical firewall flag propagation issue for non-hydrated (lazy-loaded) class methods when using `modal.Cls.from_name()` with `use_firewall=True`
13+
- The `use_firewall` flag is now properly propagated to methods accessed on class instances before the class is fully hydrated
14+
- This fix ensures that secure deserialization with `rffickle` is active on the very first execution after server startup, closing a security vulnerability where the first execution could bypass the firewall
15+
16+
**Technical details:**
17+
- Modified `_Obj.__getattr__` method in `modal/cls.py` to copy the `_use_firewall` flag from the class service function to lazy-loaded method functions
18+
- This ensures the firewall protection is active even for non-hydrated class instances, which is the typical state for `Cls.from_name()` on first access
19+
920
### 1.2.5 (2025-09-01)
1021

1122
**Bug fixes:**

RELEASE_NOTES.md

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
1-
# Roboflow Modal Client Fork v1.2.5
1+
# Roboflow Modal Client Fork v1.2.6
22

33
## Summary
4-
This release fixes a critical bug in the firewall flag propagation for parameterized Modal classes, ensuring secure deserialization works correctly when using `modal.Cls.from_name()` with the `use_firewall=True` parameter.
4+
This release fixes a critical security vulnerability where the firewall flag was not properly propagated to lazy-loaded (non-hydrated) class methods, allowing pickle deserialization exploits to succeed on the first execution after server startup.
55

6-
## Bug Fix
6+
## Critical Security Fix
77

8-
### Firewall Flag Propagation for Parameterized Classes
9-
- **Issue**: When using `modal.Cls.from_name(..., use_firewall=True)` with parameterized classes, the firewall flag was not being propagated to instance methods
10-
- **Impact**: This prevented proper secure deserialization with `rffickle` when executing untrusted code in Modal sandboxes
11-
- **Fix**: Added firewall flag propagation in `_bind_instance_method` function in `modal/cls.py`
8+
### Firewall Flag Propagation for Non-Hydrated Class Methods
9+
- **Issue**: When using `modal.Cls.from_name(..., use_firewall=True)`, the firewall flag was not being propagated to methods accessed on non-hydrated class instances (typical state on first access)
10+
- **Security Impact**: This allowed pickle deserialization exploits to bypass the firewall on the FIRST execution after server startup
11+
- **Fix**: Modified `_Obj.__getattr__` in `modal/cls.py` to properly copy the `_use_firewall` flag to lazy-loaded method functions
1212

1313
## Technical Details
1414

15-
The fix ensures that when you use the following pattern:
15+
The fix ensures that even when accessing methods on non-hydrated class instances (common with `Cls.from_name()`), the firewall protection is active:
16+
1617
```python
18+
# This now properly protects against exploits even on first execution:
1719
cls = modal.Cls.from_name("app-name", "ClassName", use_firewall=True)
18-
instance = cls(param1="value1", param2="value2")
19-
result = instance.some_method.remote(...) # Now correctly uses firewall
20+
instance = cls(workspace_id="workspace")
21+
result = instance.execute_block.remote(...) # Firewall active from first call
2022
```
2123

22-
The `use_firewall` flag is properly inherited by the method, ensuring secure deserialization throughout the execution chain.
24+
### Root Cause
25+
- When a class is loaded via `Cls.from_name()`, it starts in a non-hydrated state
26+
- Method access through `__getattr__` creates lazy-loaded functions
27+
- Previously, these lazy-loaded functions didn't inherit the `_use_firewall` flag
28+
- This meant the first execution (before hydration) was vulnerable
29+
30+
### The Fix
31+
Modified the lazy loader creation in `_Obj.__getattr__` to copy the firewall flag from the class service function to the method function, ensuring protection is active even before hydration.
2332

2433
## Files Changed
25-
- `modal/cls.py` - Added `_use_firewall` flag propagation in `_bind_instance_method`
34+
- `modal/cls.py` - Modified `_Obj.__getattr__` to propagate `_use_firewall` flag to lazy-loaded methods
2635

2736
## How to Deploy to PyPI
2837

@@ -37,15 +46,16 @@ python -m twine upload dist/*
3746
```
3847

3948
## Testing
40-
Ensure that parameterized classes with firewall enabled properly block pickle-based attacks:
41-
- Test with `modal.Cls.from_name()` using `use_firewall=True`
42-
- Verify that instance methods inherit the firewall setting
43-
- Confirm that pickle deserialization attacks are blocked
49+
Critical tests to verify the fix:
50+
- Test with `modal.Cls.from_name()` using `use_firewall=True`
51+
- Verify firewall blocks exploits on FIRST execution after server startup
52+
- Confirm no regression for hydrated class instances
53+
- Test with both parameterized and non-parameterized classes
4454

4555
## Version History
56+
- v1.2.6 - Fixed critical firewall bypass for non-hydrated class methods (first execution vulnerability)
4657
- v1.2.5 - Fixed firewall flag propagation for parameterized classes
4758
- v1.2.4 - Fixed import issues
4859
- v1.2.3 - Fixed grpclib/grpcio incompatibility
4960
- v1.2.2 - Added grpcio dependency
5061
- v1.2.1 - Fixed PyPI package build
51-
- v1.2.0 - Initial Roboflow fork with rffickle integration

modal/cls.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,16 @@ async def method_loader(fun, resolver: Resolver, existing_object_id):
434434

435435
# The reason we don't *always* use this lazy loader is because it precludes attribute access
436436
# on local classes.
437-
return _Function._from_loader(
437+
fun = _Function._from_loader(
438438
method_loader,
439439
rep=f"Method({self._cls._name}.{k})",
440440
deps=lambda: [], # TODO: use cls as dep instead of loading inside method_loader?
441441
hydrate_lazily=True,
442442
)
443+
# Copy the firewall flag from the class service function if it exists
444+
if self._cls._class_service_function and hasattr(self._cls._class_service_function, '_use_firewall'):
445+
fun._use_firewall = self._cls._class_service_function._use_firewall
446+
return fun
443447

444448

445449
Obj = synchronize_api(_Obj)

modal_version/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright Modal Labs 2025
22
"""Supplies the current version of the modal client library."""
33

4-
__version__ = "1.2.5"
4+
__version__ = "1.2.6"

0 commit comments

Comments
 (0)