oxidecomputer / oxidecomputer/omicron
TOCTOU Claude Code audit
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 572
- Forks
- 97
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 96
Description
This is a meta-issue tracking findings from a systematic audit of Omicron's database layer for time-of-check-to-time-of-use (TOCTOU) races — sequences of database operations where correctness depends on assumptions about database state that can be invalidated by concurrent operations between separate transactions. The audit was motivated by issues #10301 (missing rcgen bumps in allocate_floating_ip and silo_image_demote) and #8992 (IP pool unlink TOCTOU), both of which turned out to be instances of broader classes of bugs.
Audit conducted against main at 8061ac9fb.
[!WARNING]
Claude Code (Opus 4.7) performed this audit and wrote up this issue. I (@mergeconflict) haven't manually verified its findings or edited the issue description at all except to add this admonition. Treat everything here as innocent until proven guilty.
Summary
- Total findings: 33 distinct issues.
- By severity:
- P1 (structural inconsistency, rcgen contract broken): 3
- P2 (incorrect behavior, recoverable): 15
- P3 (theoretical / narrow / cosmetic): 15
- Already tracked elsewhere: 7 findings are covered by #8992, #9340, or #10301.
- Novel (not filed elsewhere): 26 findings. Most cluster around two related patterns: missing parent-liveness checks on silo-child creates, and missing silo_delete sweeps. Individually novel but systemically one class.
Top 3 highest-impact findings
- P1:
vpc_create_subnet_rawbypassesvpc.subnet_gen. The only producer ofvpc_subnetrows does not bump the rcgen thatproject_delete_vpcrelies on. A concurrentvpc_create_subnetduringproject_delete_vpccan tombstone the VPC with a live subnet still pointing at it. Novel — not covered by any existing issue. - P2: nine distinct silo / silo_user child tables can be orphaned after
silo_delete/silo_user_delete, because those deletes run a non-transactional post-commit sweep and the child creates don't check parent liveness. Includesscim_client_bearer_token, which is security-relevant — an orphaned token still authenticates against a tombstoned silo. - P2: ephemeral-IP-vs-unlink race. The
unlink_ip_pool_from_external_silo_queryCTE added to fix #8992 can't see a freshly-allocated ephemeral IP whoseparent_idandproject_idare both still NULL. The operator flow from #8992 is fixed; this variant is not.
The codebase is generally disciplined on the concurrency patterns that matter most (saga state CAS, state-generation ordering, collection-insert CTE, attach/detach CTE). The systemic gap is the boundary between the rcgen-gated transactional "core" of a delete and the non-transactional "cleanup" that follows — that boundary has not been fully formalized, and it keeps leaking.
Scope
What was audited
- All 16
rcgencolumns innexus/db-schema/src/schema.rs, all 27DatastoreCollectionConfigimpls innexus/db-model/src/, and every rcgen-gated delete operation. - Every child-creation path for every rcgen-gated collection, classified as SAFE (via collection-insert CTE or explicit rcgen-bumping custom CTE) or BUG.
- All 4
DatastoreAttachTargetConfigimpls and every writer of the backing FK columns. - Non-rcgen multi-step datastore methods in
nexus/db-queries/src/db/datastore/andnexus/src/app/*.rs, with emphasis on mutating methods. - Cross-layer app→datastore races: all ~96
fetch_for/lookup_foroccurrences innexus/src/app/*.rsplus action-by-action review of ~15 saga files. - IP pool operations in depth (follow-up to #8992), with adjacent floating-IP, ephemeral-IP, multicast-group, and internet-gateway operations.
- FK-bearing columns pointing at soft-deletable parents; each write path checked for parent-liveness assertion.
- All ~45 background tasks, classified by concurrency story.
- All ordering-style generation columns (
state_generation,*_gen) and their update paths.
What was NOT audited
- Deployment / blueprint execution / inventory collection. These have their own concurrency story (target-generation CAS, blueprint rendezvous) and deserve a separate audit.
- Region-replacement / volume state-machine internals. Spot-checked only; defensively coded with explicit CAS, but a deep audit would be warranted.
- Saga framework internals (
saga_state,saga_node_event). - Service-networking tables (
bgp_*,switch_port_*,bootstore_*,loopback_address). Rack-setup data, different concurrency model. - Audit log. Append-only; FK columns informational.
Known blind spots
- The audit did not attempt to reproduce any bug with a concrete failing test. Findings are by code inspection only.
- Individual saga internals of region_replacement, volume, and migration beyond entry points.
- Concurrency-timing tests are not part of this audit.
P1 findings (structural inconsistency)
All three P1 findings share a shape: there is a declared DatastoreCollectionConfig (rcgen) contract between a parent and a child, the parent's delete relies on rcgen to gate tombstoning, but at least one producer of the child bypasses the collection-insert CTE and fails to bump the parent's generation. The result is that a concurrent child create can race through the rcgen guard, leaving an orphan or a tombstoned parent with a live child.
P1-1: vpc_create_subnet_raw doesn't bump vpc.subnet_gen (novel)
- Locations:
- Producer:
nexus/db-queries/src/db/datastore/vpc.rs:989(the call toInsertVpcSubnetQueryinsidevpc_create_subnet_raw). - The CTE itself:
nexus/db-queries/src/db/queries/vpc_subnet.rs(InsertVpcSubnetQuery) — does a plainINSERT INTO vpc_subnetwithout touchingvpcat all. - Consumer:
nexus/db-queries/src/db/datastore/vpc.rs:602(project_delete_vpcUPDATE gated onvpc.subnet_gen). DatastoreCollectionConfig<VpcSubnet> for Vpcimpl:nexus/db-model/src/vpc.rs:120.
- Producer:
- Race: T1
project_delete_vpcpasses subnet-emptiness check, then T2vpc_create_subnetinserts a new subnet without bumpingsubnet_gen, T1's UPDATE WHEREsubnet_gen = oldpasses, VPC is tombstoned with a live subnet still pointing at it. - Fix proposal: Route
vpc_create_subnet_rawthroughVpc::insert_resource(...)machinery (the collection-insert CTE already knows how to bumpsubnet_genand assertvpc.time_deleted IS NULL). This requires makingInsertVpcSubnetQuerythe "insert query" input to the collection-insert CTE. The existing pattern is well-illustrated bycreate_network_interface_raw_connatnexus/db-queries/src/db/datastore/network_interface.rs:393, which wraps theInsertQueryCTE insideVpcSubnet::insert_resource. - Existing issue: none. Needs filing.
P1-2: allocate_floating_ip doesn't bump project.rcgen (known: #10301)
- Locations:
- Producer:
nexus/db-queries/src/db/datastore/external_ip.rs:281(allocate_floating_ipviaNextExternalIpCTE atnexus/db-queries/src/db/queries/external_ip.rs:654-680). - Consumer:
nexus/db-queries/src/db/datastore/project.rs:264(project_deleteUPDATE gated onproject.rcgen). project_deletecheck:ensure_no_floating_ips_in_projectatnexus/db-queries/src/db/datastore/project.rs:243.
- Producer:
- Race: T1
project_deletepasses the floating-IP-emptiness check, T2allocate_floating_ipinserts with project_id=X but only bumpsip_pool_range.rcgen, T1's UPDATE passes because project.rcgen isn't bumped. - Fix proposal: The
NextExternalIpCTE needs to additionally updateproject SET rcgen = rcgen + 1 WHERE id = X AND time_deleted IS NULL, with a sentinel-cast failure mode if project is gone. Alternatively, wrap the floating-IP allocation CTE in a larger CTE that performs the project-rcgen-bump as a prerequisite (similar to howlink_ip_pool_to_external_silo_queryuses sentinel casts). - Existing issue: #10301.
P1-3: silo_image_demote doesn't bump project.rcgen (known: #10301)
- Locations:
- Producer:
nexus/db-queries/src/db/datastore/image.rs:214(silo_image_demotedoes plaindiesel::update(dsl::image).set(...)to setproject_idfrom NULL to X). - Consumer:
nexus/db-queries/src/db/datastore/project.rs:264(project_deleteUPDATE gated onproject.rcgen). project_deletecheck:ensure_no_project_images_in_projectatnexus/db-queries/src/db/datastore/project.rs:244.
- Producer:
- Race: T1
project_deletepasses the project-image-emptiness check, T2silo_image_demotesetsimage.project_id = Xwithout bumping anything, T1's UPDATE passes because nothing bumped project.rcgen. - Fix proposal: This is a plain UPDATE that reassigns an FK column. Two viable fixes:
- Add a "move-into-collection" operation to
DatastoreCollectionConfig. A new method that atomically bumps parent rcgen, assertsparent.time_deleted IS NULL, and updates the child's FK column. This would also fix the symmetricproject_image_promotecase. See Systemic recommendations. - Fold the demote into a CTE that atomically checks
project.time_deleted IS NULL, bumpsproject.rcgen, and updates the image FK — all in a single statement with sentinel casts.
- Add a "move-into-collection" operation to
- Existing issue: #10301.
P2 findings (incorrect behavior, recoverable)
The P2 population splits into three thematic clusters, plus a handful of standalone items.
Missing parent-liveness check on silo / silo_user children
The silo_delete and silo_user_delete datastore methods tombstone the parent in a transaction, then run a non-transactional sweep of child tables as a separate statement sequence. Any child whose create path does not assert parent.time_deleted IS NULL atomically — and most do not — can insert an orphan row after the sweep completes.
The child tables affected (all paths under nexus/db-queries/src/db/datastore/):
| # | Child table | Entry point | Create path | Notes |
|---|---|---|---|---|
| P2-1 | silo_user |
local_idp_create_user |
silo_user.rs:416-453 |
|
| P2-2 | silo_group |
JIT login, SCIM | silo_group.rs:369 |
Race self-documented in source, consequence not addressed |
| P2-3 | ssh_key |
POST /v1/me/ssh-keys |
ssh_key.rs:250 |
|
| P2-4 | device_access_token |
OAuth device grant | device_auth.rs:88 |
Security-relevant |
| P2-5 | console_session |
Console login | console_session.rs:86 |
|
| P2-6 | silo_group_membership |
JIT / SCIM | silo_group.rs:624 |
|
| P2-7 | certificate |
POST /v1/certificates |
certificate.rs:30 |
|
| P2-8 | identity_provider / saml_identity_provider |
POST /v1/system/identity-providers/saml |
identity_provider.rs:103 |
|
| P2-9 | scim_client_bearer_token |
SCIM token create | scim.rs:64-98 |
Security-relevant — orphaned tokens still authenticate against tombstoned silo because scim_lookup_token_by_bearer doesn't join silo.time_deleted IS NULL |
Fix directions (any of the following, listed from local to systemic):
-
Per-site local fix: wrap each child-create in a transaction that first does
SELECT ... FROM silo WHERE id = ? AND time_deleted IS NULL FOR SHAREbefore the insert. This is straightforward but each site needs individual attention. -
Promote
siloandsilo_userto rcgen collections. Add an rcgen column to both, implementDatastoreCollectionConfig<Child> for Silo(andfor SiloUser) for every child table, and route each child insert throughinsert_resource. This is cleaner but invasive — many tables, schema change required. -
Move the
silo_deletepost-commit sweep into a transaction or a saga (this single change eliminates all P2-1 through P2-9 findings by collapsing the sweep and the parent tombstone into one atomic step). The existing comment atsilo.rs:523-525explicitly asks for this. This is the recommended fix — see Systemic recommendations.
Missing silo_delete sweep (flip side of the above)
The case where silo_delete not only doesn't run transactionally but also doesn't clean up a given child table at all. The rows persist indefinitely, orphaned and unreferenced.
-
P2-10:
silo_deletedoes not sweepsilo_image. Compound withproject_image_promotebypassingsilo.rcgen; even if promote bumpedsilo.rcgen,silo_deletenever checks for silo_images. Orphaned silo_image rows persist after silo tombstone. Fix: tombstoneimagerowsWHERE silo_id = X AND project_id IS NULLinside thesilo_deletetransaction (and reject the delete if any exist, matching the projects check). -
P2-11:
silo_deletedoes not sweepscim_client_bearer_token. Compounds with P2-9 above:silo_deletedoes not tombstone the tokens, AND the create does not check silo liveness, ANDscim_lookup_token_by_bearerdoes not joinsilo.time_deleted IS NULL. Fix: add the sweep + add a silo-liveness join in the lookup.
Non-transactional cleanup after tombstone (non-silo instances)
-
P2-12: Ephemeral IP allocation races with silo-unlink (partial #8992). Producer:
nexus/db-queries/src/db/datastore/external_ip.rs:209-213(allocate_instance_ephemeral_ipviaNextExternalIp). Consumer:unlink_ip_pool_from_external_silo_queryatnexus/db-queries/src/db/datastore/ip_pool.rs:2202-2330. The unlink CTE'sinstance_ips/floating_ipssub-CTEs filter onparent_id IS NOT NULLandproject_id IS NOT NULLrespectively, but a freshly-allocated ephemeral IP has both columns NULL untilbegin_attach_ipruns. Allows operator to move a pool toOxideInternalwith a live customer ephemeral IP attached. Fix direction: writesilo_iddirectly toexternal_ipat allocation time so guards can filter by silo without traversing the back-reference. -
P2-13 (novel):
unlink_ip_pool_from_external_silo_querydoes not checkmulticast_groupfor outstanding rows. Purely missing from the guard CTE — not a race. A non-racyDELETE /v1/system/ip-pools/{pool}/silos/{silo}can unlink a silo's multicast pool even when the silo has live multicast groups allocated. Predates the multicast feature, so #8992 doesn't cover it. Fix: add amulticast_groupssub-CTE mirroring the existinginstance_ips/floating_ipsbranches.
Other P2 findings
- P2-14:
probe_createnot transactional — #9340. - P2-15:
probe_deletenot transactional — #9340. - Advisory (downgraded from P2):
zpool_delete_self_and_all_datasetschildren check outside a transaction. Region allocation is already fenced byphysical_disk.disk_policy = in_service AND disk_state = active, so no active exploit. Worth a transactional wrap preemptively, but not P2.
P3 findings (theoretical / low consequence)
ip_pool_is_internalvs. concurrentip_pool_reserve. Classification race. Currently unexploitable —ip_pool_reservehas no callers.ip_pool_unlink_silosecond-step (gateway) cleanup non-transactional. #8992.ip_pool_delete'sip_pool_resourcecleanup window. Analyzed safe.vpc_subnet_unset_custom_routerdetaches via plain UPDATE. Asymmetric with attach CTE.vpc_delete_routerbulk child FK-clear. Self-documented as tolerated.ip_pool_link_silodefault-gateway nested loops. #8992 (explicit TODO).ip_pool_unlink_silogateway-teardown nested loops. #8992 (explicit TODO).physical_disk_deleteno rcgen — only reached from tests today.region_allocationdoesn't checkvolume.time_deletedin replacement sagas.ssh_keys_batch_assigncan orphaninstance_ssh_key.MetricProducerGclist/delete race.PhysicalDiskAdoptionbails on first racing-conflict.migration_mark_failedunconditionally bumps both generations. Structurally fragile but documented-safe.image_deleteunconditionaltime_deletedoverwrite. Cosmetic idempotency issue.- Vestigial
rcgencolumns:disk.rcgen,switch.rcgen,crucible_dataset.rcgen,volume.rcgen,vpc_router.rcgen,internet_gateway.rcgen— no active producer or consumer.
Patterns observed
These are the recurring structural shapes observed across the codebase.
Custom CTE bypasses collection-insert
A custom Diesel CTE that writes to a child table of an rcgen-governed collection but does not bump the parent's generation column. The consumer (the parent delete) relies on the rcgen guard that the producer silently bypasses.
Representatives: InsertVpcSubnetQuery (P1-1), NextExternalIp for floating IPs (P1-2), link_ip_pool_to_external_silo_query (no live contract violated because ip_pool_resource isn't in a DatastoreCollectionConfig), RegionAllocate (vestigial).
Recommendation: any custom CTE writing to a child of an existing DatastoreCollectionConfig parent should either (a) compose with insert_resource (as create_network_interface_raw_conn does — it wraps InsertQuery inside VpcSubnet::insert_resource), or (b) explicitly bump the parent generation and assert parent liveness via sentinel casts.
FK reassignment by plain UPDATE
A bare diesel::update(...).set(fk_column.eq(new_value)) that moves a child between collections without touching any generation column on either the old or new parent.
Representatives: silo_image_demote (P1-3), project_image_promote. Only two instances exist today. Both should be routed through a new "move into collection" operation on DatastoreCollectionConfig (see Systemic recommendations).
Multiple declared parents, only one enforced
A DatastoreCollectionConfig where a child has more than one declared parent but the child's insert path only goes through one of them.
Representative: Zpool has both Sled and PhysicalDisk as parents; zpool_insert_on_connection only goes through Sled::insert_resource. Currently benign.
Recommendation: either drop the unused parent declaration or enforce both.
Vestigial generation column
An rcgen column in the schema that has neither a producer (nothing bumps it) nor a consumer (no delete guard reads it).
Representatives: disk.rcgen, switch.rcgen, crucible_dataset.rcgen, volume.rcgen, vpc_router.rcgen, internet_gateway.rcgen.
Recommendation: deprecate and eventually drop.
Unbumped generation column
An rcgen column that a delete guard reads but that no producer actually bumps. This is the most dangerous case — it looks like there is a contract (consumer reads it, DatastoreCollectionConfig declares it) but there isn't one.
Representative: DatastoreCollectionConfig<VpcSubnet> for Vpc (P1-1). Every producer bypasses the CTE, but project_delete_vpc relies on vpc.subnet_gen as a guard.
Non-transactional cleanup after tombstone
A parent-delete that runs a transactional tombstone with an rcgen guard, followed by a non-transactional post-commit sweep of child tables as a separate sequence of statements.
Representatives: silo_delete child sweeps (which compound with the "missing parent-liveness check" pattern below), zpool_delete_self_and_all_datasets, ip_pool_unlink_silo second step, vpc_delete_router subnet FK clear.
The boundary between the transactional core of a delete and the non-transactional cleanup is the main systemic source of remaining bugs in this codebase.
Mutable-classification check
A read of a mutable classification column followed by an action that depends on that classification, without holding the row between the read and the action.
Representative: ip_pool_is_internal read by app-layer code, followed by a mutation; concurrent ip_pool_reserve could flip the classification between the two. Currently unexploitable because ip_pool_reserve has no callers.
Asymmetric attach / detach
Attach goes through a formal CTE (attach_resource) but detach uses a plain UPDATE, losing the "which parent am I detaching from?" invariant and bypassing any generation bump.
Representatives: vpc_subnet_unset_custom_router, vpc_delete_router's bulk FK clear. Tolerated by the reading RPW's time_deleted IS NULL join; currently no active bug but structurally inconsistent.
Missing parent-liveness check
A child-create path that inserts a row referencing a parent by ID without asserting the parent's time_deleted IS NULL atomically with the insert. Benign in isolation, but combined with a non-transactional cleanup sweep on the parent-delete side (above), the child can be inserted after the sweep completes.
Representatives: all nine findings P2-1 through P2-9. The root cause is the post-commit non-transactional sweep in silo_delete / silo_user_delete, paired with child-create sites that don't join silo.time_deleted IS NULL.
Missing delete-side sweep
A child table with a FK to a soft-deletable parent, but the parent's delete implementation never tombstones the child. Orphans are permanent. Orthogonal to "missing parent-liveness check": even if every child-create had a liveness check, the parent-delete would still need to know about the child table.
Representatives: silo_image (P2-10), scim_client_bearer_token (P2-11).
Recommendation: a compile-time or test-time check that every table with silo_id appears in silo_delete's sweep; same for project_id in project_delete, vpc_id in project_delete_vpc, etc.
Systemic recommendations
Forest-level recommendations that would prevent whole classes of bugs. Ordered by estimated impact.
1. Move silo_delete's post-commit sweep into a transaction or a saga
This single change eliminates all 9 findings P2-1 through P2-9. The current silo_delete at nexus/db-queries/src/db/datastore/silo.rs:432 has two phases: a transactional tombstone with rcgen guard, and a non-transactional post-commit sweep at lines 527-614 (password hashes, silo_users, silo_group_memberships, silo_groups, identity_providers, saml_identity_providers, certificates, ip_pool_resource links, etc.). The code comment at lines 523-525 explicitly asks for this to move to a saga.
Trade-offs:
- A single mega-transaction may be too large for CRDB to handle efficiently on a silo with many users/groups.
- A saga introduces more machinery but can handle arbitrarily large cleanups idempotently.
- An intermediate: keep the sweep non-transactional but add a
SELECT 1 FROM silo WHERE id = ? AND time_deleted IS NULL FOR SHAREas the first statement of every child-create datastore method. This is a much smaller change at the cost of per-site discipline.
Recommendation: saga-ify silo_delete, with each sweep step as its own saga action that is idempotent on re-run. This matches how other long-running deletes (instance_delete) already work.
2. Add a "move into collection" operation to DatastoreCollectionConfig
Today the trait handles "create child in collection" (via insert_resource) and "delete child" (via tombstone). It does not handle "change which collection a child belongs to" — which is what silo_image_demote and project_image_promote need. Both are currently broken (P1-3 and compound with P2-10).
A proposed signature:
fn move_resource<Parent, Child>(
old_parent_id: Parent::CollectionId,
new_parent_id: Parent::CollectionId,
child_id: Child::ResourceId,
) -> CteQuery;
The generated CTE would:
- Assert
old_parent.time_deleted IS NULLANDnew_parent.time_deleted IS NULL. - Bump both
old_parent.rcgenandnew_parent.rcgen. - Update
child.<fk_column> = new_parent_id.
All in one statement, with sentinel-cast failure for either parent being gone. This fixes P1-3 and both image-promote/demote paths.
3. Test-level check: every FK to silo has a corresponding sweep in silo_delete
A test or build-time assertion that walks nexus/db-schema/src/schema.rs, finds every table with a silo_id column, and verifies that silo_delete either (a) tombstones rows in that table or (b) rejects the delete if such rows exist.
Implementation sketch: a #[test] in nexus/db-queries/tests/ that parses the schema and greps silo_delete for references. Same for project_id in project_delete, vpc_id in project_delete_vpc, etc.
This would have caught P2-10 (silo_image) and P2-11 (scim_client_bearer_token) mechanically. Worth implementing.
4. Clean up vestigial rcgen columns
disk.rcgen, switch.rcgen, crucible_dataset.rcgen, volume.rcgen, vpc_router.rcgen, internet_gateway.rcgen are all vestigial (no producer or consumer). Options:
- Drop the schema columns in a migration, simplify the
DatastoreCollectionConfigimpls to not reference them. - Or, wire up the delete-side guard (but this is only worth it if the guard would catch a real race — today none of them would).
Recommend: deprecate and eventually drop. At minimum, file issues for each so they are discoverable.
5. Use of transaction_retry_wrapper given CRDB's implicit FOR UPDATE behavior
CockroachDB's SELECT acquires an implicit row lock under SERIALIZABLE isolation, so many check-and-act patterns that would be unsafe under READ COMMITTED are actually safe under SERIALIZABLE. This audit didn't find a case where transaction_retry_wrapper was incorrectly absent (in the sense of the isolation guarantee), but it did find cases where the absence of a txn boundary crossed a commit point (e.g. silo post-delete sweep). Those are not isolation issues — they are transactional-atomicity issues.
Conclusion: transaction_retry_wrapper is used appropriately. The issue is not whether to use it, but whether the cleanup that follows a transaction needs to be inside a bigger transaction / saga.
6. Consider updating RFD 192
RFD 192 (original rcgen/collection-insert CTE design) predates several of the patterns observed here. Recommended additions:
- Document the "custom-CTE-bypass" failure mode explicitly: custom CTEs that write to children of rcgen collections must either compose with
insert_resourceor manually bump the parent generation. Today this is only in code comments atcollection_insert.rs. - Document the "move into collection" gap (P1-3).
- Document the "non-transactional cleanup after tombstone" concern: post-transactional cleanup is a common source of orphans; if you have cleanup, it needs to be in a saga or a larger transaction.
7. Store silo_id directly on external_ip
P2-12's root cause is that the unlink CTE traverses parent_id -> instance -> project -> silo to attribute an ephemeral IP to a silo. A freshly-allocated ephemeral has parent_id = NULL, so the traversal misses it.
If external_ip.silo_id were populated at allocation time by NextExternalIp, the unlink CTE's guards would not need traversal. This would likely fix P2-12 and prevent future additions of this class.
8. Dedicated DatastoreCollectionConfig for silo + silo_user
This is an alternative to Recommendation #1. If silo had a formal collection relationship with each child table, each child insert would go through insert_resource (atomic parent-liveness + rcgen bump) and the post-delete sweep would have a natural "walk collection" structure.
Invasive; requires either per-child-table impls or a macro/derive. Less attractive than saga-ifying silo_delete (Recommendation #1).
Sub-issues to file
The findings above split into discrete filing units. Check each off as the corresponding issue is filed.
P1 (must file)
-
vpc_create_subnet_rawdoes not bumpvpc.subnet_gen, races withproject_delete_vpc. Covers finding P1-1. New issue. -
silo_image_demote/project_image_promoteshould route throughDatastoreCollectionConfigto bump silo/project rcgen. Covers P1-3. Could be added to #10301 since both touch the image path.
P2 groups (should file)
-
silo_deleteandsilo_user_deletehave non-transactional post-commit sweeps that race with concurrent child creates. Covers findings P2-1 through P2-9 as one systemic fix. Recommended implementation: saga-ifysilo_delete. -
silo_deletedoes not tombstonesilo_imageorscim_client_bearer_tokenrows; add lint to catch future child tables. Covers P2-10 and P2-11. Should include the build-time check that everysilo_id-bearing table is swept. - Ephemeral IPs invisible to
ip_poolunlink guard; populateexternal_ip.silo_idat allocation. Covers P2-12. Partial of #8992; could be updated to #8992 instead of filed new. -
ip_pool_unlink_silodoes not check outstandingmulticast_grouprows. Covers P2-13. Simple CTE change.
P3 cleanup (nice to file)
- Clean up vestigial rcgen columns (
disk,switch,crucible_dataset,volume,vpc_router,internet_gateway). -
vpc_subnet.custom_router_idis updated via plain UPDATE instead ofVpcRouter::detach_resource. Covers thevpc_subnet_unset_custom_routerandvpc_delete_routerbulk FK clear findings from the asymmetric attach/detach pattern. -
MetricProducerGclist/delete race. Trivial fix. -
PhysicalDiskAdoptionearly-exit on conflict. -
region_allocationdoesn't checkvolume.time_deletedin replacement sagas. -
migration_mark_failedfragile generation bump. - Add
move_resourceoperation toDatastoreCollectionConfig(systemic). Covers P1-3 and any future move-child-between-parents operations. - Update RFD 192 with post-audit patterns (documentation).
Covered by existing issues (no new filing required)
- #10301 —
allocate_floating_ipandsilo_image_demotercgen gaps (P1-2, P1-3). - #9340 —
probe_createandprobe_deletenon-transactional. - #8992 —
ip_pool_unlink_siloTOCTOU and related IP pool ops (covers the original operator race and the second-step gateway cleanup).
References
- RFD 192 — original rcgen / collection-insert CTE design. If you update it, consider citing this audit.
- #10301 — floating_ip and silo_image_demote rcgen gaps (covers P1-2, P1-3).
- #9340 — probe_create / probe_delete non-transactional.
- #8992 — ip_pool_unlink_silo TOCTOU and related IP pool ops (partially covers P2-12, plus the related IP pool races and the gateway-cleanup P3).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the audit locations in nexus/db-queries/src/db/datastore/ and the DatastoreCollectionConfig implementations in nexus/db-model/src/, beginning with the P1 findings. Validate one race against the cited producer and consumer code; the audit names no reproducing tests. Done requires independently verifying and tracking the findings as focused issues or fixes, rather than treating this broad meta-issue as one change.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, sql
- Domain
- backend, databases, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100