mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 18:09:05 -04:00
fix(trl): disable inline GRPO reward code by default (RCE) POST /api/fine-tuning/jobs accepts reward_functions[].code, an inline Python body, and compile_inline_reward() execs it against a restricted-builtins allowlist (_SAFE_BUILTINS). That allowlist is not a security boundary: ().__class__.__bases__[0].__subclasses__() reaches os._wrap_close and thus os.system, giving arbitrary code execution. The fine-tuning endpoint is unauthenticated by default, so any caller could run code on the host. Hardening the allowlist is a losing game against CPython introspection, so inline reward code is now refused unless the operator explicitly opts in with LOCALAI_TRL_ALLOW_INLINE_REWARD=true on the backend. Builtin reward functions are unaffected. The gate lives in build_reward_functions(), the single point all inline specs flow through. Docs updated to stop describing the allowlist as a sandbox and to document the opt-in. Fixes #11015 Signed-off-by: Isabel Wu <231155141+wuisabel-gif@users.noreply.github.com> Co-authored-by: Isabel Wu <231155141+wuisabel-gif@users.noreply.github.com>
49 lines
1.6 KiB
Python
49 lines
1.6 KiB
Python
"""
|
|
Tests for reward_functions.py — no gRPC / model deps, pure stdlib.
|
|
|
|
Covers the inline-code opt-in gate (issue #11015): inline reward code is
|
|
arbitrary code execution and must stay disabled unless the operator opts in.
|
|
"""
|
|
import os
|
|
import unittest
|
|
|
|
import reward_functions as rf
|
|
|
|
INLINE_SPEC = [{"type": "inline", "name": "ok", "code": "return [1.0 for _ in completions]"}]
|
|
|
|
|
|
class TestInlineRewardGate(unittest.TestCase):
|
|
def setUp(self):
|
|
self._saved = os.environ.pop(rf.ALLOW_INLINE_ENV, None)
|
|
|
|
def tearDown(self):
|
|
if self._saved is None:
|
|
os.environ.pop(rf.ALLOW_INLINE_ENV, None)
|
|
else:
|
|
os.environ[rf.ALLOW_INLINE_ENV] = self._saved
|
|
|
|
def test_inline_refused_by_default(self):
|
|
with self.assertRaises(ValueError) as cm:
|
|
rf.build_reward_functions(INLINE_SPEC)
|
|
self.assertIn("disabled by default", str(cm.exception))
|
|
|
|
def test_builtin_works_without_optin(self):
|
|
fns = rf.build_reward_functions([{"type": "builtin", "name": "format_reward"}])
|
|
self.assertEqual(len(fns), 1)
|
|
self.assertTrue(callable(fns[0]))
|
|
|
|
def test_inline_enabled_with_optin(self):
|
|
os.environ[rf.ALLOW_INLINE_ENV] = "true"
|
|
fns = rf.build_reward_functions(INLINE_SPEC)
|
|
self.assertEqual(fns[0](["x"]), [1.0])
|
|
|
|
def test_optin_falsey_values_stay_disabled(self):
|
|
for val in ("", "0", "false", "no", "off"):
|
|
os.environ[rf.ALLOW_INLINE_ENV] = val
|
|
with self.assertRaises(ValueError):
|
|
rf.build_reward_functions(INLINE_SPEC)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|