Skip to content

Fix assertion failure when a package constrains itself#230

Open
dralley wants to merge 1 commit into
prefix-dev:mainfrom
dralley:constrains-itself
Open

Fix assertion failure when a package constrains itself#230
dralley wants to merge 1 commit into
prefix-dev:mainfrom
dralley:constrains-itself

Conversation

@dralley

@dralley dralley commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

When a package both provides and conflicts with the same capability (example: in CentOS Stream, "centos-stream-release" provides "system-release" and also conflicts with "system-release"), the constrains clause would try to forbid the package from coexisting with itself. This caused WatchedLiterals::constrains to panic with "both literals cannot be false" because both watched literals referred to the same variable.

Skip self-referential constrains entries: a package that is already in the solution obviously cannot constrain itself out of it.

Assisted-By: Claude Opus 4.6

@dralley

dralley commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

centos-stream-release details: https://centos.pkgs.org/10-stream/centos-baseos-x86_64/centos-stream-release-10.0-21.el10.noarch.rpm.html

Provides: system-release = 10.0-21.el10
Conflicts: system-release

This is admittedly a strange thing to do, but here was the rationale: https://gitlab.com/redhat/centos-stream/rpms/centos-stream-release/-/commit/ce231c10b48cb4ddde860bfc9d6f79c76ba92e0a

I believe the semantics here are basically "if this package is installed, you must not install another provider, and if another provider is installed, you cannot install this package".

@dralley

dralley commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Let me know the ideal way to provide tests, unless you want to do it

@baszalmstra baszalmstra left a comment

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.

The constraint introduces a tautology. I think a more technically correct approach would be to (not crash and) block the package from being installable using an assertion. This would be the same as when this is done through a transitive dependency (e.g. A depending on B, which constrains A). I don't think skipping the clause is the right approach. But maybe I misunderstand the use-case?

@dralley

dralley commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

So, it's a package that is installed and can be updated (on my CentOS Stream dev container it's at release 21)

[root@473fed33ed83 src]# sudo dnf info centos-stream-release
Last metadata expiration check: 0:00:15 ago on Wed 03 Jun 2026 02:36:52 AM UTC.
Installed Packages
Name : centos-stream-release
Version : 10.0
Release : 21.el10
Architecture : noarch
Size : 38 k
Source : centos-stream-release-10.0-21.el10.src.rpm
Repository : @System
Summary : CentOS Stream release files
URL : https://centos.org
License : GPL-2.0-or-later
Description : CentOS Stream release files.

I haven't tracked down exactly what libsolv is doing to handle this scenario, but libsolv / DNF must be handling it somehow. Probably it's doing something similar.

@dralley dralley force-pushed the constrains-itself branch 2 times, most recently from ab34b25 to c16fc12 Compare June 5, 2026 02:37
@dralley

dralley commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Ah, so here's some libsolv documentation:

https://github.com/openSUSE/libsolv/blob/2f58c6f86edd978d6bdbd87dce9c85388e9b9dcc/doc/libsolv-pool.txt?plain=1#L207C1-L210C50

https://github.com/openSUSE/libsolv/blob/2f58c6f86edd978d6bdbd87dce9c85388e9b9dcc/doc/libsolv-bindings.txt?plain=1#L345-L348

https://github.com/openSUSE/libsolv/blob/2f58c6f86edd978d6bdbd87dce9c85388e9b9dcc/src/rules.c#L985-L993

So, libsolv does indeed do basically the same thing as this patch, but it's behind a solver configuration flag, because the desired behavior varies by ecosystem. Would you want to do something similar (or is there already a mechanism for solver config options)?

Unrelated: looks like the multiversion code is right there as well. And that might need a similar solution as well

@baszalmstra

Copy link
Copy Markdown
Contributor

Is that then something we should not just solve in the dependency provider itself? Simply dont return self-referential constraints?

We should still solve the crash of course, but I think we should backtrack in that case.

@dralley

dralley commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

How would that work exactly? The "conflicts" is on any provider of system-release (of which there are multiple different versions of centos-stream-release and also potentially other packages provided by forks, if I'm reading that gitlab commit message correctly) - it's not solely pointing at itself, so I don't think you can just omit the constraint entirely. But I'm not sure how you'd construct a version set that constrains "everyone except me". If it could be done, it'd probably be kinda hacky I think.

@dralley dralley force-pushed the constrains-itself branch from c16fc12 to 6b790f3 Compare June 5, 2026 16:36
@baszalmstra

Copy link
Copy Markdown
Contributor

Conceptually the DependencyProvider provides the ecosystem implementation. Whether to allow self referential constraints or not seems like an ecosystem specific implementation, so I think this should be part of that.

We can either model this as a seperate function to enable disable globally, we can allow this per nameid when returning candidates, or even per solvable when returning dependency information, or even per versionset with a new function.

WDYT?

@dralley

dralley commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

as a seperate function to enable disable globally

You mean a trait method on DependencyProvider? I suppose, but it's a bit of an awkward way to do configuration and adds a breaking change (if that's what you mean, I might also be misunderstanding). edit: not really a breaking change because, like my suggestion, it could have a default impl

If I can suggest a tweak on that, going w/ the SolverConfig idea, what if the DependencyProvider had a trait method to produce a default SolverConfig with whichever ecosystem-specific parameters it needed, and then Solver could adopt that when calling Solver::new(provider) instead of exclusively using the global set of defaults.

And the DependencyProvider trait could easily provide a default implementation of that, just use SolverConfig::default().

(if that was the case I would probably drop that 2nd commit w/ the activity params change, it wouldn't make much sense anymore).

we can allow this per nameid when returning candidates

That could probably work. Do you prefer that over option 1 (+tweaks)? I could spit out both for comparison purposes.

or even per solvable when returning dependency information, or even per versionset with a new function.

I think that's probably overkill. At least at the moment I have no example of a versioned self-conflicts, only this example of a conflict over a global virtual provide.

@dralley dralley force-pushed the constrains-itself branch 2 times, most recently from 95a411e to 829c580 Compare June 5, 2026 19:49
@dralley

dralley commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@baszalmstra Ok, this passes locally (the C++ side is broken of course). Once you give me a final design direction I'll clean it up.

The multi-version change ended up being a very similar implementation so I include that commit as well, but I can split that off into a different PR if you want.

@baszalmstra

Copy link
Copy Markdown
Contributor

I'm still a bit on the fence; please help me understand this better. I could not understand how self-referential constraints make any sense, because the constraint literally says that it cannot install itself; a tautology. But in combination with multi-version, I can see how it makes sense. But it's basically saying: you can have multiple versions of package A, but if you select version A@42 (the version that adds the self-referential constraints), that's the only possible version. Right?

I am wondering if there is not a more elegant way of modeling that specifically. Resolvo takes a lot of care to try to minimize the number of clauses that it has to maintain. I wonder if it would take fewer clauses if we model this particular behavior specifically rather than just adding general constraint clauses.

@dralley

dralley commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

The two aren't directly related, I'm only thinking about both because I would eventually want to have both and because the approach you suggested was essentially the same as what I was working on for multi-version, not because there's a direct relationship.

I agree with you that from a purely logical perspective it doesn't make a ton of sense. What I've been told (from Slack) is:

It means that exactly one package can satisfy that requirement. There may be multiple packages that could but it's declaring that they aren't co-installable.

and then another response:

That seems to be exactly it. You can’t install 2 different system-release packages at the same time. But you can update since you’ll be removing one to install another (no conflict)

[root@r9 ~]# yum install centos-stream-release-9.0-36.el9.noarch.rpm --repofrompath centos-stream,https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os
Updating Subscription Management repositories.
Added centos-stream repo from https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os
centos-stream                                                                                                                                           9.9 MB/s | 8.9 MB     00:00
Last metadata expiration check: 0:00:03 ago on Fri Jun  5 23:44:22 2026.
Error:
Problem: problem with installed package redhat-release-9.7-0.7.el9.x86_64
 - package centos-stream-release-9.0-36.el9.noarch from @commandline conflicts with system-release provided by redhat-release-9.7-0.7.el9.x86_64 from @System

Specifically this package seems to include things like distro public GPG keys for package signing, secure boot certificates, and the list of distro repo configs. In other words it's a package that you don't want to be able to remove or replace easily lest the system become unusable, & kind of defines what the distro is in some sense (the root of trust and such).

Fedora also has a similar pattern: https://src.fedoraproject.org/rpms/fedora-release/blob/f44/f/fedora-release.spec#_156

Otherwise all I know that it seems to be something DNF / libsolv explicitly supports. And really it seems to just be standard behavior across an entire solve and necessarily something that is per-package configurable.

@dralley dralley force-pushed the constrains-itself branch from 829c580 to bab305a Compare June 9, 2026 04:14
@dralley dralley marked this pull request as draft June 9, 2026 04:18
@dralley

dralley commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

If you want to / have time to play with it yourself, you can.

Code is here: https://github.com/prefix-dev/resolvo-rpm/

Here's the repo metadata I'm using: https://drive.google.com/file/d/12e_lwXdLfykQn0r3fHL7qlX886wJXnF4/view?usp=sharing

Extract the files and dump them in tests/assets/, or use the fetch command I added to download whichever repos you want independently of the zip.

Example: cargo run -- resolve --repo tests/assets/cs10-baseos bash

I need to clean up this branch a bit though FYI. That example no longer reproduces the original issue exactly, it hits a related one first.

@dralley dralley force-pushed the constrains-itself branch from bab305a to 2d1099c Compare June 12, 2026 17:30
@dralley dralley force-pushed the constrains-itself branch 3 times, most recently from fac083d to bbcfd13 Compare June 27, 2026 03:41
@dralley dralley marked this pull request as ready for review June 27, 2026 03:43
@dralley

dralley commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Changed: Added a solver_config() method to the DependencyProvider trait, with a default impl that just returns the default value. Solver now calls this during construction to configure itself.

This allows DependencyProvider implementations to to make tweaks to the default config of the Solver.

In such a case, the constrains clause would try to forbid the package
from coexisting with itself.  This caused WatchedLiterals::constrains
to panic with "both literals cannot be false" because both watched
literals referred to the same variable.

There exists a special case in certain packaging ecosystems such as RPM,
where a package can both provide and conflict with the same capability
(example: in CentOS Stream, the "centos-stream-release" provides
"system-release" and also conflicts with "system-release").

This is explicitly permitted and supported by libsolv, which provides
the configuration flag "forbidselfconflicts" which can be disabled.

Provide an option to skip self-referential constrains entries, so that
implementations can configure the resolver such that a package that
is already in the solution cannot constrain itself out of it.

Assisted-By: Claude Opus 4.6
@dralley dralley force-pushed the constrains-itself branch from bbcfd13 to fc53d87 Compare June 27, 2026 03:54
Comment thread src/solver/mod.rs
/// (i.e. it conflicts with itself) is marked uninstallable. When `false`,
/// self-conflicts are silently ignored.
pub forbid_self_conflicts: bool,
}

@dralley dralley Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@baszalmstra Do you think this is reasonable?

Libsolv offers a couple such options (e.g. 1, 2), but I know your intention is to stay as generic as possible.

I also have a Candidates based implementation, but I don't think it actually ends up simpler and is probably less performant, since it is treated as a a global option so if you were to emulate it with a map per-nameid or per-solvable then it would require a lot of hashmap insertions and subsequent lookups where the answer will always be the same. The additional flexibility isn't needed, at least not by RPM.

Comment thread src/solver/mod.rs
/// Creates a single threaded block solver, using the provided
/// [`DependencyProvider`].
pub fn new(provider: D) -> Self {
let config = provider.solver_config();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Uses the config provided by DependencyProvider by default, which implementors can optionally define (has default impl) but can be overridden manually on the solver (makes testing easier)

@baszalmstra

Copy link
Copy Markdown
Contributor

Im on a holiday this week, Ill have all the time to think this through properly.

Im hesitant to merge this as is because it feels like genericly allowing self references just feels like a hack. It feels to me that the versionset is just wrong, I think it should exclude the solvable itself. But I can also see that this is harder to express.

That said, does this still work now that #246 is merged? It breaks the assumption that a constraint with a certain id always matches the same solvables.

@baszalmstra

Copy link
Copy Markdown
Contributor

Maybe we can combine this with your previous PR? If we detect a self reference but we allow multiple versions for the package that we then disable all other solvables? Thats what we are actually trying to express.

@dralley

dralley commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

I'm open to whatever you think is best, so long as it works.

That said, does this still work now that #246 is merged? It breaks the assumption that a constraint with a certain id always matches the same solvables.

It seems to still work. (49c2d6668d89 present for both examples)

with forbid_self_conflicts = true (existing main branch behavior)

dalley@dalley-thinkpadt14gen5:~/Devel/rpm-dev/resolvo-rpm$ cargo run -- resolve --repo tests/assets/cs10-baseos bash
   Compiling resolvo-rpm v0.1.0 (/home/dalley/Devel/rpm-dev/resolvo-rpm)
    Finished `dev` profile [optimized + debuginfo] target(s) in 3.88s
     Running `target/debug/resolvo-rpm resolve --repo tests/assets/cs10-baseos bash`
Error: The following packages are incompatible
└─ bash > 0.0.0 cannot be installed because there are no viable options:
   └─ bash bash 0:5.2.26-4.el10 | bash 0:5.2.26-6.el10 would require
      └─ filesystem >= 3, which cannot be installed because there are no viable options:
         └─ filesystem filesystem 0:3.18-15.el10 | filesystem 0:3.18-16.el10 | filesystem 0:3.18-9.el10 would require
            └─ setup *, which cannot be installed because there are no viable options:
               └─ setup setup 0:2.14.5-3.el10 | setup 0:2.14.5-4.el10 would require
                  └─ system-release *, which cannot be installed because there are no viable options:
                     ├─ centos-stream-release centos-stream-release 0:10.0-2.el10 would constrain
                     │  └─ system-release < 0, which conflicts with any installable versions previously reported
                     └─ centos-stream-release centos-stream-release 0:10.0-3.el10 | centos-stream-release 0:10.0-5.el10 | centos-stream-release 0:10.0-7.el10 | centos-stream-release 0:10.0-8.el10 would constrain
                        └─ system-release < 0, which conflicts with any installable versions previously reported

With forbid_self_conflicts = false (enabled by this change)

dalley@dalley-thinkpadt14gen5:~/Devel/rpm-dev/resolvo-rpm$ cargo run -- resolve --repo tests/assets/cs10-baseos bash
   Compiling resolvo-rpm v0.1.0 (/home/dalley/Devel/rpm-dev/resolvo-rpm)
    Finished `dev` profile [optimized + debuginfo] target(s) in 1.67s
     Running `target/debug/resolvo-rpm resolve --repo tests/assets/cs10-baseos bash`
basesystem             0:11-21.el10            [tests/assets/cs10-baseos]
bash                   0:5.2.26-4.el10         [tests/assets/cs10-baseos]
centos-gpg-keys        0:10.0-2.el10           [tests/assets/cs10-baseos]
centos-stream-release  0:10.0-2.el10           [tests/assets/cs10-baseos]
centos-stream-repos    0:10.0-2.el10           [tests/assets/cs10-baseos]
filesystem             0:3.18-9.el10           [tests/assets/cs10-baseos]
glibc                  0:2.39-17.el10          [tests/assets/cs10-baseos]
glibc-all-langpacks    0:2.39-17.el10          [tests/assets/cs10-baseos]
glibc-common           0:2.39-17.el10          [tests/assets/cs10-baseos]
glibc-gconv-extra      0:2.39-17.el10          [tests/assets/cs10-baseos]
libgcc                 0:14.1.1-5.el10         [tests/assets/cs10-baseos]
ncurses-base           0:6.4-13.20240127.el10  [tests/assets/cs10-baseos]
ncurses-libs           0:6.4-13.20240127.el10  [tests/assets/cs10-baseos]
setup                  0:2.14.5-3.el10         [tests/assets/cs10-baseos]
tzdata                 0:2024a-3.el10          [tests/assets/cs10-baseos]
tzdata                 0:2025a-1.el10          [tests/assets/cs10-baseos]
tzdata                 0:2025b-1.el10          [tests/assets/cs10-baseos]

17 packages resolved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants