stamparm/maltrail · error
cannot modify a finalized TrailsDict
Error message
cannot modify a finalized TrailsDict
What it means
This is a generic lifecycle-state guard in TrailsDict.__setitem__: once the dict has been finalized — either frozen or switched to a memory-mapped read-only backend (self._frozen / self._mmap set) — the internal index structures are considered immutable, so any key assignment is refused. It fires when application code keeps a reference to a TrailsDict after finalization (e.g. after freeze or loading from a bin file) and then attempts to store or overwrite a trail. Fix by performing all mutations before finalizing, or by creating a new mutable TrailsDict instance (e.g. via clear()/constructor) instead of writing to the finalized one.
Solutions
- Create a new TrailsDict for updates instead of writing to the finalized one
- Delay freeze()/bin-loading until all writes are complete
- Call clear() (which re-inits) before writing if a fresh empty dict is intended
- Guard writes with a check for the frozen/mmap state and skip or log instead
Example fix
// before
if trail_changed:
trails[key] = value # trails already frozen
// after
if trail_changed and trails._frozen is None and trails._mmap is None:
trails[key] = value Defensive patterns
Strategy: type-guard
Validate before calling
def trails_writable(td):
return td._frozen is None and td._mmap is None Type guard
def is_writable_trailsdict(td):
return isinstance(td, TrailsDict) and td._frozen is None and td._mmap is None Try / catch
try:
trails[key] = value
except Exception as e:
if "finalized" in str(e):
trails = TrailsDict(); trails[key] = value # start a fresh cycle Prevention
- Treat frozen/mmap-backed dicts as strictly read-only
- Build updates in a new dict and swap atomically
- Audit code paths that retain references past finalization
- Coordinate threads with a flag/lock around finalization
When it happens
Trigger: trails.freeze() followed by trails['x'] = (1,2); assigning into a TrailsDict loaded from a memory-mapped bin; an update()/build path that finishes and freezes before a late writer writes.
Common situations: Code holding a reference to a frozen dict shared across threads/sensors; re-running an updater against an already-finalized dict; forgetting to re-create (or clear) the dict before a new update cycle.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- [x] invalid IP address
- not a Maltrail provenance sidecar (bad magic)
- provenance sidecar is truncated
- packet too short for header-protection sample
- trail bin too small
AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13).
Data as JSON: /api/errors/63be8cd63ea067a3.
Report an issue: GitHub.
Appendix: source
Thrown at core/trailsdict.py:270
while i < n and hi[i] == target_hi:
if lo[i] == target_lo:
return pair_list[values[i]]
i += 1
return default
def __len__(self):
mm = self._mmap
if mm is not None:
return mm["length"]
frozen = self._frozen
return frozen[5] if frozen is not None else len(self._trails)
def clear(self):
self.__init__()
def __setitem__(self, key, value):
if self._frozen is not None or self._mmap is not None:
raise Exception("cannot modify a finalized TrailsDict")
if not isinstance(value, (tuple, list)):
raise Exception("unsupported type '%s'" % type(value))
pair = (value[0], value[1])
shared = self._pairs.get(pair)
if shared is None:
shared = pair
self._pairs[pair] = pair
self._trails[key] = shared
def __delitem__(self, key):
if self._frozen is not None or self._mmap is not None:
raise Exception("cannot modify a finalized TrailsDict")
del self._trails[key]
def update(self, value):
if self._frozen is not None or self._mmap is not None:
raise Exception("cannot modify a finalized TrailsDict")View on GitHub (pinned to 77cfb06d76)