Skip to content

[auditlog] External delattr on patched ORM methods silently and permanently disables auditing #3702

Description

@alopezm-ironside

Module

auditlog

Describe the bug

Any module that removes patched ORM methods from registry classes — base_automation in Odoo core does exactly this — disables auditlog in the worker that performed the operation, silently. No traceback, no log entry. Writes served by that worker stop being audited.

The worker does not recover on its own, because auditlog's re-patching guard checks a marker attribute that the external removal does not clear, so auditlog believes it is still patched.

The blast radius is one worker, not the instance: see the recovery section below. The practical consequence is not an audit outage but silent, partial gaps in the audit trail that cannot be identified after the fact, which for a forensic tool is arguably worse — a missing record is indistinguishable from a change that never happened.

I searched the tracker before opening this and did not find an existing report.

To Reproduce

Affected versions: 17.0, 18.0, 19.0

Steps to reproduce the behavior:

  1. Install auditlog and base_automation.
  2. Create an auditlog.rule on res.partner with log_write, state subscribed/confirmed.
  3. Write to a partner and confirm an auditlog.log record is created.
  4. Create any base.automation record (any model, any trigger).
  5. Write to that partner again.

Step 5 produces no auditlog.log record, when served by the same worker that performed step 4. Subsequent writes, creates and unlinks on every audited model are also unlogged on that worker.

Recovery and scope

The worker that ran _update_registry() stays broken. signal_changes() writes the new sequence and assigns it to its own registry_sequence, so its next check_signaling() finds no difference and it never rebuilds.

Other workers do recover. check_signaling() detects the sequence change and calls Registry.new(self.db_name), which builds fresh model classes; the stale markers are gone with them, so auditlog re-patches normally.

The affected worker recovers when it is recycled, or when it later observes a registry signal raised elsewhere.

Expected behavior

Auditing keeps working after an unrelated module reloads its own ORM hooks, or at minimum fails loudly instead of silently stopping.

Additional context

Reproduced at runtime on 17.0. 18.0 and 19.0 are affected by code inspection: both sides of the interaction are unchanged across the three branches.

base_automation._update_registry() runs on every create/write of a base.automation record and calls _unregister_hook():

# odoo/addons/base_automation/models/base_automation.py  (identical in 17.0, 18.0, 19.0)
def _unregister_hook(self):
    """ Remove the patches installed by _register_hook() """
    NAMES = ['create', 'write', '_compute_field_value', 'unlink', '_onchange_methods', "message_post"]
    for Model in self.env.registry.values():
        for name in NAMES:
            try:
                delattr(Model, name)
            except AttributeError:
                pass

It iterates over every model in the registry and deletes those attributes regardless of which module installed them, so auditlog's patches go with them.

auditlog then declines to reinstall, because _patch_methods() decides based on a marker attribute that the delattr above never touches:

# auditlog/models/rule.py:239 (17.0), :251 (18.0)
# auditlog/models/auditlog_rule.py:278 (19.0)
check_attr = "auditlog_ruled_write"
if rule.log_write and not hasattr(model_model, check_attr):
    updated = rule._patch_method(model_model, "write", check_attr)

The method is gone but auditlog_ruled_write survives on the class, so the guard evaluates to False and the patch is never reinstalled.

Inspecting the .origin chain of res.partner.write before and after creating an automation on res.partner:

before:
    AuditlogRule._make_write.<locals>.write_full                       | rule.py
    ResPartner.write                                                   | res_partner.py

after:
    BaseAutomation._register_hook.<locals>.make_write.<locals>.write   | base_automation.py
    ResPartner.write                                                   | res_partner.py

auditlog's frame is gone from the chain while its marker remains.

Wider than base_automation. Registry.setup_models() applies the same pattern — _unregister_hook() over every model, then _register_hook() over every model. So any runtime registry rebuild in a live process goes through the same blanket removal; creating a custom field via ir_model.py is another trigger. The setup_models() path would self-heal if the guard were sound, because it calls _register_hook() on every model afterwards. The base_automation._update_registry() path would not: there self._register_hook() is base.automation's own hook, so auditlog.rule._register_hook() is never reached.

Why this is easy to miss. The failure produces absent side effects rather than errors, and missing log records are indistinguishable from "nothing needed logging". Because only the originating worker is affected and it recovers on recycling, the damage is partial and intermittent rather than total, which reads as flakiness rather than as a bug.

Possible direction. Not a patch proposal, just where the fix seems to belong: the guard could reflect whether the patch is actually installed rather than trusting a marker that an external delattr can desynchronise. Walking the .origin chain and checking whether any frame belongs to auditlog distinguishes "my patch is alive" from "someone else patched this method", which the presence of .origin alone does not.

Happy to open a PR if maintainers agree on the direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions