Skip to content

a target object could impersonate the metadata proxy and kill the whole run - #269

Merged
devdanzin merged 1 commit into
mainfrom
metaproxy-detection-spoofed-by-catchall-getattr
Sep 3, 2026
Merged

a target object could impersonate the metadata proxy and kill the whole run#269
devdanzin merged 1 commit into
mainfrom
metaproxy-detection-spoofed-by-catchall-getattr

Conversation

@devdanzin

Copy link
Copy Markdown
Owner

Found while triaging the smoketest fleet on the new machine. 15 of 29 fusil runs — about
half the wall clock — died with the same generator crash:

TypeError: '_ShutdownTheme' object is not callable
  write_python_code.py:343 in _get_object_methods

What happened

_MetaProxy was identified by duck-typing: getattr(obj, "_fusil_is_meta", False). Any object
with a catch-all __getattr__ answers that truthily — and CPython 3.16 ships one,
traceback._ShutdownTheme, the stand-in used when _colorize cannot be imported during late
shutdown:

class _ShutdownTheme:
    """Empty stand-in if `_colorize` cannot be imported during late shutdown."""
    def __getattr__(self, _): return self

So generation took the metadata branch on it and called _fusil_raw_methods() — which returns
the theme again, and is not callable.

Two things make this worse than a bad session:

  • the TypeError escapes into the MAS and terminates the fusil process, so the fleet
    supervisor restarts the instance from scratch and the run's progress is lost;
  • it fires with --discover-in-target off, where no proxy can exist at all, so the flag
    should never have been consulted in the first place.

Three more sites had the same hole

get_arg_number and class_arg_number read _fusil_arity / _fusil_ctor_arity off whatever
answered the flag, and _fuzz_one_module_object read _fusil_class_name. All four now go
through one is_meta_proxy().

Why isinstance, and why a new module

isinstance is the only check a target cannot forge. Moving the lookup to type(obj) would
fix _ShutdownTheme but not a hostile metaclass with __getattr__ — and fusil injects
those deliberately (the metaclass bombs), so that variant is tested too.

isinstance needs the class visible to both the generator and the arity code, and
write_python_code already imports arg_numbers — so _MetaProxy moves into its own module
rather than being imported cyclically. write_python_code re-exports it, so nothing else
changes.

Tests

tests/python/test_meta_proxy.py: a real proxy is recognised; a catch-all __getattr__
cannot impersonate one (asserting first that the old check was truthy on it, so the test
would have caught this); a hostile metaclass cannot either; ordinary objects are not proxies;
and both arity helpers fall through to their normal path instead of reading forged metadata.

1291 tests OK, ruff check and ruff format --check both clean.

…le run

Found in the smoketest fleet on the new machine: 15 of 29 fusil runs (about half
the wall clock) died with

    TypeError: '_ShutdownTheme' object is not callable
      write_python_code.py:343 in _get_object_methods

_MetaProxy was identified by duck-typing, `getattr(obj, "_fusil_is_meta", False)`.
Any object with a catch-all __getattr__ answers that truthily, and CPython 3.16
ships one -- traceback._ShutdownTheme, the stand-in used when _colorize cannot be
imported during late shutdown:

    class _ShutdownTheme:
        def __getattr__(self, _): return self

So generation took the metadata branch on it and called _fusil_raw_methods(), which
returns the theme again and is not callable. The TypeError escaped into the MAS and
terminated the fusil PROCESS, not the session -- the fleet supervisor then restarted
the instance from scratch. It happened with --discover-in-target off, where no proxy
can exist at all, so the flag should never have been consulted.

Three more call sites had the same hole: get_arg_number and class_arg_number read
_fusil_arity / _fusil_ctor_arity off whatever answered the flag, and
_fuzz_one_module_object read _fusil_class_name.

The check is now isinstance. That needs the class visible to both the generator and
the arity code, and write_python_code imports arg_numbers, so _MetaProxy moves to
its own module rather than being imported cyclically.

isinstance rather than a duck-type on type(obj) on purpose: fusil injects hostile
METACLASSES deliberately (the metaclass bombs), so moving the lookup from the
instance to its type would still be forgeable. There is a test for each shape.
@devdanzin
devdanzin merged commit f90d7fa into main Sep 3, 2026
2 checks passed
@devdanzin
devdanzin deleted the metaproxy-detection-spoofed-by-catchall-getattr branch September 3, 2026 04:51
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.

1 participant