Skip to content

Alpine coarse_version is not monotonic for versions ending in a letter (like OpenSSL 1.1.1w) #5641

Description

@LordAizen1

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions