Skip to content

memberof: derive membership graphs once per transaction - #9051

Draft
karlg100 wants to merge 10 commits into
SSSD:masterfrom
karlg100:kag/issue-9049-transactional-memberof
Draft

memberof: derive membership graphs once per transaction#9051
karlg100 wants to merge 10 commits into
SSSD:masterfrom
karlg100:kag/issue-9049-transactional-memberof

Conversation

@karlg100

@karlg100 karlg100 commented Aug 7, 2026

Copy link
Copy Markdown

Problem

The memberOf plugin currently performs recursive reverse-membership searches
and derived-attribute writes while processing individual graph changes.

Dense caches and large group replacements can therefore trigger repeated
graph walks and large numbers of redundant writes within one transaction.
This can keep the backend occupied beyond its watchdog deadline and block
identity and login requests.

Solution

Introduce a transaction-scoped membership graph engine that:

  • snapshots graph-relevant cache data on the first relevant write;
  • journals direct membership, ghost, delete, and rename changes;
  • stages journal changes only after the lower LDB write succeeds;
  • condenses membership cycles using strongly connected components;
  • derives ancestor, memberUid, and ghost sets once at prepare-commit;
  • writes only derived attributes whose values changed;
  • bypasses graph setup for metadata-only cache updates;
  • retains a legacy operational fallback during migration.

Expected lower-layer errors, such as duplicate member additions and removal of
absent members, leave the graph journal unchanged and do not poison the
transaction. An internal graph failure after a successful database write
still prevents commit because the database and journal can no longer be
safely reconciled.

Allocation growth, value cardinality, and integer conversions are explicitly
bounded before allocating or copying directory-controlled values.

Tests

Add focused coverage for:

  • repeated empty transaction lifecycles;
  • transaction cancellation and migration-marker rollback;
  • add, modify, delete, and rename replay in one transaction;
  • direct and inherited ghost provenance;
  • cycle handling;
  • metadata-only updates;
  • reducing a 3,500-member group to four members;
  • duplicate additions and absent deletions followed by a valid update;
  • direct member, reverse memberOf, and derived memberUid results.

Scope

This addresses the repeated membership-graph work and large-group replacement
path described in #9049.

It depends on the rename support proposed in #9050. It does not include the
separate NSS mmap cache-warming optimization.

Validation

  • The equivalent transactional implementation has been built and exercised
    on the RHEL 9 and RHEL 10 investigation branches.
  • Focused sysdb and NSS tests passed on those branches.
  • Build, focused transaction tests, and the supervised reproducer will be run
    against the exact master-rebased series before marking this PR ready.

Refs #9049
Depends on #9050

karlg100 added 10 commits August 7, 2026 12:26
A raw LDB rename changes the entry DN but leaves member and memberOf values that refer to the old DN. That disconnects otherwise valid direct and transitive membership edges.

Add a rename handler that finds references to the old DN, forwards the rename, then replaces only the affected member and memberOf values while preserving controls and response data.

Dependency: none. This is independently useful and is required before the same-GID sysdb rename change.

Related: SSSD#9049
The rename reference search accumulates an unbounded number of matching entries. Keep its counters in size_t and reject growth at SIZE_MAX before computing num_entries + 1 for talloc_realloc().

This prevents integer wrap from turning a large result count into an undersized allocation followed by an out-of-bounds write.

Dependency: memberof rename support from the preceding patch.
When a group arrives under a new name with a GID already present in the cache, deleting and recreating the old entry discards membership state and forces expensive reconstruction.

Treat the entry as a rename only when stable identity attributes agree. Prefer SID or UUID, fall back to originalDN, rename the cached DN, remove the stale timestamp-cache key, and then store the incoming attributes.

After ldb_rename(), explicitly replace the stored name. Searches in the open transaction can already expose the new RDN and otherwise make the generic no-op comparison suppress this required write.

Dependency: memberof rename support, because all member and memberOf references must follow the renamed DN.

Related: SSSD#9049
Exercise a same-identity, same-GID group rename while the group is both a child and a parent in a nested graph.

Query member and memberOf explicitly, then verify the old name disappears, direct and inherited references use the new DN, and initgroups returns the renamed group without stale membership.

Dependencies: memberof rename support and the verified same-GID sysdb rename patches.
The legacy rebuild path rewrites memberOf and memberUid on every user and group even when the derived value sets are identical. In large caches those no-op LDB writes dominate transaction time and can push the backend into the watchdog window.

Hash the original and rebuilt value sets, suppress duplicate memberUid values, and skip the modify request when neither derived attribute changed.

Dependency: none. This optimization is independent of the transactional graph engine and can be cherry-picked on its own.

Related: SSSD#9049
The legacy module performs recursive reverse-membership searches and derived-attribute writes while processing each changed edge. Dense caches turn large replacements or same-GID refreshes into repeated graph walks that can occupy the backend past its watchdog interval.

Introduce a transaction-scoped graph engine. It snapshots graph-relevant cache data on the first relevant write, journals direct ghost and rename state, condenses cycles with strongly connected components, derives ancestor/memberUid/ghost sets once at prepare_commit, and writes only changed derived attributes.

Public ghost writes remain on forwarded requests, preserving lower-module schema and modify validation. The transaction snapshot also tracks the public ghost attribute so successful write sequences compute derived deltas from the same transient state stored below memberof. A later patch adds the separate failure latch for rejected forwarded writes.

Metadata-only cache refreshes bypass graph setup. A persistent migration marker distinguishes direct ghost values from inherited legacy values, and SSSD_MEMBEROF_LEGACY retains an operational fallback during rollout. LDB remains the storage engine.

This implementation stays in one compilation unit because its snapshot, journal, cycle handling, and flush stages share private invariants; splitting them would create unbuilt or partially functional intermediate commits.

Dependency: the memberof rename handler patch, used as the legacy fallback when the transactional engine is disabled.

Fixes the repeated graph-work mechanism described in SSSD#9049
Graph snapshots and work queues derive allocation sizes from directory-controlled value counts. Check addition, capacity doubling, element-size multiplication, LDB unsigned cardinality limits, and value_length + 1 before allocating or copying.

Also reject non-empty values with a NULL source pointer and make the final size_t-to-unsigned assignment explicit after validation.

Dependency: the transactional memberof graph engine. This patch is intentionally separate so the allocation and cardinality protections can receive focused review and fast-track treatment.
The transaction graph must describe the state accepted by lower LDB modules. Forward graph-relevant writes through completion callbacks and apply add, modify, delete, and rename journal changes only after the lower request succeeds.

Expected duplicate additions and absent deletions now leave the journal unchanged and do not poison the transaction. If an internal graph update fails after a successful database write, latch the failure and reject prepare_commit because the database and journal can no longer be reconciled safely.

Dependencies: the transactional memberof graph engine and its lower-request callback infrastructure.
Add focused coverage for repeated empty transaction lifecycles, migration-marker rollback, add/modify/delete/rename replay within one transaction, direct and inherited ghost provenance, and a 3,500-member replacement mixed with metadata-only updates.

The scale case verifies direct member values, derived memberUid values, and retained/removed memberOf edges after reducing the group to four members.

The ghost case replaces direct ghost values, verifies that inherited values stay out of ghostDirect, and confirms that deleting an absent ghost is rejected by lower LDB while transaction cancellation preserves existing values.

Dependencies: the transactional memberof graph engine and the rename test patch, which provides the shared test_message_has_value() helper. These tests do not require the failed-write latch patch.
Exercise an absent member deletion and a duplicate member addition inside one transaction, then perform a valid addition and commit.

The failed no-op writes must be rejected by the lower LDB layer without changing or poisoning the transaction journal. The later successful write must still update direct membership, reverse memberOf, and derived memberUid state.

This covers the asynchronous lower-request callback while preserving the expected sysdb transaction semantics.
@karlg100

karlg100 commented Aug 7, 2026

Copy link
Copy Markdown
Author

waiting to submit this from draft until #9050 has been reviewed and accepted.

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