Remix.run Logo
trjordan 4 days ago

There is absolutely a good reason for version ranges: security updates.

When I, the owner of an application, choose a library (libuseful 2.1.1), I think it's fine that the library author uses other libraries (libinsecure 0.2.0).

But in 3 months, libinsecure is discovered (surprise!) to be insecure. So they release libinsecure 0.2.1, because they're good at semver. The libuseful library authors, meanwhile, are on vacation because it's August.

I would like to update. Turns out libinsecure's vulnerability is kind of a big deal. And with fully hardcoded dependencies, I cannot, without some horrible annoying work like forking/building/repackaging libuseful. I'd much rather libuseful depend on libinsecure 0.2.*, even if libinsecure isn't terribly good at semver.

I would love software to be deterministically built. But as long as we have security bugs, the current state is a reasonable compromise.

seniorsassycat 4 days ago | parent | next [-]

Yeah, this felt like a gap in the article. You'd have to wait for every package to update from the bottom up before you could update you top levels to remove a risk (or you could patch in place, or override)

But what if all the packages had automatic ci/cd, and libinsecure 0.2.1 is published, libuseful automatically tests a new version of itself that uses 0.2.1, and if it succeeds it publishes a new version. And consumers of libuseful do the same, and so on.

CognitiveLens 4 days ago | parent [-]

The automatic ci/cd suggestion sounds appealing, but at least in the NPM ecosystem, the depth of those dependencies would mean the top-level dependencies would constantly be incrementing. On the app developer side, it would take a lot of attention to figure when it's important to update top-level dependencies and when it's not.

silverwind 3 days ago | parent [-]

Could aggregate the incrementing to for example 1 per day.

deredede 4 days ago | parent | prev | next [-]

What if libinsecure 0.2.1 is the version that introduces the vulnerability, do you still want your application to pick up the update?

I think the better model is that your package manager let you do exactly what you want -- override libuseful's dependency on libinsecure when building your app.

trjordan 4 days ago | parent [-]

Of course there's no 0-risk version of any of this. But in my experience, bugs tend to get introduced with features, then slowly ironed out over patches and minor versions.

I want no security bugs, but as a heuristic, I'd strongly prefer the latest patch version of all libraries, even without perfect guarantees. Code rots, and most versioning schemes are designed with that in mind.

MarkusQ 4 days ago | parent [-]

Except the only reason code "rots" is that the environment keeps changing as people chase the latest shiny thing. Moreover, it rots _faster_ once the assumption that everyone is going to constantly update get established, since it can be used to justify pushing non-working garbage, on the assumption "we'll fix it in an update".

This may sound judgy, but at the heart it's intended to be descriptive: there are two roughly stable states, and both have their problems.

PhilipRoman 4 days ago | parent | prev | next [-]

Slightly off topic but we need to normalize the ability to patch external dependencies (especially transitive ones). Coming from systems like Yocto, it was mind boggling to see a company bugging the author of an open source library to release a new version to the package manager with a fix that they desperately needed.

In binary package managers this kind of workflow seems like an afterthought.

eitau_1 4 days ago | parent [-]

nixpkgs shines especially bright in this exact scenario

skybrian 4 days ago | parent | prev | next [-]

Go has a deterministic package manager and handles security bugs by letting library authors retract versions [1]. The 'go get' command will print a warning if you try to retrieve a retracted version. Then you can bump the version for that module at top level.

You also have the option of ignoring it if you want to build the old version for some reason, such as testing the broken version.

[1] https://go.dev/ref/mod#go-mod-file-retract

guhcampos 4 days ago | parent | prev | next [-]

The author hints very briefly that Semantic Version is a hint, not a guarantee, to which I agree - but then I think we should be insisting on library maintainers that semantic versioning *should* be a guarantee, and in the worst case scenario, boycott libraries that claim to be semantically versioned but don't do it in reality.

oiWecsio 4 days ago | parent | next [-]

I don't understand why major.minor.patchlevel is a "hint". It had been an interface contract with shared libraries written in C when I first touched Linux, and that was 25+ years ago; way before the term "semantic version" was even invented (AFAICT).

michaelt 4 days ago | parent [-]

Imagine I make a library for loading a certain format of small, trusted configuration files.

Some guy files a CVE against my library, saying it crashes if you feed it a large, untrusted file.

I decide to put out a new version of the library, fixing the CVE by refusing to load conspicuously large files. The API otherwise remains unchanged.

Is the new release a major, minor, or bugfix release? As I have only an approximate understanding of semantic versioning norms, I could go for any of them to be honest.

Some other library authors are just as confused as me, which is why major.minor.patchlevel is only a hint.

shwestrick 4 days ago | parent | next [-]

I like this example.

The client who didn't notice a difference would probably call it a bugfix.

The client whose software got ever-so-slightly more reliable probably would call it a minor update.

The client whose software previously was loading large files (luckily) without issue would call it major, because now their software just doesn't work anymore.

michaelt 4 days ago | parent [-]

It's also an almost-real situation (although I wasn't the library developer involved)

You can Google "YAMLException: The incoming YAML document exceeds the limit" - an error introduced in response to CVE-2022-38752 - to see what happens when a library introduces a new input size limit.

What happened in that case is: the updated library bumps their version from 1.31 to 1.32; then a downstream application updates their dependencies, passes all tests, and updates their version from 9.3.8.0 to 9.3.9.0

oiWecsio 3 days ago | parent | prev [-]

> Imagine I make a library for loading a certain format of small, trusted configuration files.

> Some guy files a CVE against my library, saying it crashes if you feed it a large, untrusted file.

Not CVE-worthy, as the use case clearly falls outside of the documented / declared area of application.

> refusing to load conspicuously large files [...] Is the new release a major, minor, or bugfix release?

It deserves a major release, because it breaks compatibility. A capability that used to work (i.e,. loading a large but trusted file) no longer works. It may not affect everyone, but when assessing impact, we go for the most conservative evaluation.

andix 4 days ago | parent | prev [-]

It can't be a guarantee. Even the smallest patches for vulnerabilities change the behavior of the code. Most of the time this is not a problem, but weird things happen all the time. Higher memory usage, slower performance, some regressions that are only relevant for a tiny amount of users, ...

SchemaLoad 4 days ago | parent [-]

Pretty much. Everything is a breaking change to someone. Best to just ignore sem ver and have a robust automated test suite and deployment process that minimises issues with a bad build.

tonsky 4 days ago | parent | prev [-]

It’s totally fine in Maven, no need to rebuild or repackage anything. You just override version of libinsecure in your pom.xml and it uses the version you told it to

zahlman 4 days ago | parent [-]

So you... manually re-lock the parts you need to?

aidenn0 4 days ago | parent | next [-]

Don't forget the part where Maven silently picks one version for you when there are transitive dependency conflicts (and no, it's not always the newest one).

deredede 4 days ago | parent | prev [-]

Sure, I'm happy with locking the parts I need to lock. Why would I lock the parts I don't need to lock?

skywhopper 3 days ago | parent [-]

Because you can’t know which ones you “need” to lock.

lenkite 3 days ago | parent [-]

You can definitely know this. Use

    mvn dependency:tree -Dverbose
Or use maven-enforcer-plugin to fail the build on conflicts.