add support for linking multiple copies of s2n-tls in an application
jmayclin opened this issue · comments
Use Case:
s2n-tls need to make sure that features work across versions of s2n-tls. This is necessary because customer version upgrades are not atomic, so customers frequently run two different versions of s2n-tls in a single fleet. Or a customer might serialize a connection to disk, upgrade s2n-tls, and then need to deserialize that connection.
An easy way to write these tests would be to use cargo to depend on old versions
# bindings/rust/integration/Cargo.toml
# this is the current version of s2n-tls that is under development
s2n-tls = { path = "../s2n-tls"}
old-s2n-tls = { package = "s2n-tls", version = "=0.2.0" }
// integration/src/lib.rs
#[test]
fn session_resumption_test {
let old_session_ticket = handshake(
old_s2n_tls::connection::Connection(),
old_s2n_tls::connection::Connection()
);
let new_server = s2n_tls::connection::Connection();
let new_client = s2n_tls::connection::Connection();
new_client.set_session(session_ticket)
# assert that we can resume a connection when using an old session ticket
handshake(new_server, new_client);
}
Problem 1: Cargo "link"
When trying to specify an additional s2n-tls dependency, cargo returns an error.
[dependencies]
s2n-tls = { path = "../s2n-tls" }
+ old-s2n-tls = { package = "s2n-tls", version = "=0.2.0" }
error: failed to select a version for `s2n-tls-sys`.
... required by package `s2n-tls v0.1.7 (/home/ec2-user/workspace/s2n-tls/bindings/rust/s2n-tls)`
... which satisfies path dependency `s2n-tls` (locked to 0.1.7) of package `bench v0.1.0 (/home/ec2-user/workspace/s2n-tls/bindings/rust/bench)`
versions that meet the requirements `=0.1.7` (locked to 0.1.7) are: 0.1.7
the package `s2n-tls-sys` links to the native library `s2n-tls`, but it conflicts with a previous package which links to `s2n-tls` as well:
package `s2n-tls-sys v0.2.0`
... which satisfies dependency `s2n-tls-sys = "=0.2.0"` of package `s2n-tls v0.2.0`
... which satisfies dependency `old-s2n-tls = "=0.2.0"` of package `bench v0.1.0 (/home/ec2-user/workspace/s2n-tls/bindings/rust/bench)`
Only one package in the dependency graph may specify the same links value. This helps ensure that only one copy of a native library is linked in the final binary. Try to adjust your dependencies so that only one package uses the `links = "s2n-tls"` value. For more information, see https://doc.rust-lang.org/cargo/reference/resolver.html#links.
The relevant section is this
the package
s2n-tls-sys
links to the native librarys2n-tls
, but it conflicts with a previous package which links tos2n-tls
as well: package s2n-tls-sys v0.2.0
Rust allows packages to declare the native dependencies that they link to by specifying the links key in the cargo.toml. s2n-tls-sys specifies links = "s2n-tls"
so to Cargo the build situation looks like the following
graph TD
NEW_LIB[s2n-tls 0.2.3] -->|depends| NEW_SYS[s2n-tls-sys 0.2.3]
OLD_LIB[s2n-tls 0.2.0] -->|depends| OLD_SYS[s2n-tls-sys 0.2.0]
NEW_SYS[s2n-tls-sys 0.2.3] -->|links| ARTIFACT[libs2n.a]
OLD_SYS[s2n-tls-sys 0.2.0] -->|links| ARTIFACT[libs2n.a]
However, this is actually unnecessarily restrictive for most customers, since there is a separate libs2n
compiled by each s2n-tls-sys
.
Solution 1:
Instead of specifying a single "links" key, we can specify a version-dependent “link” property.
links = “s2n-tls-VERSION”
Our s2n-tls-sys
generation logic already includes support for replacing text tokens, so this should be relatively easy to do. Note that we'd also need to rename the actual artifact outputted by the compilation process.
graph TD
NEW_LIB[s2n-tls 0.2.3] -->|depends| NEW_SYS[s2n-tls-sys 0.2.3]
OLD_LIB[s2n-tls 0.2.0] -->|depends| OLD_SYS[s2n-tls-sys 0.2.0]
NEW_SYS[s2n-tls-sys 0.2.3] -->|links| NEW_ARTIFACT[libs2n_0_2_3.a]
OLD_SYS[s2n-tls-sys 0.2.0] -->|links| OLD_ARTIFACT[libs2n_0_2_0.a]
Problem 2: Duplicate Symbols
While the above solution will successfully let cargo build the project, it doesn’t actually solve the problem of duplicate symbols. In s2n-tls-sys, all of the bindings are declared using extern C
linkage.
// bindings/rust/s2n-tls-sys/src/api.rs
extern "C" {
#[doc = " Creates a new s2n_config object. This object can (and should) be associated with many connection\n objects.\n\n The returned config will be initialized with default system certificates in its trust store.\n\n The returned config should be freed with `s2n_config_free()` after it's no longer in use by any\n connection.\n\n @returns A new configuration object suitable for configuring connections and associating certs\n and keys."]
pub fn s2n_config_new() -> *mut s2n_config;
}
But there are now two different definitions of s2n_config_new
, one in libs2n_0_2_3.a
and one in libs2n_0_2_0.a
.
Solution 2: Symbol Prefixing
To get around this, s2n-tls can prefix it’s symbols, similar to how AWS-LC-RS prefixes it’s symbols. AWS-LC is able to do this using existing build functionality inherited from boringssl (link).
I'm not sure what exactly the solution would look like for s2n-tls, but some options would be
- autogenerated "prefix header" that would swap all of the public symbols names to a prefixed version?
#define s2n_config_new s2n_config_new_0_2_3
- objcopy shenanigans? This seems much messier than handling it through the compiler.
Whoops, should have searched a bit harder for that. Closing as duplicate.