google / google/osv.dev

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

Open
#5,641 1 comment 0 reactions 0 assignees View on GitHub
stale
Dominant language
Go
Stars
2.9k
Forks
369
Avg merge
1d 17h
Merged PRs (30d)
149

Description

### 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

```py
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.

Contributor guide

Open the contributing guide

Research direction

Start with osv/ecosystems/alpine.py and coarse_version_monotonicity_test.py; read the invariant in ecosystems_base.py and run the existing Alpine monotonicity tests. Add coverage for letter-suffixed versions such as 1.1.1a and 1.1.1w, then verify their coarse versions remain ordered and affected ranges are not inverted.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, python
Domain
backend, testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.