Sitelet https://github.com/theupdateframework/python-tuf/pull/2995
Skip to content

Fix __hash__ crashing on unhashable dict attributes - #2995

Open
Sachith77 wants to merge 2 commits into
theupdateframework:developfrom
Sachith77:fix/metadata-hash-unhashable-dicts
Open

Fix __hash__ crashing on unhashable dict attributes#2995
Sachith77 wants to merge 2 commits into
theupdateframework:developfrom
Sachith77:fix/metadata-hash-unhashable-dicts

Conversation

@Sachith77

@Sachith77 Sachith77 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Description

Follow-up to #2973, which fixed __hash__ for Role and DelegatedRole.

Same problem was still there in the other metadata classes: Signed, Root, MetaFile, Snapshot, Delegations, TargetFile, Targets, and Metadata. Their __hash__ implementations passed raw dictionaries into hash(), which throws TypeError: unhashable type: 'dict'.

Timestamp.__hash__ looked fine on its own, but still breaks because it inherits from Signed and MetaFile.

All these classes define __eq__, so they need a working __hash__ too if they're going to be used in sets or as dict keys. test_metadata_eq_.py covers equality for these classes but never tested hashing, which is probably why this slipped through.

Fix follows the same approach as #2973 — hash a subset of immutable fields instead of the raw dicts.

unrecognized_fields is left out since it can hold arbitrary nested JSON. Everything else I just converted to tuples.

Testing

  • test_metadata_hash checks that for each affected class, hash() doesn't raise, equal objects produce equal hashes, and instances can be used as set members. This fails on develop with 8 subtest errors before the fix.
  • test_metadata_hash_ignores_unrecognized_fields checks the deliberate exclusion of unrecognized_fields — objects that only differ in that field are unequal but can share a hash, which is fine given the data model.

@Sachith77
Sachith77 requested a review from a team as a code owner August 24, 2026 19:40
Follow-up to theupdateframework#2973, which fixed Role.__hash__ and DelegatedRole.__hash__.
The same bug remains in the other implementations: Signed, Root, MetaFile,
Snapshot, Delegations, TargetFile, Targets and Metadata all pass a raw dict
to hash(), so hash() raises "TypeError: unhashable type: 'dict'".
Timestamp.__hash__ is itself correct but inherits the failure from Signed
and MetaFile.

All of these classes define __eq__, so __hash__ is required for them to be
usable in a set or as a dict key. test_metadata_eq_.py covers __eq__ for
exactly these classes but never calls hash(), which is why this went
unnoticed.

Hash a subset of immutable fields, as theupdateframework#2973 did. unrecognized_fields is
excluded throughout since it holds arbitrary nested JSON. Snapshot.meta and
Targets.targets contribute len() rather than their keys, to keep hashing
O(1) for roles with many entries.

Signed-off-by: Sachith Reddy <sachith.24bcs10403@sst.scaler.com>
@Sachith77
Sachith77 force-pushed the fix/metadata-hash-unhashable-dicts branch from ee0f4ec to 2932e04 Compare August 24, 2026 20:00

@jku jku left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I guessed the rest of them had issues after #2973... Left some questions in code.

Comment thread tuf/api/_payload.py Outdated
return hash(
(self.version, self.length, self.hashes, self.unrecognized_fields)
)
return hash((self.version, self.length))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about self.hashes here?

Comment thread tuf/api/_payload.py Outdated

def __hash__(self) -> int:
return hash((super().__hash__(), self.meta))
return hash((super().__hash__(), len(self.meta)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

len(self.meta) sounds a bit strange

Comment thread tuf/api/_payload.py Outdated
return hash(
(self.length, self.hashes, self.path, self.unrecognized_fields)
)
return hash((self.length, self.path))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about self.hashes?

Comment thread tuf/api/_payload.py Outdated

def __hash__(self) -> int:
return hash((super().__hash__(), self.targets, self.delegations))
return hash((super().__hash__(), len(self.targets), self.delegations))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

len(self.targets) sounds wrong

Signed-off-by: Sachith Reddy <sachith.24bcs10403@sst.scaler.com>
@Sachith77

Copy link
Copy Markdown
Contributor Author

Yeah, both are fair. hashes is just dict[str, str] so there was no real reason to drop it, I was being over-careful. And with len(), two snapshots with the same number of entries ended up with the same hash.

Used tuple(sorted(...)) in all four since a dict isn't hashable on its own, and the sort keeps it stable if the key order varies.

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.

2 participants