Repository navigation
aes: feature flags are not additive #245
Description
The aes crate has two feature flags, both of which remove functionality:
Lines 29 to 31 in f68ad0d
| [features] | |
| compact = [] # Reduce code size at the cost of slower performance | |
| force-soft = [] # Disable support for AES hardware intrinsics |
This is an incorrect use of Cargo feature flags. From my comments at rwf2/cookie-rs#176 (comment), which focus on force-soft:
This seems like an ill-fitted, perhaps incorrect use of features. Features should only be used to add capabilities, but
force-softremoves the ability to make use of AES-NI. The consequence is that a consumer ofaeshas little control over whether AES-NI is actually used: all it takes is a single dependency, direct or indirect, enablingforce-softto disable AES-NI completely, without recourse.I would suggest what I would consider to be the "correct" approach: always enable AES-NI if it is known to statically exist, and otherwise, add a feature that adds the ability to dynamically detect its existence, which of course becomes a no-op if the first condition is met.
Somewhere in the codebase, you already have 2 versions of the code. Let's call them
aes::niandaes::soft.Effectively, we want this:
fn aes() { #[cfg(have_aesni)] return aes::ni(); #[cfg(feature(autodetect)] if detect(aes-ni) { return aes::ni() } aes::soft() }This structure makes it clear that
autodetectis additive; it adds the ability to dynamically detect AES-NI and removes nothing otherwise; there is nonot(feature(autodetect)). This is the only fully correct use of features. If the code that requires1.49is restricted todetect, then MSRV can remain at 1.41 as long asautodetectis not enabled.
The same reasoning applies to the compact feature. If it must exist at all, a non-feature --cfg aes_compact is the correct approach. This restores control of expected performance to the top-level binary as opposed to any crate in the transitive closure of dependencies.
See also the full comment thread in rwf2/cookie-rs#176.
Activity
tarcieri commentedon May 10, 2021
This would be a breaking change, so if we were to try to implement something like this, it would be in
aesv0.8 at the earliest.I'll let @newpavlov speak to the rationale for
force-softas I switched my original implementation in #208 to that from anautodetectfeature at his request.Regarding
compact, that was added in #195 as asemi_fixslicefeature which I later renamed tocompact. I think it's valuable for embedded users, but also tricky to expose.tarcieri commentedon May 12, 2021
Another dimension to this problem would be potentially adding support for compact round keys in addition to semi-fixslicing: #191
Should that be gated under something like
compact? Separate?tarcieri commentedon May 13, 2021
Regarding
force-soft, I'm wondering if perhaps instead anintrinsicsfeature might be a better way to go.This could have the following behavior:
intrinsics: soft backendintrinsics, withouttarget_feature(s): autodetect CPU featuresintrinsics, withtarget_feature: intrinsics onlyintrinsics, withtarget_feature: still soft backend[-]`aes` feature flags are not additive [/-][+]aes: feature flags are not additive [/+]newpavlov commentedon May 20, 2021
I do not support this interpretation. In my opinion features MUST be additive only in the sense of public API and MUST NOT cause compilation errors if several features are enabled simultaneously. Meanwhile adding the under the hood functionality as in our case at best is SHOULD (yeah, I see that the quote uses "should", but I am not sure if it was done with the RFC in mind).
As mentioned in the IRLO thread, unfortunately, Cargo does not have proper tools to handle such use-cases (i.e. mutually exclusive features and features which must be toggled only by an end app, not libraries), so we have to work with what we got. In other words, it boils down to matter of defaults. Most users want for AES-NI intrinsics to be used if CPU supports them, after all
force-softuse cases are quite rare.Yes, we could state that
intrinsicsfeatures should not be enabled by library crates outside dev builds, but in my experience this approach not only makes life significantly harder for crate users (who have to bother configuration and may have to discover that for best performance they have to enable a feature of a crate buried deep in their dependency tree). Also with high probability there will be crates which enable such feature unconditionally, "poisoning" all projects dependent on it and thus making it virtually impossible switching to the software implementation for any sizable enough project. Same goes for making it a default feature, most users will not bother withdefault-features = false.To summarize: I think the current approach is the most practical one at the moment and should not be changed until Cargo will improve support of such use cases.
SergioBenitez commentedon Jun 4, 2021
Cargo does have such a feature via non-
featurecfgs. Seeing as these functionality changes should only be reasonably enabled by end-applications, non-featurecfgs seem like a perfect fit.I'm a bit confused. The primary feature in question is
force-softwhich disables the use of AES-NI intrinsics, but this block of text seems to be arguing as if the reality was the inverse. There is nointrinsicsfeature; this is a proposal by @tarcieri to ameliorate the concern I am raising. Can you clarify?tarcieri commentedon Jun 4, 2021
Between the AES-NI backend, ARMv8 backend, and 32-bit and 64-bit fixsliced backends, and trying to support various combinations of software-only and autodetected intrinsics+software-only fallback, the
#[cfg(...)]gating, even withcfg-if, has gotten quite convoluted.I think it might be worth trying to PR using an additive
intrinsicsfeature versus a subtractiveforce-softfeature just to see whether it's an improvement, and if so, how much.SergioBenitez commentedon Jun 4, 2021
I'm not sure I follow why there should be a difference. You'll need a
#[cfg(..)]whether you use afeaturecfgor a "regular"cfg.Yes, this would assuage my concern. What kind of improvement are you referring to?
newpavlov commentedon Jun 11, 2021
The cited text is about the proposed
intrinsicsfeature and why in my opinion it will not be a good solution in practice.It could work in theory, but it has the same problems with discoverability. Also enabling it is a bit annoying with the current tooling (IIUC you either have to edit
.cargo/configor enable it usingRUSTFLAGS). But sinceforce-softuses are quite rare, I think it may work. At the very least, I prefer this approach to theintrinsicsfeature. Though I am still not sure if it's worth to remove theforce-softfeature, since it does not create any problems in practice (unlike mutually exclusive features used in some crates).Ideally we would declare such "non-features" in a crate's
Cargo.tomland maybe somehow indicate mutually exclusive combinations, socargowould be able to query all possible configuration options for a project's dependency tree and you would be able to enable them via itsCargo.toml. Do you know if there is an RFC for something similar? If not, I think it may be worth to draft it.P.S.: BTW
force-softdoes enable capability of building theaescrate on pre-1.49 toolchains, so even from the theoretic point of view the situation is not so clear-cut. ;)SergioBenitez commentedon Jun 12, 2021
I've reinterpreted your comments in this light and offer my commentary below.
The idea would be that
intrinsicsis enabled by default and is safe to use in libraries given the fall-back to dynamic feature detection and, failing that, the software implementation. This makes "best performance" the default. Also note that the present conditions run directly against your argument: as it stands, a single dependency can make it irreversibly impossible for any crate in the graph to benefit from AES-NI.The presence of a non-
featurecfgwould make this a non-issue, making it possible for an end-user who unconditionally desires a software implementation to request it and receive it. Given the rarity of such users and likelihood that they are already operating under environments with non-traditional requirements, the trade-off in extra effort to enable such acfgseems more than worth it.You can do this yourself using
compile_error!. It is always incorrect to error if twofeaturecfgs are enabled, but doing the same for non-featurecfgs is totally okay:Sure, but using an older version of the crate accomplishes the same thing without the impossible to surmount downsides. Further, if enough crates do use the feature, then eventually no crate with any significant dependencies will benefit from AES-NI.
newpavlov commentedon Jun 12, 2021
You do not address the
default-features = falseissue of theintrinsicsfeature approach. In the next paragraph you switch to thecfg-based approach.Yes, but none of the higher-level crates in this org enable this feature by default. And there is a very little incentive to do so (unlike with the
intrinsicsfeature approach), since as you correctly note we want performance (i.e. AES-NI autodetection) enabled by default. Thus my statement about practicality of the current approach.As I've said in the previous comment, I do find the
cfg-based approach viable and we probably should consider switching to it in the next breaking release. But I am on the fence about its worth with the current state of tooling (especially considering that we would have to migrate several other crates to this approach as well), so I probably will leave this decision to @tarcieri.You are missing the point about discoverability and convenience of the declarative approach. The
aescrate can be buried quite deep in a dependency tree and I am quite confident that we do not want to introduce two config options and emit a compile-time error if one of options does not get enabled. While such compilation error would help with discoverability, it will be really annoying.No, the older
aesversions are not compatible with the up-to-date version of theciphercrate and thus incompatible with recent versions of higher-level crates.SergioBenitez commentedon Jun 12, 2021
I am not proposing one approach xor the other, I'm proposing both.
intrinsicsenabled by default with a non-featurecfgthat can overwrite it. Or, alternatively, just the latter.It's not about crates in this org, it's about the crates in an arbitrary dependency graph. For instance, @tarcieri enabled
force-softin a PR to thecookiecrate to maintain MSRV.cookiehas 84 dependents, among themreqwest,rocket,actix, andservo. Effectively every crate dealing with HTTP in any way eventually depends oncookie. Every single one of these crates would have been unable to make use of AES-NI inaeswithout recourse if I had accepted the PR.No, I don't think I am. Features and
cfgs should be called out somewhere besidesCargo.toml: features declared inCargo.tomlmay or may not be public, so it is not a reliable documentation source. Unless you document public features andcfgs, reading aCargo.tomlonly gives you an idea of the superset of features. Furthermore,Cargo.tomlcan obviously contain comments indicating the presence ofcfgs, if for some reason you wantCargo.tomlto be a canonical reference. I assumed all of this was understood. My suggestion to usecompile_error!()was as a matter of UX: you get immediate, actionable feedback if you configure things incorrectly, not type errors that have no meaning to the developer.Or perhaps I'm misunderstanding you completely. Why would a deep dependency, or any dependency at all, use non-
featurecfgs? I also don't understand which options you think should be mutually exclusive. Nothing that we've stated thus far would seem to implicate such a requirement.I'm not sure how this is relevant? You'd clearly need to match up your dependencies, but this is orthogonal to the issue at hand.
SergioBenitez commentedon Jul 28, 2021
Checking in. Any chance of moving forward?
tarcieri commentedon Jul 29, 2021
I'm personally for it in general, but probably not for making another breaking release for awhile.
Just for refactoring the internals I'd like to go to 1.51 and use const generics (as I've done in the nightly-only ARMv8 backend), but that's probably a bit of an aggressive version bump in the interim.