Summary
APK.coarse_version in osv/ecosystems/alpine.py breaks the rule that
ecosystems_base.py says it must follow: "if v1 < v2, then
coarse_version(v1) <= coarse_version(v2)". It breaks for Alpine versions that
end in a letter, like OpenSSL's 1.1.1a through 1.1.1w, which is how OpenSSL
ships in Alpine. When this happens the stored [coarse_min, coarse_max] range
gets flipped, and the API's coarse pre-filter can miss versions that are
actually affected.
How to reproduce
from osv.ecosystems import alpine
apk = alpine.APK()
for v in ['1.1.1', '1.1.1a', '1.1.1k', '1.1.1w', '1.1.2']:
print(v, apk.coarse_version(v))
1.1.1 00:00000001.00000001.00000001
1.1.1a 00:00000001.00000001.00000000 <- patch drops to 0
1.1.1k 00:00000001.00000001.00000000
1.1.1w 00:00000001.00000001.00000000
1.1.2 00:00000001.00000001.00000002
The sort order is 1.1.1 < 1.1.1a < ... < 1.1.1w, but coarse(1.1.1) is
greater than coarse(1.1.1a).
What causes it
coarse_version uses separators_regex=r'[.]' and implicit_split=False, so a
piece like "1w" never gets split into 1 and w. "1w" is not a decimal, so
the loop that reads integer parts stops there and the patch number falls back to
0.
Why it matters
For a range with introduced 1.1.1 and fixed 1.1.1w, _get_coarse_min_max in
osv/models.py stores coarse_min = coarse(1.1.1) = ...1.1.1 and
coarse_max = coarse(1.1.1w) = ...1.1.0. So coarse_min ends up bigger than
coarse_max. The query filter in gcp/api/server.py uses
coarse_min <= coarse(v) <= coarse_max, so every affected version fails the
check and gets dropped.
One caveat: if the advisory also has an enumerated versions list, a second
AffectedVersions entity is built with min/max and can hide the miss at
query time. The miss is fully live for advisories that only have a range, and
the stored range is flipped either way.
Why the existing test doesn't catch it
apk_version_strategy in coarse_version_monotonicity_test.py only builds
versions with _rc, _p, and -r suffixes. It never puts a bare letter at the
end, so the property test can't generate 1.1.1w and passes today.
This is not the known Go transitivity issue
The Go code already turns Alpine coarse versioning off:
// go/osv/ecosystem/apk.go
func (e apkEcosystem) Coarse(_ string) (string, error) {
// TODO(michaelkedar): semantic.AlpineVersion currently breaks transitivity
// rules (a < b, b < c, c < a) in some cases with invalid versions.
// Which makes coarse versions kinda broken.
return "", ErrCoarseNotSupported
}
and FuzzAPKMonotonicity is skipped, with Alpine commented out of
coarse_large_test.go.
That TODO is about transitivity breaking on invalid versions. This is a
different thing: monotonicity breaking on valid versions (1.1.1a through
1.1.1w). The Python path still has coarse versioning on, so Go fails safe here
and Python does not.
Suggested fix
Two options, whichever you prefer:
- Set
implicit_split=True in APK.coarse_version. It fixes the cases above
(letter versions map to the same coarse as their number base) and does not
break the existing @example cases.
- If Alpine coarse is meant to stay off, make the Python side fall back the way
Go does with ErrCoarseNotSupported.
Either way I would add letter suffixes to apk_version_strategy so this stays
covered.
Happy to work on this if you assign it to me.
Summary
APK.coarse_versioninosv/ecosystems/alpine.pybreaks the rule thatecosystems_base.pysays it must follow: "if v1 < v2, thencoarse_version(v1) <= coarse_version(v2)". It breaks for Alpine versions that
end in a letter, like OpenSSL's
1.1.1athrough1.1.1w, which is how OpenSSLships in Alpine. When this happens the stored
[coarse_min, coarse_max]rangegets flipped, and the API's coarse pre-filter can miss versions that are
actually affected.
How to reproduce
The sort order is
1.1.1 < 1.1.1a < ... < 1.1.1w, butcoarse(1.1.1)isgreater than
coarse(1.1.1a).What causes it
coarse_versionusesseparators_regex=r'[.]'andimplicit_split=False, so apiece like
"1w"never gets split into1andw."1w"is not a decimal, sothe loop that reads integer parts stops there and the patch number falls back to
0.
Why it matters
For a range with introduced
1.1.1and fixed1.1.1w,_get_coarse_min_maxinosv/models.pystorescoarse_min = coarse(1.1.1) = ...1.1.1andcoarse_max = coarse(1.1.1w) = ...1.1.0. Socoarse_minends up bigger thancoarse_max. The query filter ingcp/api/server.pyusescoarse_min <= coarse(v) <= coarse_max, so every affected version fails thecheck and gets dropped.
One caveat: if the advisory also has an enumerated
versionslist, a secondAffectedVersionsentity is built withmin/maxand can hide the miss atquery time. The miss is fully live for advisories that only have a range, and
the stored range is flipped either way.
Why the existing test doesn't catch it
apk_version_strategyincoarse_version_monotonicity_test.pyonly buildsversions with
_rc,_p, and-rsuffixes. It never puts a bare letter at theend, so the property test can't generate
1.1.1wand passes today.This is not the known Go transitivity issue
The Go code already turns Alpine coarse versioning off:
and
FuzzAPKMonotonicityis skipped, with Alpine commented out ofcoarse_large_test.go.That TODO is about transitivity breaking on invalid versions. This is a
different thing: monotonicity breaking on valid versions (
1.1.1athrough1.1.1w). The Python path still has coarse versioning on, so Go fails safe hereand Python does not.
Suggested fix
Two options, whichever you prefer:
implicit_split=TrueinAPK.coarse_version. It fixes the cases above(letter versions map to the same coarse as their number base) and does not
break the existing
@examplecases.Go does with
ErrCoarseNotSupported.Either way I would add letter suffixes to
apk_version_strategyso this stayscovered.
Happy to work on this if you assign it to me.