Skip to content
Open
Changes from 3 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
3c3e079
Cargo RFC for min publish age
tmccombs Feb 23, 2026
7fdd325
RFC 3923: Update RFC number
tmccombs Feb 23, 2026
66c9b27
RFC 3923: Update date
tmccombs Feb 23, 2026
45e41e6
Update text/3923-cargo-min-publish-age.md
tmccombs Feb 24, 2026
d78605c
RFC 3923: Clarification
tmccombs Feb 24, 2026
f14ccfa
Update text/3923-cargo-min-publish-age.md
tmccombs Mar 1, 2026
1dfe133
Add debian prior art
tmccombs Mar 1, 2026
4ac399a
Update text/3923-cargo-min-publish-age.md
tmccombs Mar 1, 2026
6864124
RFC 3923: Address feedback
tmccombs Mar 2, 2026
5733af0
RFC 3923: Add examples to Guide
tmccombs Mar 7, 2026
cea13e1
RFC 3923: Answer question
tmccombs Mar 7, 2026
8337118
Fix text formatting
tmccombs Mar 7, 2026
51e0a7c
RFC 3923: Add more rationale
tmccombs Mar 17, 2026
53f6f4b
fix(summary): Make user involvement explicit
epage Mar 17, 2026
1f05184
fix(motivation): Be clearer on gradual rollout
epage Mar 17, 2026
391cd7f
fix(motivation): Be more technically precise
epage Mar 17, 2026
bcf027b
fix(prior): Clarify renovate's support
epage Mar 17, 2026
2a5150a
fix(future): Clarify exclude list
epage Mar 17, 2026
22c6790
RFC 3923: Recommend against long duration
tmccombs Mar 31, 2026
b3ddfe8
style: Fix typo
epage Mar 17, 2026
9bb3f3f
fix(summary): Clarify and focus the summary
epage Mar 17, 2026
8ee33ca
fix(motivation): Clarify supply chain risk
epage Mar 17, 2026
f24682c
fix(motivation): Clarify supply chain is about automated scanners
epage Mar 17, 2026
0ef9cc2
fix(motivation): Clarify use case and users for non-automated scanners
epage Mar 17, 2026
2df26d7
fix(motivation): More succinct summary
epage Mar 17, 2026
0689bbf
fix(guide): Be less technical and more teaching focused
epage Mar 17, 2026
8e9a134
style(ref): Remove extra indentation
epage Mar 17, 2026
6380769
style(ref): Tweak formatting
epage Mar 17, 2026
52f9088
fix(ref): Clarify behavior section
epage Mar 17, 2026
8d2b13c
fix(alt): Clarify the do-nothing
epage Mar 17, 2026
63cc024
fix(alt): Clarify third-party
epage Mar 17, 2026
ff63c6e
fix(alt): Split location/name
epage Mar 17, 2026
b3ab608
fix(alt): Provide reason for not cooldown
epage Mar 17, 2026
0025ba2
style(alt): Clean up fallback/deny
epage Mar 17, 2026
eed6975
feat(alt): Talk about exclude lists
epage Mar 17, 2026
c730fea
style(alt): Cover exclude list after the replacements
epage Mar 17, 2026
dfa39eb
style: Adjust indentation
epage Mar 17, 2026
f60db7d
fix: Remove redundent text
epage Mar 17, 2026
085ed23
feat(unresolved): Add some questions
epage Mar 17, 2026
b3e4bb6
style(guide): Fix a typo
epage Apr 1, 2026
19878da
style(ref): Fix a typo
epage Apr 1, 2026
8d07c75
fix(guide): Make examples less ambiguous
epage Apr 1, 2026
3a1d23c
Merge pull request #1 from epage/tmccombs-cargo-min-publish-age
tmccombs Apr 2, 2026
c466280
fix(guide): Call out difference between cargo update/cargo add
epage Apr 3, 2026
670284b
feat(alt): Cover timestamp vs duration
epage Apr 3, 2026
29b5772
fix(rationale): Cover publish in name being redundant
epage Apr 3, 2026
90e3c7c
fix(alt): Capture some exclude discussion
epage Apr 3, 2026
d68798b
fix(alt): Expand on deny vs fallback
epage Apr 3, 2026
c875967
Correct typo in example
tmccombs Apr 11, 2026
cc4f7e0
Merge pull request #2 from epage/edits
tmccombs Apr 24, 2026
7976883
fix: quote 0 consistently as string value
weihanglo Apr 27, 2026
52940eb
fix: `cargo update --precise <yanked>` is stable
weihanglo Apr 25, 2026
eadef8b
fix(summary): deny semantics
weihanglo Apr 25, 2026
c6dda11
fix(guide): deny semantics
weihanglo Apr 25, 2026
64f02f5
fix(ref): deny semantics
weihanglo Apr 25, 2026
a32ca28
fix(rationale): deny semantics
weihanglo Apr 25, 2026
5bb52db
fix(future): deny semantics
weihanglo Apr 25, 2026
3270483
fix(unresolved): deny semantics
weihanglo Apr 27, 2026
5a47fdf
feat(future): add yanked alerting
weihanglo Apr 27, 2026
6b0ce8c
fix(future): `resolver.now` with offline concern
weihanglo Apr 27, 2026
bcbcb75
refactor(future): move exclude list to rationale
weihanglo Apr 27, 2026
1db9c74
refactor(future): move fallback option to rationale
weihanglo Apr 27, 2026
3696956
feat(unresolved): do we need resolver setting?
weihanglo Apr 28, 2026
8021085
fix(ref): mention source replacement briefly
weihanglo Apr 28, 2026
7311b7b
refactor(ref): remove duplicate as having in unresolved
weihanglo Apr 28, 2026
fcbe967
refactor(ref): make yanked-version like outstanding
weihanglo Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 327 additions & 0 deletions text/3923-cargo-min-publish-age.md
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.

The RFC doesn't clearly lay out the expected way to deal with urgent security updates.
From my reading of it, I would deal with those by using cargo update --precise or putting it in Cargo.toml, getting a warning, and then after having the new version in the lockfile, everything works normally.
But this would mean that when regenerating the lockfile (when update --precise was used instead of Cargo.toml), this security update would be lost silently, which sounds suboptimal. Having an exclude array like pnpm does not have this problem.
Can you add why you decided against package exclusion configuration and whether this threat of regenerating the lockfile after a security update is important?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

But this would mean that when regenerating the lockfile (when update --precise was used instead of Cargo.toml), this security update would be lost silently, which sounds suboptimal

If there is something you depend on, you should raise your version requirement.

Having an exclude array like pnpm does not have this problem.

But that is a heavy hammer. It doesn't record why it was excluded and you can easily forget to un-exclude when it is no longer applicable. With raising a version requirement, you automatically start using the feature again with the dependency.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Having an exclude array like pnpm does not have this problem.

I did include that in the Future Possibiliites, but I could probably expand on that more.

Copy link
Copy Markdown
Member

@weihanglo weihanglo Apr 27, 2026

Choose a reason for hiding this comment

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

This revision (6b0ce8c) switched to deny by default and moved fallback to future possibilities based on the conclusion from the 2026-04-07 Cargo team meeting. In this revision, the behavior is more well-understood since it is the same as the pre-existing yanked versions mechanism. On the implementation side the change would also be small enough to ship it faster, and incrementally work on future extensions.

View changes since the review

Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
- Feature Name: cargo_min_publish_age
Comment thread
clarfonthey marked this conversation as resolved.
- Start Date: 2026-02-23
- RFC PR: [rust-lang/rfcs#3923](https://github.com/rust-lang/rfcs/pull/3923)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

## Summary
[summary]: #summary

This proposal adds a new configuration option to cargo that specifies a minimum age for package
updates. When adding or updating a dependency, cargo won't use a version of that crate that
is newer than the minimum age, unless no possible version is older.
Comment thread
tmccombs marked this conversation as resolved.
Outdated
Comment thread
epage marked this conversation as resolved.
Outdated

## Motivation
[motivation]: #motivation

There are a couple of reasons why one may wish not to use the most recent version of a package.

One such reason is to mitigate the risk of [supply chain attacks](https://en.wikipedia.org/wiki/Supply_chain_attack). Often, supply chain
attacks are found fairly quickly after they are deployed. Thus, if the dependency isn't updated
immediately after a release, you can have some protection against a new release of a dependency
containing malicious code. In light of recent supply chain attacks on the NPM ecosystem,
there has been an increased interest in using automated tools to ensure that packages used
are older than some age. This creates a window of time between when a dependency is compromised
and when that release is used by your project. See for example the blog post
"[We should all be using dependency cooldowns](https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns)".
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Moving discussion from longer/less coherent thread: https://github.com/rust-lang/rfcs/pull/3923/changes#r2841703101

Rewording the arguments, I don't really think that this constitutes an empirical basis for the benefit of these delays. What would constitute a valid empirical basis is vulnerabilities that have been caught by this system after it's been implemented in other places. These should exist in practice since deployment of these mitigations is slow at best, and there should be cases where some people have the mitigations and some don't, and are thus vulnerable. But I haven't seen any so far.

The only argument listed is that the time between publication and mitigation is short, and so, if we delay deployment until a certain time after publication, then deployment won't happen before mitigation.

This really doesn't feel valid for one primary reason: people need to notice that there's a problem in order to mitigate it, and delaying mitigation also delays the time that people actually encounter an issue.

Like, my argument is that the vulnerabilities we should be worrying about are ones like the xz vulnerability, which fall cleanly into this class: they require an analysis of multiple moving parts and are only going to be noticed once someone actually has them running.

There are plenty of notable crates where an exploit would easily be noticed immediately and delaying dependency updates would catch them, in most cases. For example, if someone tried to release a malicious version of syn, we would definitely know almost immediately.

However, there are plenty of other situations where a vulnerability would not be noticed, because Rust's trait system lets you hide code in the most unsuspecting of places. Would you notice if one crate you used suddenly started relying on a particular hasher implementation that was promising but not as scrutinized, and then that hasher implementation was running malicious code? Would you notice if a reasonable-looking trait import also happened to silently change the behaviour of a method that looked fine?

The xz example is such a good example because it fits so well into so many examples. You don't have to be a bad actor hired by a nation-state to exploit a codebase; you just need to get your code in front of one tired maintainer who says "looks good to me" when you've snuck something suspicious in. Or, even just one compromised account: how many repos do you know are big enough to have several PRs per day, and how many of those do you scrutinize every PR?

Simply assuming that a timer starts ticking from release until someone notices a vulnerability is very naïve, because while that is definitely the case for obvious exploits like adding bitcoin_miner::run() to the top of a function, all it takes is one layer of indirection to hide that, and then it's not obvious at all.

There are definitely quick "hit-and-run" vulnerabilities that involve a compromised account that might be stopped by this, but so far we haven't seen any of those in the Rust ecosystem, and it seems like there are plenty of other low-hanging fruit with dependency updating that could be chosen instead of a simple time limit, especially considering how it can make security updates challenging to deploy.

There are a few other things discussed in other threads as ways of further improving the dependency-updating and dependency-locking systems, but here I wanted to specifically hone in on why I don't think that the data here creates a valid argument in this context.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There are plenty of notable crates where an exploit would easily be noticed immediately and delaying dependency updates would catch them, in most cases. For example, if someone tried to release a malicious version of syn, we would definitely know almost immediately.

However, there are plenty of other situations where a vulnerability would not be noticed, because Rust's trait system lets you hide code in the most unsuspecting of places.

I'm a bit confused by this. As we said in the other thread, this isn't meant to be an exhaustive solution but one part of improving the whole and one we can deploy rather cheaply / quick for a quick improvement while we continue to work towards the larger improvements.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This really doesn't feel valid for one primary reason: people need to notice that there's a problem in order to mitigate it, and delaying mitigation also delays the time that people actually encounter an issue.

FWIW, the core thesis is that the relevant parties here are security scanners, not early victims. In the Python ecosystem, for example, the overwhelming majority of malicious package reports comes from automated static analysis, not from users who have already been victimized.

(And therefore it's not that people need to notice a problem, it's that systems need to be in place to monitor for problems and people should generally wait for that monitoring rather than acting as ecosystem canaries themselves.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That was not clear from the article, at least, although I definitely was biased against the article despite the fact that you could not have more clearly stated that the problem itself is overhyped. That definitely changes the situation in a way that should be reflected more clearly in the RFC IMHO.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That was not clear from the article, at least, although I definitely was biased against the article despite the fact that you could not have more clearly stated that the problem itself is overhyped.

I'm open to suggestions on how to phrase or edit the blog post, I thought I had covered that with this paragraph:

Cooldowns enforce positive behavior from supply chain security vendors: vendors are still incentivized to discover and report attacks quickly, but are not as incentivized to emit volumes of blogspam about “critical” attacks on largely underfunded open source ecosystems.

(But I freely admit that the post is terse, and I'm always open to feedback on clarifying my language.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's also worth pointing out that many of these attacks have involved a developer's credentials to the repository and/or package registry getting compromised, and in such cases, the developer often eventually realizes their credentials have been compromised. But that isn't always immediate, and it can take time for them to realize what happened, restore their account, and yank the malicious release.

Comment thread
epage marked this conversation as resolved.
Outdated

Another reason to wish to delay using a new release, is because new versions can introduce new bugs. By only
using versions that have had some time to "mature", you can mitigate the risk of encountering those bugs a little.
Hopefully the bugs will be found before you start using the new version and thus could update to a version that fixes those bugs,
or lock your version, so you don't get the buggy version, if the bugs apply to you.
Comment thread
epage marked this conversation as resolved.
Outdated

Although cargo, of course, allows you to specify exact versions in `Cargo.toml` and has a lock file that can freeze the versions used,
that requires manually inspecting each version of each transitive dependency to confirm it complies with a policy of
using a version older than some age.
Comment thread
epage marked this conversation as resolved.
Outdated

As such, it would be useful to have an option to put a limit on commands like `cargo add` and `cargo update`
so that they can only use package releases that are older than some threshold.
Comment thread
epage marked this conversation as resolved.
Outdated
Comment thread
epage marked this conversation as resolved.
Outdated


## Guide-level explanation
Comment thread
epage marked this conversation as resolved.
[guide-level-explanation]: #guide-level-explanation

The `registry.global-min-publish-age` [configuration option][1][^1] for Cargo can be used to specify a minimum age for published versions to use.

When set, it contains a duration specified as an integer followed by a unit of "seconds", "minutes", "days", "weeks", or "months".
Comment thread
epage marked this conversation as resolved.
Outdated
If a new crate would be added with a command such as `cargo add` or `cargo update`, it will use a version with a publish
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

“If a new crate would be added” is confusing. Taken colloquially, it sounds like min-publish-age would only apply to new packages (i.e. cargo add or updating adding a new dependency) and not cargo updates to existing packages; but I suspect it actually means new versions of packages whether or not that package name was previously in the dependency graph.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This wording was removed in 0689bbf

time ("pubtime") before that is older than that duration, if possible.
Comment thread
tmccombs marked this conversation as resolved.
Outdated

The `resolver.incompatible-publish-age` configuration can also be used to control how `cargo` handles versions whose
publish time is newer than the min-publish-age. By default, it will try to use an older version, unless none is available
that also complies with the specified version constraint, or the `rust-version`. However by setting this to "allow"
it is possible to disable the min-publish-age checking.
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.

I'm trying to understand when I would want to set this. Is there perhaps an example we can provide of typical use-cases? Isn't setting this to allow the equivalent of removing the registry.global-min-publish-age configuration?

Or does registry.global-min-publish-age apply always (e.g., even with a lock file?) whereas resolver.incompatible-publish-age only applies when we're actively searching for new versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the question "why set to allow when you can set the time to 0?"

It would be good to clarify this. One controls time, the other behavior but both have a way to turn it off due td their design,

I'd say turning off the behavior is for tranisently turning it off for all registries rather than setting it for each registry.

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.

I think the common case for most users is that they only have one registry, right? In that case just commenting out or setting the time to zero feels better to me than complicating the configuration out of the gate with two options that interact.

(It seems like we can always add the extra configuration to temporarily opt out later).

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.

Added this to Unresolved Questions, though I think Mark has a point.

  • The complexity might be unnecessary if we don't extend the resolver config beyond "allow" and "deny".
  • Resolver behavior is not even a per-registry level config. If users want to permit a custom registry (which is trusted) but crates.io, they cannot use resolver.incompatible-publish-age. They still need to use per registry CARGO_REGISTRY_<name>_MIN_PUBLISH_AGE=0 to turn it off. It is rare though valid as a use case.

I am slightly towards removing resolver.incompatible-publish-age even semantically it is appealing to me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Link to commit?

For myself, I'm unconvinced about removing it. While most users only have one registry, we need to still support the multi-registry users. Uncommenting isn't accurate when doing things like running a "test latest versions" job in CI. It is also very handy to have a simple, universally applicable example in our documentation.

This also keeps the way we talk about all of these things consistent.

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.

Link to commit?

3696956


It is also possible to configure the `min-publish-age` per cargo registry. `registries.<name>.min-publish-age` sets
the minimum publish age for the `<name>` registry. And `registry.min-publish-age` sets it for the default registry
crates.io registry.

[1]: https://doc.rust-lang.org/cargo/reference/config.html
[^1]: As specified in `.cargo/config.toml` files

## Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

This RFC adds a few new configuration options to [cargo configuration](https://doc.rust-lang.org/cargo/reference/config.html).

### Added to [Configuration Format](https://doc.rust-lang.org/cargo/reference/config.html#configuration-format)

```toml
[resolver]
incompatible-publish-age = "fallback" # Specifies how resolver reacts to these

[registries.<name>]
min-publish-age = "..." # Override `registry.global-min-publish-age` for this registry

[registry]
min-publish-age = "0" # Override `registry.global-min-publish-age` for crates.io
global-min-publish-age = "0" # Minimum time span allowed for packages from this registry
```

### Added to [`[resolver]`](https://doc.rust-lang.org/cargo/reference/config.html#resolver)
#### `resolver.incompatible-publish-age`

* Type: String
* Default: `"fallback"`
* Environment: `CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE`


When resolving the version of a dependency to use, specify the behavior for versions with a `pubtime` (if present) that is incompatible with `registry.min-publish-age`. Values include:

* `allow`: treat pubtime-incompatible versions like any other version
* `fallback`: only consider pubtime-incompatible versions if no other version matched


If the value is `fallback`, then cargo will print a warning if no suitable version can be found and the resolver is forced to select a version that is newer
than allowed by the appropriate `min-publish-age` setting.


See the [resolver](https://doc.rust-lang.org/cargo/reference/resolver.html#rust-version) chapter for more details.
### Added to [`[registries]`](https://doc.rust-lang.org/cargo/reference/config.html#registries)
#### `registries.min-publish-age`

* Type: String
* Default: none
* Environment: `CARGO_REGISTRIES_<name>_MIN_PUBLISH_AGE`


Specifies the minimum timespan since a version's `pubtime` that it may be considered for `resolver.incompatible-publish-age` for packages from this registry. If not set, `registry.global-min-publish-age` will be used.

Will be ignored if the registry does not support this.

It supports the following values:

* An integer followed by “seconds”, “minutes”, “hours”, “days”, “weeks”, or “months”
* `"0"` to allow all packages


### Added to [`[registry]`](https://doc.rust-lang.org/cargo/reference/config.html#registry)
#### `registry.min-publish-age`

* Type: String
* Default: none
* Environment: `CARGO_REGISTRY_<name>_MIN_PUBLISH_AGE`


Specifies the minimum timespan since a version's `pubtime` that it may be considered for `resolver.incompatible-publish-age` for packages from crates.io not set, `registry.global-min-publish-age` will be used.

It supports the following values:

* An integer followed by “seconds”, “minutes”, “hours”, “days”, “weeks”, or “months”
* `"0"` to allow all packages


#### `registry.global-min-publish-age`

* Type: String
* Default: `"0"`
* Environment: `CARGO_GLOBAL_REGISTRY_<name>_MIN_PUBLISH_AGE`

Specifies the global minimum timespan since a version's `pubtime` that it may be considered for `resolver.incompatible-publish-age` for packages. If `min-publish-age` is not set for a specific registry using `registries.<name>.min-publish-age`, Cargo will use this minimum publish age.

It supports the following values:

* An integer followed by “seconds”, “minutes”, “hours”, “days”, “weeks”, or “months”
* `"0"` to allow all packages


### Behavior

In addition to what is specified above

* `min-publish-age` only apply to dependencies fetched from a registry, such as crates.io. They do not apply to git or path dependencies, in
part because there is not always an obvious publish time, or a way to find alternative versions.
Comment thread
epage marked this conversation as resolved.
Outdated
* At this time, if a specific version is explicitly specified in Cargo.toml, or on the command line, that has higher precedence than the publish time check,
and will be assumed to be valid. In the future it may be possible to change this behavior.
* `cargo add`
* If a version is not explicitly specified by the user and the package is fetched from a registry (not a path or git), `min-publish-age` options
will be respected.
* `cargo install`
* If a specific version is not specified by the user, respect `registries.min-publish-age` for the version of the crate itself,
as well as transitive dependencies when possible.
Copy link
Copy Markdown
Contributor

@epage epage Feb 23, 2026

Choose a reason for hiding this comment

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

MSRV resolver doesn't apply to cargo install, so I assume it shouldn't apply here

Suggested change
* `cargo install`
* If a specific version is not specified by the user, respect `registries.min-publish-age` for the version of the crate itself,
as well as transitive dependencies when possible.
* `cargo install`
* Unchanged to match the behavior of the MSRV-aware resolver

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added a suggestion

Granted, rust-lang/cargo#16694 might muddy the waters here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Personally, I think that having this apply to cargo install as a way of preventing installing an executable package that has been published very recently is also very useful. Although perhaps users may want a different policy for cargo install than for updating libraries?

There are also some significant differences between MSRV and this:

  • For MSRV, the minimum rust version comes from the package Manifest, whereas for the minimum publish age, it comes from Carog configuration, which may be set by the user themselves. It seems odd to me that setting global-min-publish-age in ~/.cargo/config.toml wouldn't apply to crates installed with cargo install and their dependencies.
  • For MSRV and cargo install, generally the user doesn't care if crates are compatible with the minimum version in the package they are installing, so much as that they are compatible with the version of rust they are using to compile it? Does the resolver at least prefer crates that are compatible with the version of rust that cargo is using?

That said, I do think it would be surprising for a .cargo/config.toml in the package itself to impact the resolver when running cargo install.

So maybe we should specify that configuration from $CARGO_HOME/config.toml is used for checking minimum publish age in cargo install but not configuration from the package being installed itself?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Personally, I think that having this apply to cargo install as a way of preventing installing an executable package that has been published very recently is also very useful. Although perhaps users may want a different policy for cargo install than for updating libraries?

We've already started down the path of documenting resolver as not applying to cargo install and I think it would be a problem to be inconsistent on that.

We can separately look into an install.resolver table.

Or we could look into more general fixes for cargo install so that the installed package is also checksumed, like installing dependencies.

That said, I do think it would be surprising for a .cargo/config.toml in the package itself to impact the resolver when running cargo install.

cargo install re-reloads the config from only CARGO_HOME

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.

I removed the mention entirely in 64f02f5. cargo install is not relevant anymore as pubtime-incompatible versions is just another yanked versions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Don't we still need to talk about cargo install because Cargo.lock is not used by default?

* `cargo update`
* Unless `--precise` is used to specify a specific version, any crates updated from the registry will only consider versions published
before the time specified by the appropriate `min-publish-age` option. If `--precise` is used, that version will be used, even if it
newer than the policy would otherwise allow (although in the future, there may be an option to deny that).
* If the version of a crate in the lockfile is already newer than `min-publish-age`, then `cargo update` will not update that crate, nor will
it downgrade to an older version. It will leave the version as it is.
* When a lockfile is generated, as with `cargo generate-lockfile` or other commands such as `cargo build` that can do so, then versions will be
selected that comply with the `min-publish-age` policy, if possible.
* If the only version of a crate that satisfies the `min-publish-age` constraint is a yanked version, it will behave as if no versions satisfied the
`min-publish-age` constraint. In other words, yanked versions has higher priority than the `min-publish-age` configuration.

## Drawbacks
[drawbacks]: #drawbacks
Comment on lines +258 to +259
Copy link
Copy Markdown
Contributor

@epage epage Apr 28, 2026

Choose a reason for hiding this comment

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

The [resolver] configs support a disjoint set of values which could be confusing

View changes since the review


The biggest drawback is that if this is widely used, it could potentially lead to it taking longer for problems to be discovered after a version is published.
However, most likely, there will be a spread of values used, depending on risk tolerance, and hopefully the result is actually that there will be a more gradual rollout in most
cases.

## Rationale and Alternatives
[rationale-and-alternatives]: #rationale-and-alternatives
Comment on lines +269 to +270
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like there is content from the issue that isn't here, for example...

we should talk about the reason we are going with fallback and not deny is as a way to allow people to override this when there is a fix or feature they need. We've been experimenting with ways of doing this with cargo update --precise but if a newer transitive dependency is needed, you can't do that.

I think the thread also covered that fallback for this doesn't have the same problems as incompatible-rust-version because that suffers from a lack of data while crates.io exhaustively sets pubtime and so can any other registry that supports it.

We also had conversions on naming that isn't here. e.g. publish is intentionally in the name to help connect it to this only working on some deps.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, I think we talked about the use case for exclude being lessened because

  • per-registry support
  • ability to override

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I attempted to address this in 51e0a7c

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.

we should talk about the reason we are going with fallback and not deny is as a way to allow people to override this when there is a fix or feature they need. We've been experimenting with ways of doing this with cargo update --precise but if a newer transitive dependency is needed, you can't do that.

Switched to deny by default and moved fallback to future possibilities based on the conclusion from the 2026-04-07 Cargo team meeting. Not sure if this review comment is still relevant.


### Why not leave this to third party tools?

There are already some third party tools that fulfill this functionality to some degree. For example, dependabot and renovate can
be used for updating Cargo.toml and Cargo.lock, and both support some form of minimum publish age. And the cargo-cooldown project provides
an alternative to `cargo update` that respects a minimum publish age.

However, these tools only work for updating and adding dependencies outside of cargo itself, they do not
have any impact on explicitly run built-in cargo commands such as `cargo update` and `cargo add`.
Having built-in support makes it easier to enforce a minimum publish age policy.
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.

It is easier, yes, but more importantly having this in Cargo is the only way to have it actually be secure. As soon as you run cargo build after a malicious package has been resolved, the build is compromised.
This can't be anywhere but in Cargo's resolver to achieve the security benefits.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think that is only true if you don't have a Cargo.lock file, or your Cargo.lock file is missing something from your Cargo.toml file.

But I did try to mention that in d78605c



### Configuration Locations

The locations and names of the configuration options in this proposal were chosen to be
consistent with existing Cargo options, as described in [Related Options in Cargo](#related-options).

## Prior Art
Copy link
Copy Markdown
Contributor

@clarfonthey clarfonthey Feb 23, 2026

Choose a reason for hiding this comment

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

One thing that's particularly relevant IMHO is how npm treats updating package.json and package.lock, which is different from how cargo updates Cargo.lock (and not Cargo.toml, usually).

Right now, Cargo.lock is the brute-force solution of ensuring deterministic builds: every single dependency is locked to an exact version, no exceptions, until the lockfile is updated. Except, there really isn't a way to update the lockfile except updating everything, or updating a single crate. And there isn't really much control over the lockfile except a few key configuration options like MSRV or choosing the minimum valid option.

There are plenty of tools that other projects use to help mitigate some dependency issues that could definitely be helpful if they were part of Cargo. For example, the rust-lang repo has a large list of all crates that can ever be dependencies, ever, to ensure that all crates are at minimum vetted to be from trusted sources. And other tools like cargo vet exist to manage this trust process in a more
configurable way.

It would be nice if you could have an intermediary between Cargo.toml and Cargo.lock that let you add a few more limits on dependencies, like:

  • Only allow certain transitive dependencies (of specified versions) and warn or error otherwise
  • Distinguishing between "minimum" versions and "preferred" versions: for example, your code might work with version 1.0.0 of a crate but you've vetted version 1.1.2 and you haven't checked newer versions yet (NPM does this by just updating package.json directly, lower versions be damned, and this could be a reasonable option for Cargo too potentially for binary or internal library crates)

It's not clear whether all of these changes should be part of Cargo, but at least some of them could be, and we should make it easier to understand how things are being updated to avoid cases where someone accidentally updates to a vulnerable version. Minimum release time could still be one of the constraints added to packages under this system, but it wouldn't be the only one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah, I think minimum release ages are only a (small) part of a larger toolbox here. I think the larger toolbox includes things like trusted packages/reviews as well as capability analysis (e.g. foo v1.0.0 could only do local I/O, but foo v1.0.1 can do networked I/O and thus merits more attention).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minimum release time could still be one of the constraints added to packages under this system, but it wouldn't be the only one.

If thats the case, I'm not sure I understand the concern. Yes, there are more things that we can be doing. Is it that the system for these rules isn't generalized like rust-lang/cargo#7193 ?

Only allow certain transitive dependencies (of specified versions) and warn or error otherwise

We have several different issues exploring different angles on this:

Distinguishing between "minimum" versions and "preferred" versions: for example, your code might work with version 1.0.0 of a crate but you've vetted version 1.1.2 and you haven't checked newer versions yet (NPM does this by just updating package.json directly, lower versions be damned, and this could be a reasonable option for Cargo too potentially for binary or internal library crates)

In my opinion, we should work to having vetting minimum dependencies. rust-lang/cargo#5657 includes two different ways to resolve to it to check it but I have concerns about each approach and I wonder if we should instead be more aggressive with Cargo.toml, like with rust-lang/cargo#15583

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(I almost deleted my last two replies so we could focus on the bigger topic in the first but didn't want to lose track of the links)

[prior-art]: #prior-art

Comment thread
tmccombs marked this conversation as resolved.
### pnpm

`minimumReleaseAge` is a configuration option which takes a number of minutes as an argument. It then won't update or install releases that were released less than that many minutes ago. This also applies to transitive dependencies.

`minimumReleaseAgeExclude` is an array of package names, or package patterns for which the `minimumReleaseAge` does not apply, and the newest applicable release is always used. It also allows specifying specific versions to be allowed.

Both configuration options can be set in global config, a project-specific config file, or with environment variables (for a specific invocation).

### yarn

Has a configuration setting that can be used in `.yarnrc.yml` named `npmMinimalAgeGate` that can be used to set the minimum age for installed package releases. It looks like it allows specifying units, as the example for three days is `3d`, however I haven't found any definitive description of the syntax.

As far as I can tell, there is no way to provide exclusions to this rule, or different times for different packages or repositories.

### uv

The `--exclude-newer` option can be used to set a timestamp (using RFC 3339 format), or a duration (either "friendly" or ISO 8601 format)
and won't use releases that happened after that timestamp. There is also an `--exclude-newer-package` option, which allows overriding the `exclude-newer` time for individual packages.

Both of these settings can also be used in the `uv` configuration file (`pyproject.toml`).

### pip

Pip has an `--uploaded-prior-to` option that only uses versions that were uploaded prior to an ISO 8601 timestamp. Can also be controlled with the `PIP_UPLOADED_PRIOR_TO`
environment variable.

### dependabot

The `cooldown` option provides a number of settings, including:

- `default-days` – Default minimum age of release, in days
- `semver-major-days`, `semver-minor-days`, `smever-patch-days` -- Override the cooldown/minimum-release-age based on what kind of release it is.
- `include` / `exclude` – a list of packages to include/exclude in the "cooldown". Supports wildcards. `exclude` has higher priority than `include`.

"Security" updates bypass the `cooldown` settings.

Dependabot doesn't support cooldown for all package managers.

This is specified in the dependabot configuration file.

### renovate

The options below can be provided in global, or project-specific configuration files, as a CLI option, or as an environment variable.

`minimumReleaseAge` specifies a duration which all updates must be older than for renovate to create an update. It looks like the duration specification uses units (ex. "3 days"), however, again I can't find a precise specification for the syntax.

I think it is possible to create separate rules with different `minimumReleaseAge` configurations.
Comment thread
epage marked this conversation as resolved.
Outdated

"Security" updates bypass the minimum release age checks.

### deno

Deno supports a [configuration option](https://deno.com/blog/v2.6#controlling-dependency-stability) for `minimumDependencyAge` in the configuration file, or
`--minimum-dependency-age` on the CLI. It supports an ISO-8601 duration, RFC 3339 timestamp, or an integer of minutes.

### cargo-cooldown

There is an existing experimental third-party crate that provides a plugin for enforcing a cooldown: [https://github.com/dertin/cargo-cooldown]

### Related Options in Cargo
[related-options]: #related-options-in-cargo

Some precedents in Cargo

[`cache.auto-clean-frequency`](https://doc.rust-lang.org/cargo/reference/config.html#cacheauto-clean-frequency)

* "never" — Never deletes old files.

* "always" — Checks to delete old files every time Cargo runs.

* An integer followed by “seconds”, “minutes”, “hours”, “days”, “weeks”, or “months”


[`resolver.incompatible-rust-versions`](https://doc.rust-lang.org/cargo/reference/config.html#resolverincompatible-rust-versions)

* Controls behavior in relation to your [`package.rust-version`](https://doc.rust-lang.org/cargo/reference/rust-version.html) and those set by potential dependendencies

* Values:

* allow: treat rust-version-incompatible versions like any other version
* fallback: only consider rust-version-incompatible versions if no other version matched


[`package.resolver`](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) is only a version number. When adding `incompatible-rust-version`, we intentionally deferred anything being done in manifests.

[`[registry]`](https://doc.rust-lang.org/cargo/reference/config.html#registry)

* Set default registry

* Sets credential providers for all registries

* Sets crates.io values


[`[registries]`](https://doc.rust-lang.org/cargo/reference/config.html#registries)

* Sets registry specific values


`yanked`: can't do new resolves to it but left in if already there. Unstable support to force it with `--precise` but that doesn't apply recursively.

pre-release: requires opt-in through version requirement. Unstable support to force it with `--precise` but that doesn't apply recursively.

We use the term `publish` and not `release`


## Unresolved Questions
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.

I'm wondering how/if we expect this to interact with alternative date sources (some future, some partially already visible today). For example, if we implement something like TUF or a mirror of crates.io, the "age" here might want to be relative to the index snapshot we're using, rather than to absolute local time. Maybe there's a question worth adding about exploring what "now" is?

As a concrete example of that, mirrors today can implement a version of this RFC by delaying imports of new versions by the period into the index they expose, rather than implementing that in Cargo. Fully replacing that with this RFC would require that the now timestamp is kept deterministic over time without a lock file checked in, which could be done if the index 'checkout' itself had the now timestamp stored when it was fetched. That would also help users opt-in more eagerly without needing that controlled at the mirror level, while still staying behind for most versions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should include that as an alternative.

Note that pubtime is already stabilized in the registry and so for a TUF mirror of crates.io, they would have to preserve pubtime or else they would be invalid.

If we don't, for the unaware, we should call out that this relies on the already stable pubtime field.

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.

Putting it more concisely: the RFC as written I think assuming the relative age is relative to wall-clock now. I think registries should be able to tell Cargo when now is.

For example, in offline mode, I think cargo ought to use the last time it fetched from the network, not continue ratcheting forward. (Or at least we should consider that...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you help me understand what the use case is for this?

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.

If I'm on a flight, I don't really want Cargo suddenly realizing I have new crates "available" because they have matured in my index copy: I want to keep my index static and without the last N days as of when it was fetched. I think Cargo partially supports this with offline mode, but not completely: even if some newer crate is available locally, I probably don't want to start using it, because without an up to date index I can't know if it was yanked in the period of time since I fetched the index.

My understanding is that our model is that automated scanners take 24-72 hours to pick up bad code in lots of cases. If I believe that I want to use the index as of -72 hours from when I fetched it, not -72 hours from whatever my local time is.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think anything like this could potentially be added later and feel like it should then be best left to a future possibility to keep this more narrowly scoped (for faster resolving of discussion and implementation).

There also isn't a coherent snapshot of the index that we are operating on because of the sparse index design. We only update the entries we request and not all of those that have been downloaded. We'd have to keep per-entry "last update" times and make this relative to those. This is also adding a lot of writes even when the cache is unchanged which seems like it could affect performance and health of the drive (granted, target-dirs are even worse there) for something that won't be used in most cases.

I'm also not aware of any prior art that tracks it this carefully. Some allow the user to set a date for comparison. cargo generate-lockfile --publish-time will limit packages according to a specific date. As a future possibility, we could have a config for people to override "now".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

naming:

  • should we leave room for adding absolute times?
  • do we still need publish if its in the registry table? Probably yes to make it clear what is looked at and in shorthand communication.

Copy link
Copy Markdown
Member

@weihanglo weihanglo Apr 6, 2026

Choose a reason for hiding this comment

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

do we still need publish if its in the registry table? Probably yes to make it clear what is looked at and in shorthand communication.

Yes otherwise it could also be yank time (though crates.io doesn;t expose that yet)

should we leave room for adding absolute times?

The only use case I can imagine is rust-lang/cargo#5221, which we have the unstable cargo generate-lockfile --publish-time to provide a time machine. However, cargo generate-lockfile --publish-time is not perfect because it updates the entire lockfile, and people may have some deps wanted to be pinned.

If this doesn't have room for absolute times, we might need something like cargo update --publish-time to help people selectively upgrade/downgrade specific packages. IMHO that would be a bad idea. I'd like to see all CLI flag can be specified in Cargo config rather than one-off flag in only CLI.

[unresolved-questions]: #unresolved-questions

* Should "deny" be an allowed value for `resolver.incompatible-publish-age`? And if so, how does that behave? What is the error message? Is it overridden
Comment thread
epage marked this conversation as resolved.
Outdated
by a version specified in `Cargo.lock` or with the `--precise` flag?
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.

And how would it interact with important urgent security updates?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I've removed this from open questions, and left it as just a Future possibility for now.

* Would it be better to have `registry.min-publish-age` be the global setting, and `registries.crates-io.min-publish-age` be the setting for the crates.io registry?
The current proposal is based on precedent of "credential-provider" and "global-credential-provider", but perhaps we shouldn't follow that precedent?
Comment on lines +484 to +485
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reading the RFC I found myself wondering what "global-" meant here, since in the rest of Cargo config docs "global" refers to user settings ("global cache", "global git configuration", "globally change profile settings"). So I don't think the precedent is great?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unfortunately, there is already precedence that [registry] is for crates.io and we have already set the precedence that to use a global- prefix to disambiguate it.

* How do we make it clear when things are held back?
* The "locking" message for [Cargo time machine (generate lock files based on old registry state) #5221](https://github.com/rust-lang/cargo/issues/5221) lists one time but the time here is dependent on where any given package is from
* We list MSRVs for unselected packages, should we also list publish times? I'm assuming that should be in local time
* Locking message for [Cargo time machine (generate lock files based on old registry state) #5221](https://github.com/rust-lang/cargo/issues/5221) is in UTC time, see [Tracking Issue for _lockfile-publish-time_ #16271](https://github.com/rust-lang/cargo/issues/16271), when relative time differences likely make local time more relevant
* Implementation wise, will there be much complexity in getting per registry information into `VersionPreferences` and using it?
* `fallback` precedence between this and `incompatible-rust-version`?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

imo unspecified though I would put incompatible-rust-version at a higher precedence in the implementation so that people are more likely to have successful builds.


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.

Another question worth pointing out: "Are we comfortable making the security guarantees that the build remains secure when a not-yet-of-age malicious update has been released?". I think the answer here ought to be yes, but that's a decision that should be made deliberately and knowingly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I worry when "making [a] security guarantee" on whether we are actually committing to that level (ie a report needs to be issued and a hotfix released).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Any such guarantee would need to be predicated on some conditions, including:

  • The build was possible before the malicious release on the target platform and compiler version. I.E. there are no cases that require a fallback (a "deny" policy could potentially prevent a compromised build if that isn't the case, but that is currently put off as a future possibility)
  • The malicious release happened more recently than the minimum publish age.
  • Either none of the versions needed for a secure build have been yanked OR there a lock file is used.

## Future Possibilities
[future-possibilities]: #future-possibilities

- Support "deny" for `resolver.incompatible-publish-age`.
- This is initially excluded, because it isn't clear how this should behave with respect to versions already in Cargo.lock, or use with the `--precise` flag.
- Add a way to specify that the minimum age doesn't apply to certain packages