Summary
spawn orphans reports unassociated Elastic IPs with no ownership test at all, then tells the operator to remove them via two different mechanisms, both of which are wrong: one is a no-op, the other irreversibly deletes a resource spawn never created.
In a shared account this is an invitation to delete another project's static address. I hit it during a DFT pilot campaign in a multi-project account: the single EIP orphans reported and offered to clean up is the control-node address of an unrelated CloudFormation stack.
This is a follow-up to the EIP-visibility work in #262 / #273. #273 got the invariant exactly right — "spawn releases only what it alone creates … EIP handling here is therefore reporting-only" — and correctly hardened RemoveResource to refuse addresses. The gap is upstream of that: the scan that decides what to report has no attribution check for the unassociated case, and the user-facing text contradicts the invariant the code enforces.
Reproduction
spawn 0.100.0, commit 6521c239e5f246e4ddb476e5df67e4e58df241e5, build 2026-08-11T18:51:00Z, us-east-1.
$ spawn orphans --region us-east-1
REGION SERVICE TYPE ID STATE CREATED
us-east-1 ec2 address eipalloc-002da3736d205bc22 unassociated -
1 resource(s).
Run 'spawn cleanup' to remove these (running instances are never removed).
The reported address is not spawn's, is not mine, and carries no spawn:managed tag:
$ aws ec2 describe-addresses --allocation-ids eipalloc-002da3736d205bc22 \
--region us-east-1 --query 'Addresses[0].{ip:PublicIp,tags:Tags}'
{
"ip": "34.224.250.21",
"tags": [
{"Key": "elastic-gcs:role", "Value": "control"},
{"Key": "elastic-gcs:region", "Value": "us-east-1"},
{"Key": "Name", "Value": "elastic-gcs-control-us-east-1"},
{"Key": "aws:cloudformation:logical-id", "Value": "ControlNodeEIP"},
{"Key": "aws:cloudformation:stack-name", "Value": "ElasticGCS-Control-us-east-1"},
{"Key": "aws:cloudformation:stack-id", "Value": "arn:aws:cloudformation:...:stack/ElasticGCS-Control-us-east-1/..."}
]
}
It is a CloudFormation-managed ControlNodeEIP. Releasing it would break that stack and drift it, since CloudFormation still believes it owns the allocation.
Bug 1 — the "only yours" default does not apply to addresses
--all is documented as widening from "only yours" to "every principal in the account". For this address the two outputs are byte-identical:
$ spawn orphans --region us-east-1 # default: "only yours"
us-east-1 ec2 address eipalloc-002da3736d205bc22 unassociated -
$ spawn orphans --region us-east-1 --all # "every principal"
us-east-1 ec2 address eipalloc-002da3736d205bc22 unassociated -
So the address row is unconditional. That is understandable — an EIP allocation has no creator attribution to filter on — but it means the narrowest invocation of orphans still reports a resource belonging to an unrelated stack, with nothing in the output to indicate that.
Note the asymmetry with #273's own reasoning. For an attached EIP, the association with a spawn instance is real ownership evidence, and #273's logic is sound. For an unassociated EIP there is no association left to test, so the same code path reports it with the ownership question silently unanswered.
spawn resources has the same issue, and is arguably worse: cleanup --help describes the resource set as "the shared AWS resources spore.host created (tagged spawn:managed)", but this untagged third-party address is listed in resources alongside genuinely spawn-managed instances and volumes.
Bug 2 — the orphans footer names a command that cannot act on addresses
The footer says:
Run 'spawn cleanup' to remove these (running instances are never removed).
Per #273 Part 2, cleanup filters addresses out by design. When the report contains only addresses — exactly my case — the footer's advice is a guaranteed no-op. An operator who follows it sees cleanup do nothing and reasonably concludes the tool is broken, or escalates to something that isn't a no-op.
Bug 3 — orphans --help asserts ownership the scan never established
spawn never allocates them, so it never releases them. Any EIP listed is yours to release with 'aws ec2 release-address'
The first clause is the correct invariant. The second does not follow from it: "spawn didn't create it" does not imply "you did." The help text converts an absence of attribution into a positive ownership claim, and then recommends the one AWS operation on an EIP that cannot be undone. This is the sentence that would have caused the damage — RemoveResource's refusal to release addresses is bypassed entirely, because the tool has routed the operator around its own guardrail and out to the raw CLI.
Impact
orphans is the safety net (#262: "at minimum, warn"). Here it warns about someone else's resource and hands over a destructive command with an unearned assurance of ownership. The failure is not that spawn deletes anything — #273 made sure it can't — it is that the text tells a human to do what the code correctly refuses to do.
Suggested fix
- Attribute before reporting. For unassociated EIPs, treat presence of foreign ownership markers as disqualifying — at minimum
aws:cloudformation:stack-id (and ideally any aws:* reserved-prefix tag, since those are written by AWS-managed provisioners, not by hand). Either drop them from orphans or move them to a clearly separate advisory section.
- Show the evidence. Add a tag/owner column, or annotate the row (
unassociated (CloudFormation: ElasticGCS-Control-us-east-1)). The operator needs the attribution in the report; requiring a manual describe-addresses to find out whether the safety net is pointing at a real problem defeats it.
- Fix the footer conditionally. Do not print "Run 'spawn cleanup'" when the report contains no cleanup-eligible resources.
- Rewrite the help sentence. Something like: "spawn never allocates Elastic IPs, so it never releases them. Any EIP listed here is outside spawn's control — verify its owner before acting, as
release-address is irreversible." Drop the word "yours".
Point 4 is the one that matters most; 1–3 are defence in depth.
Workaround
Verified the tags manually and left the address alone. Recorded in the campaign's teardown log that the one resource spawn orphans reports is deliberately not mine and must not be released — a note that has to be re-made by hand on every teardown, because the tool asserts the opposite.
Related: #262 (EIP billing on stopped instances), #273 (added EIP visibility, established the reporting-only invariant).
Summary
spawn orphansreports unassociated Elastic IPs with no ownership test at all, then tells the operator to remove them via two different mechanisms, both of which are wrong: one is a no-op, the other irreversibly deletes a resource spawn never created.In a shared account this is an invitation to delete another project's static address. I hit it during a DFT pilot campaign in a multi-project account: the single EIP
orphansreported and offered to clean up is the control-node address of an unrelated CloudFormation stack.This is a follow-up to the EIP-visibility work in #262 / #273. #273 got the invariant exactly right — "spawn releases only what it alone creates … EIP handling here is therefore reporting-only" — and correctly hardened
RemoveResourceto refuse addresses. The gap is upstream of that: the scan that decides what to report has no attribution check for the unassociated case, and the user-facing text contradicts the invariant the code enforces.Reproduction
spawn0.100.0, commit6521c239e5f246e4ddb476e5df67e4e58df241e5, build2026-08-11T18:51:00Z, us-east-1.The reported address is not spawn's, is not mine, and carries no
spawn:managedtag:It is a CloudFormation-managed
ControlNodeEIP. Releasing it would break that stack and drift it, since CloudFormation still believes it owns the allocation.Bug 1 — the "only yours" default does not apply to addresses
--allis documented as widening from "only yours" to "every principal in the account". For this address the two outputs are byte-identical:So the address row is unconditional. That is understandable — an EIP allocation has no creator attribution to filter on — but it means the narrowest invocation of
orphansstill reports a resource belonging to an unrelated stack, with nothing in the output to indicate that.Note the asymmetry with #273's own reasoning. For an attached EIP, the association with a spawn instance is real ownership evidence, and #273's logic is sound. For an unassociated EIP there is no association left to test, so the same code path reports it with the ownership question silently unanswered.
spawn resourceshas the same issue, and is arguably worse:cleanup --helpdescribes the resource set as "the shared AWS resources spore.host created (taggedspawn:managed)", but this untagged third-party address is listed inresourcesalongside genuinely spawn-managed instances and volumes.Bug 2 — the
orphansfooter names a command that cannot act on addressesThe footer says:
Per #273 Part 2,
cleanupfilters addresses out by design. When the report contains only addresses — exactly my case — the footer's advice is a guaranteed no-op. An operator who follows it seescleanupdo nothing and reasonably concludes the tool is broken, or escalates to something that isn't a no-op.Bug 3 —
orphans --helpasserts ownership the scan never establishedThe first clause is the correct invariant. The second does not follow from it: "spawn didn't create it" does not imply "you did." The help text converts an absence of attribution into a positive ownership claim, and then recommends the one AWS operation on an EIP that cannot be undone. This is the sentence that would have caused the damage —
RemoveResource's refusal to release addresses is bypassed entirely, because the tool has routed the operator around its own guardrail and out to the raw CLI.Impact
orphansis the safety net (#262: "at minimum, warn"). Here it warns about someone else's resource and hands over a destructive command with an unearned assurance of ownership. The failure is not that spawn deletes anything — #273 made sure it can't — it is that the text tells a human to do what the code correctly refuses to do.Suggested fix
aws:cloudformation:stack-id(and ideally anyaws:*reserved-prefix tag, since those are written by AWS-managed provisioners, not by hand). Either drop them fromorphansor move them to a clearly separate advisory section.unassociated (CloudFormation: ElasticGCS-Control-us-east-1)). The operator needs the attribution in the report; requiring a manualdescribe-addressesto find out whether the safety net is pointing at a real problem defeats it.release-addressis irreversible." Drop the word "yours".Point 4 is the one that matters most; 1–3 are defence in depth.
Workaround
Verified the tags manually and left the address alone. Recorded in the campaign's teardown log that the one resource
spawn orphansreports is deliberately not mine and must not be released — a note that has to be re-made by hand on every teardown, because the tool asserts the opposite.Related: #262 (EIP billing on stopped instances), #273 (added EIP visibility, established the reporting-only invariant).