Skip to content

Add Simplifying-Software-Component-Updates (fixes #813) #861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 24, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions docs/Simplifying-Software-Component-Updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Simplifying Software Component Updates

*by the [Open Source Security Foundation (OpenSSF)](https://openssf.org) [Best Practices Working Group](https://best.openssf.org/), 2025-04-24*

This document guides component creators and component users to simplify updates and help avoid backward incompatibility problems when updating. A key technique is for component developers to avoid creating backward incompatibilities wherever practical. ***Backward-incompatible changes to an application programmer interface (API) often lead to unaddressed security vulnerabilities*.**

## Introduction

Modern software systems are mostly reused software. Studies show that, for example, the average percentage of open source software (OSS) in software applications is somewhere between 70% [[Black Duck 2025](https://www.blackduck.com/resources/analyst-reports/open-source-security-risk-analysis.html)] and 90% [[Sonatype 2024](https://www.sonatype.com/state-of-the-software-supply-chain/introduction)]. These numbers don’t even include reused closed source software. These reused components often reuse other components and can be *many* layers deep. Manually handling this scale can lead to many failures, so it’s important to automate dependency management (e.g., through package managers). In *theory*, if a reused component has a vulnerability, users should simply update to a newer version that fixes that vulnerability once the newer version is available.

Unfortunately, sometimes newer versions of components fail to support older interfaces, or change their functionality in backward-incompatible ways. This leads to many or most of their users being stuck on older versions of the software for long periods or perhaps forever. One study found that 90% of analyzed codebases contain OSS components that are more than 10 versions behind [[Black Duck 2025](https://www.blackduck.com/resources/analyst-reports/open-source-security-risk-analysis.html)]. When a vulnerability is found—and eventually one is—the vulnerability is fixed in the “current” component version, but many users of that software will remain vulnerable because it’s impractical for them to update to the current version. Some Linux distributions backport security fixes to older versions, making updates easier to handle, but this also keeps users in old versions no longer supported by the original project. Eventually this support ends and the update effort is typically enormous. In addition, system failures that are caused by updates train component users that component developers cannot be trusted to develop backward-compatible updates. These terrible experiences will lead component users to believe that upgrades are dangerous, so they won’t update when they should update.

In short: *backward-incompatible changes often lead to unaddressed security vulnerabilities*.

Historically fewer software components were reused, and there were fewer layers of indirect use. When only one component is being reused by a program, handling a small backward-incompatible change is relatively simple. However, at scale, such changes become a massive problem. Modern software development involves much larger scales. For example, if you start creating an application using React (a widely used system), you begin with over two thousand packages before adding any functionality. [[Singh2022](https://medium.com/frontendweb/find-how-many-packages-we-need-to-run-a-react-hello-world-app-695fbb755af7)]

Backward-incompatible changes are also increasingly problematic because most software components are used *indirectly*. When there are many layers of dependencies, it takes time for each layer’s updates to trickle up, introducing a sort of “speed of light” rate limit for updating software. Any delay in updating any intermediate layer impedes updates of all transitive users. For example, [[Wetter2021](https://security.googleblog.com/2021/12/understanding-impact-of-apache-log4j.html)] found in response to the Log4Shell vulnerability that “most artifacts that depend on log4j do so indirectly. The deeper the vulnerability is in a dependency chain, the more steps are required for it to be fixed. […] For greater than 80% of the packages, the vulnerability is more than one level deep, with a majority affected five levels down (and some as many as nine levels down).”

Developers have created mechanisms to deal with backward incompatibility, but these often create larger problems later. A developer may clone some code; in cloning, code is copied into the project. Unfortunately, these copies may include vulnerabilities, and since their origin is no longer automatically tracked, those vulnerabilities are hidden by the development process and are no longer automatically updated. An alternative is shading, a “variant of cloning where entire packages are cloned and renamed.” This may be done at build time (aka “b-shading”) and some ecosystems have tools specifically to support b-shading (e.g., the Maven shade plugin). While b-shading solves an immediate problem and is trackable by tools, the approach also introduces longer-term risks as it tends to endlessly defer necessary updates. Other kinds of shading are used as well [[Dietrich2023](https://arxiv.org/abs/2306.05534)]. In all cases, alternatives create risks when compared to simply updating a given component to its current version.

In extreme cases, such as the Log4Shell vulnerability, specialized programs were created to directly hotpatch programs to perform updates [[Nalley2021](https://aws.amazon.com/blogs/opensource/hotpatch-for-apache-log4j/)]. This extreme approach is *not* reasonable to apply in “normal” circumstances and risks causing many additional problems.

Some component developers claim keeping older interfaces working “can’t” be done. Yet it’s usually quite possible. Even large projects have managed to maintain backward-compatible interfaces for decades, even within current branches of a codebase. An example is the Linux kernel, whose developers sometimes call this “don’t break userspace”. As [Linus Torvalds (leader of the Linux kernel project) noted in 2005](https://yarchive.net/comp/linux/gcc_vs_kernel_stability.html), “We care about user-space [external] interfaces to an insane degree. We go to extreme lengths to maintain even badly designed or unintentional interfaces. Breaking user programs simply isn't acceptable… We know that people use old binaries for years and years and that making a new release doesn't mean that you can just throw that out. You can trust us. … I'm not talking about never obsoleting bad interfaces at all. I'm talking about the unnecessary breakage that comes from changes that simply aren't needed, and that isn't given proper heads-up for.” This simple policy has enabled the Linux kernel developers to build trust with their users.

Even in rare cases where a backward-incompatible change *must* be done, the damage caused by backward-incompatible interfaces can usually be limited.

## Component Creators

Consider the following whenever making changes that might change the component’s external interfaces:

1. *Ensure users have an automated update mechanism.* For example, ensure that the component is in at least one repository and can be downloaded via a package manager. Note that the [Cyber Resilience Act (CRA) Annex I(I)(2)(c)](https://eur-lex.europa.eu/eli/reg/2024/2847/oj#anx_I) requires that products “ensure that vulnerabilities can be addressed through security updates, including, where applicable, through automatic security updates that are installed within an appropriate timeframe enabled as a default setting, with a clear and easy-to-use opt-out mechanism, through the notification of available updates to users, and the option to temporarily postpone them”.
2. *Consider maintaining a stable release branch that only receives security updates and no new features*. Such branches are often called long-term support (LTS) branches and include only backported vulnerability fixes. LTS branches enable users to rapidly update to fix vulnerabilities. Approach the users of your project to support such LTS branches. It’s still important to support older APIs where practical on newer branches, so that users can more easily move to a current branch. If there aren’t enough resources to maintain stable branches, it’s more important to have a single branch where later versions continue to support earlier APIs.
3. *Avoid making backward-incompatible changes to existing external interfaces to the extent feasible*. You can create *new* interfaces, but do your best to keep old ones working. E.g., if you use semantic versioning (SemVer), do your best to *never* change the major version number once it becomes “1”.
4. *Mark an interface “deprecated” if you don’t want more users of it, but where practical, don’t remove that interface.* In general, do not *remove* a deprecated interface unless using it is by *definition* a security vulnerability. The term “deprecated” should *not* be interpreted as “will be removed in the future” but as “we recommend using something else instead”. We encourage projects to *allow the continued use of* deprecated interfaces where practical.
5. *Don’t change an API in ways that **encourage** errors*. For example, don’t keep an API name but swap the order of parameters. Instead, create a new name with the order swapped while leaving the old interface in place.
6. *If you want to create a higher-level/more abstract API that is easier to use, consider keeping the older API as the “lower-level” API*. Consider implementing the “higher-level” API in terms of the “lower-level API”.
7. *If you have a new API approach at the same level of abstraction, consider creating the new API, then implement the old API using the new API*. This provides the old functionality, and it also helps verify that the new API didn’t lose functionality. If you decide to later remove the old API, perhaps many years later, it will be easier to do so.
8. *If you found a better name, create the new name, but ensure the old name will continue to work*. You might choose to implement the old name by calling the new name.
9. *Where practical, change an API in backward-compatible ways*. In many ecosystems you can add *optional* parameters without changing an interface, as long as the new parameters’ defaults cause the original behavior. This makes it easier to add functionality without breaking an existing API.
10. *Where practical, make it easy to gradually transition to the new API*. Instead of requiring all code to switch to the new API, make it possible to gradually update different files over time.
11. *Consider using a versioned API, e.g., “v1” and “v2”*. This is common in REST interfaces. You can then create a new version of the API, while continuing to support the older API, because it’s easy to see which interface was intended.
12. *In system packages and libraries, consider using “compat symbols” which lets the linker select from one of many implementations of a function*. These can be used to provide multiple versions of an interface, including backward-compatible ones. [[Delorie2019](https://developers.redhat.com/blog/2019/08/01/how-the-gnu-c-library-handles-backward-compatibility)]
13. *If you have internal interfaces you do **not** intend to keep stable, **clearly** document this where potential users can see it*. An example is the Linux kernel, which has a stable external interface to userspace applications but clearly documents that its internal kernel module interfaces are *not* stable.
14. *Ensure you have an automated test suite that tests older and newer APIs*.
15. *Attempt to anticipate likely future changes to an API, so that they will have fewer impacts on users*. For example, you might add an extra parameter like “flags” or “configuration” that enables adding capabilities later without changing the API for existing users.
16. *Consider supporting subsetted functionality*. Code that can’t be executed can’t be exploited and functional changes won’t impact users. Therefore, consider making it easy to enable only a subset of the component’s functionality. Mechanisms for doing this include splitting functions into components (so users can choose a subset of them), providing a plug-in architecture (so users can choose their plug-ins), or a configuration system that allows users to enable or disable specific functions.

## Component Users

1. *Be cautious when adding dependencies.* Before adding a dependency, check if the functionality you need is already available in your existing dependencies or standard library. Every dependency introduces maintenance burden, and may become unmaintained and/or introduce security risks. [Evaluate](https://best.openssf.org/Concise-Guide-for-Evaluating-Open-Source-Software) each dependency based on its quality, maintenance, and security. While avoiding adding a new dependency reduces supply chain risk, there is an inherent risk in reinventing the wheel – the code you write will almost certainly have bugs and potential security vulnerabilities. A well-maintained and widely used library will likely be more robust and secure than writing your own implementation*.*
2. *Configure dependencies so you only use what you need.* Some components make it easy to load only part (e.g., through a plug-in architecture or configuration). It’s hard to exploit code that isn’t there or can’t be executed.
3. *Use package manager(s) to track dependencies and enable automated updates*. Most modern systems have too many dependencies to manage manually.
4. *If your ecosystem allows it, applications (but not libraries) should use lockfiles where available*. A lockfile is a text file identifying the specific version of the dependencies used by a project so they can be precisely reproduced. Ideally a lockfile includes cryptographic hashes (so later tampering can be detected and prevented). Be as specific and complete as the ecosystem supports.
5. *Enable tools to automatically detect when a vulnerable component is being used*. E.g., in GitHub, enable Dependabot.
6. *Implement automated tests and run them every time dependencies change*. These automated tests should check both functionality and security. Make sure the tests pass in all situations you care about (e.g., different platforms or different customer configurations). Running tests after updating dependencies provides evidence that the updates won’t break functionality or security.
7. *For system applications and libraries, consider building on the oldest platform you want to support*. For more information see [[Delorie2019](https://developers.redhat.com/blog/2019/08/01/how-the-gnu-c-library-handles-backward-compatibility)].
8. *If upgrading to a newer version of a component is impractical, consider backporting vulnerability fixes to older versions*. Such backports can be done downstream or in a stable (LTS) branch maintained by the project. If the project does not maintain a stable release branch, but that component is so critically important to a user to backport vulnerability fixes downstream, consider contributing those backports upstream and/or offering support in maintaining such a stable release branch.
9. *Consider sourcing open source components from (distribution) providers which maintain stable releases of older components*.
10. *Strive to avoid maintaining downstream modifications of open source code*. Downstream modifications may be pursued by users for various different reasons (e.g. business or use case specific features). However, maintaining such downstream modifications create a serious risk of accumulating changes over time which significantly hinder uplifting in a timely and seamless manner, due to the need to adapt those changes to every new upstream release. Instead, if changes are needed, e.g., new features, consider contributing or jointly developing those with the upstream project.

## Related Materials

We encourage you to check out these related materials:

1. [Concise Guide for Developing More Secure Software](https://best.openssf.org/Concise-Guide-for-Developing-More-Secure-Software)
2. [Concise Guide for Evaluating Open Source Software](https://best.openssf.org/Concise-Guide-for-Evaluating-Open-Source-Software)
3. Free course [“Developing Secure Software” (LFD121)](https://openssf.org/training/courses/)
4. Other [guidance from the OpenSSF Best Practices Working Group](https://best.openssf.org/)
5. [OpenSSF main site](https://openssf.org/)

## References

[[BlackDuck2025](https://www.blackduck.com/resources/analyst-reports/open-source-security-risk-analysis.html)] Black Duck, 2025, Open Source Security & Risk Analysis Report, [https://www.blackduck.com/resources/analyst-reports/open-source-security-risk-analysis.html](https://www.blackduck.com/resources/analyst-reports/open-source-security-risk-analysis.html)

[[Delorie2019](https://developers.redhat.com/blog/2019/08/01/how-the-gnu-c-library-handles-backward-compatibility)] Delorie, DJ, 2019-08-01, “How the GNU C Library handles backward compatibility”, [https://developers.redhat.com/blog/2019/08/01/how-the-gnu-c-library-handles-backward-compatibility](https://developers.redhat.com/blog/2019/08/01/how-the-gnu-c-library-handles-backward-compatibility)

[[Dietrich2023](https://arxiv.org/abs/2306.05534)] Dietrich, Jens, Shawn Rasheed, Alexander Jordan, and Tim White, 2023, “On the Security Blind Spots of Software Composition Analysis”, [https://arxiv.org/abs/2306.05534](https://arxiv.org/abs/2306.05534)

[[Nalley2021](https://aws.amazon.com/blogs/opensource/hotpatch-for-apache-log4j/)] Nalley, David and Volker Simonis, 2021-12-12, “Hotpatch for Apache Log4j”, [https://aws.amazon.com/blogs/opensource/hotpatch-for-apache-log4j/](https://aws.amazon.com/blogs/opensource/hotpatch-for-apache-log4j/)

[[Singh2022](https://medium.com/frontendweb/find-how-many-packages-we-need-to-run-a-react-hello-world-app-695fbb755af7)] Singh, Rajdeep, 2022, Find How Many Packages We Need to Run a React' Hello World' App, [https://medium.com/frontendweb/find-how-many-packages-we-need-to-run-a-react-hello-world-app-695fbb755af7](https://medium.com/frontendweb/find-how-many-packages-we-need-to-run-a-react-hello-world-app-695fbb755af7)

[[Sonatype2024](https://www.sonatype.com/state-of-the-software-supply-chain/introduction)] Sonatype, 2024, [https://www.sonatype.com/state-of-the-software-supply-chain/introduction](https://www.sonatype.com/state-of-the-software-supply-chain/introduction)

[[Wetter2021](https://security.googleblog.com/2021/12/understanding-impact-of-apache-log4j.html)] Wetter, James, and Nicky Ringland, 2021-12-17, Open Source Insights Team “Understanding the Impact of Apache Log4j Vulnerability”, [https://security.googleblog.com/2021/12/understanding-impact-of-apache-log4j.html](https://security.googleblog.com/2021/12/understanding-impact-of-apache-log4j.html)

## Credits

This document was developed by the OpenSSF Best Practices Working Group (WG). The initial draft was developed by David A. Wheeler. The need for it was identified during the [OpenSSF Policy Summit DC 2025](https://events.linuxfoundation.org/openssf-policy-summit-dc/) of 2025-03-04 at the [OSS Best Practices Breakout Group](https://docs.google.com/document/d/1XzlTmDBPBpUPzVEE2ftOOwcqNRV1hqIPaeJSJk__ksE/edit). We give grateful thanks to the contributions (sorted alphabetically) of Avishay Balter, Chris de Almeida, David A. Wheeler, Georg Kunz, Matt Wilson, and S. Joshua N.