👉 Building statically linked binaries in Rust is a breeze unless you also need to statically link some external dependencies as well as take care about Glibc.

rust-logo

Rust as well as Go are the new generation of programming languages that make it a breeze to produce statically linked binaries … in some cases. If you want to see an example of how to also package your Rust application with some external dependencies, like OpenSSL or PostgreSQL, read on.

Dynamic linking

By default Rust compiler statically links all native Rust-dependencies, but when it comes to external dependencies it links them dynamically by default.

For more information on linkage itself see 👉 The Rust Reference 👈

But, let’s verify the theory first, so let’s:

  • create a test project
  • add dependency to OpenSSL
  • build the project
  • inspect shared dependencies

Before starting with an experiment make sure you have both openssl and pkg-config installed in your system (if it is Linux). In NixOS it is as easy as starting a shell with $ nix-shell -p openssl pkgconfig.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# create a test project
if [ -d /tmp/testssl ]; then rm -rf /tmp/testssl; fi
cargo new --bin /tmp/testssl
cd /tmp/testssl
cat <<EOF >> Cargo.toml
openssl-sys = "0.9"
openssl = "0.10"

[workspace]
EOF

# add dependency to OpenSSL
cat <<EOF > src/main.rs
use openssl::rsa::Rsa;

fn main() {
  let _ = Rsa::generate(2048).ok();
  println!("Hello, world!");
}
EOF

# build the project
cargo build

# inspect shared dependencies
ldd ./target/debug/testssl

The output of the last command should be similar to the following one:

1
2
3
4
5
6
7
8
linux-vdso.so.1 (0x00007ffd2cdfe000)
libssl.so.1.1 => /nix/store/wbs8ia6vsm5gp0fg0bgvhv6sdh23wbx6-openssl-1.1.1f/lib/libssl.so.1.1 (0x00007f22d908e000)
libcrypto.so.1.1 => /nix/store/wbs8ia6vsm5gp0fg0bgvhv6sdh23wbx6-openssl-1.1.1f/lib/libcrypto.so.1.1 (0x00007f22d8da2000)
libdl.so.2 => /nix/store/6m2k8kx8h216jlx9dg3lp4m90bz05yck-glibc-2.30/lib/libdl.so.2 (0x00007f22d8d9d000)
libpthread.so.0 => /nix/store/6m2k8kx8h216jlx9dg3lp4m90bz05yck-glibc-2.30/lib/libpthread.so.0 (0x00007f22d8d7c000)
libgcc_s.so.1 => /nix/store/6m2k8kx8h216jlx9dg3lp4m90bz05yck-glibc-2.30/lib/libgcc_s.so.1 (0x00007f22d8d62000)
libc.so.6 => /nix/store/6m2k8kx8h216jlx9dg3lp4m90bz05yck-glibc-2.30/lib/libc.so.6 (0x00007f22d8ba1000)
/nix/store/6m2k8kx8h216jlx9dg3lp4m90bz05yck-glibc-2.30/lib/ld-linux-x86-64.so.2 => /nix/store/6m2k8kx8h216jlx9dg3lp4m90bz05yck-glibc-2.30/lib64/ld-linux-x86-64.so.2 (0x00007f22d9168000)

Here you can clearly see that the testssl application expects some runtime dependencies to be in place when it runs, and one of them is OpenSSL:

1
libssl.so.1.1 => ...-openssl-1.1.1d/lib/libssl.so.1.1 (0x00007f3bd4ee5000)

In case when at least a single dependency is not present, you simply will not be able to run your application. And so, in order to overcome this hurdle, it is possible to statically link such dependencies into the target executable. It is not simple, but nevertheless doable.

Another peculiar dependency you may have noticed in that list is the dependecy to Glibc. By default Rust directs some system calls to Glibc that interfaces Linux Kernel. Glibc is very dynamic in nature and thus trying to statically link Glibc is going to be completely futile as it will anyway try to load some of its own dependencies dynamically in runtime. This problem will be addressed by statically linking another Libc, called Musl. In contrast to Glibs, Musl was designed such that it could be statically linked.

Yet another peculiarity of Glibc is that when you build your executable against newer versions of Glibc it might end up being impossible to run on the target systems with older versions of Glibc. These cases are rare, but nevertheless important to remember!

Static linking

Let’s now see how it is possible to statically link some external dependencies. For bravity and reproducibility I will be using docker-based approach. But it is farely straightforward to reproduce it in other environments if necessary.

  • First, I will show how to build a base image with some external dependencies (more specifically, OpenSSL, PostgreSQL and ZLib).
  • Then, I will show how to bake a base Rust image targetting Musl instead of Glibc.
  • And finally, I will show that it is possible to produce a fully statically linked executable in Rust with all its external dependencies built right inside of it.

Build external dependencies

So, let’s build a base docker image that will include external dependencies:

  • OpenSSL
  • PostgreSQL
  • ZLib

Notice though that each of these dependencies are in turn built against Musl! This is the key to success 👏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
FROM debian:stretch
USER root
ARG OPENSSL_VER=1.1.1a
ARG POSTGRES_VER=11.1
RUN apt-get update && \
    apt-get install -y \
      musl-dev musl-tools file nano git zlib1g-dev cmake make g++ curl pkgconf \
      linux-headers-amd64 ca-certificates xutils-dev libpq-dev libssl-dev \
      --no-install-recommends && \
    rm -rf /var/lib/apt/lists/*
ENV MUSL_PREFIX=/musl
RUN mkdir /workdir && mkdir $MUSL_PREFIX
WORKDIR /libworkdir
RUN ln -s /usr/include/x86_64-linux-gnu/asm /usr/include/x86_64-linux-musl/asm && \
    ln -s /usr/include/asm-generic /usr/include/x86_64-linux-musl/asm-generic && \
    ln -s /usr/include/linux /usr/include/x86_64-linux-musl/linux
RUN curl -sL http://www.openssl.org/source/openssl-${OPENSSL_VER}.tar.gz | tar xz
RUN curl -sL https://ftp.postgresql.org/pub/source/v${POSTGRES_VER}/postgresql-${POSTGRES_VER}.tar.gz | tar xz
RUN curl -sL https://zlib.net/zlib-1.2.11.tar.gz | tar xz
RUN cd zlib-1.2.11 && \
    CC="musl-gcc -fPIE -pie" LDFLAGS="-L/musl/lib/" CFLAGS="-I/musl/include" \
      ./configure --prefix=$MUSL_PREFIX && \
    make -j$(nproc) && \
    make install
RUN cd openssl-${OPENSSL_VER} && \
    CC="musl-gcc -fPIE -pie" LDFLAGS="-L/musl/lib/" CFLAGS="-I/musl/include" \
      ./Configure no-shared no-async --prefix=$MUSL_PREFIX --openssldir=$MUSL_PREFIX/ssl linux-x86_64 && \
    make depend && \
    make -j$(nproc) && \
    make install
RUN echo "/musl/lib" >> /etc/ld-musl-x86_64.path
RUN cd postgresql-${POSTGRES_VER} && \
    CC="musl-gcc -fPIE -pie" LDFLAGS="-L/musl/lib/" CFLAGS="-I/musl/include" \
      ./configure --prefix=$MUSL_PREFIX --host=x86_64-unknown-linux-musl --without-readline && \
    make -j$(nproc) && \
    make install

Let’s build a base image now (assuming the above file is saved as Dockerfile.base):

1
docker build -f Dockerfile.base -t base .

This Dockerfile allows to build different images with different versions of OpenSSL and/or PostgreSQL:

1
docker build -f Dockerfile.base --build-arg POSTGRES_VER=10.1 -t base .

Build Rust image against Musl

The following Dockerfile.rust-base file shows how to build Rust based image that targets Musl as well as setup some critical environment variables to make it all possible:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
FROM base
ARG RUST_VER=1.42.0
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y  --default-toolchain ${RUST_VER}
ENV PATH=/root/.cargo/bin:$PATH
RUN cargo install cargo-tree
RUN rustup target add x86_64-unknown-linux-musl
ENV PATH=$MUSL_PREFIX/bin:$PATH \
    PKG_CONFIG_ALLOW_CROSS=true \
    PKG_CONFIG_ALL_STATIC=true \
    PKG_CONFIG_PATH=$MUSL_PREFIX/lib/pkgconfig \
    PQ_LIB_STATIC_X86_64_UNKNOWN_LINUX_MUSL=true \
    PG_CONFIG_X86_64_UNKNOWN_LINUX_GNU=/usr/bin/pg_config \
    OPENSSL_STATIC=true \
    OPENSSL_DIR=$MUSL_PREFIX \
    SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
    SSL_CERT_DIR=/etc/ssl/certs \
    LIBZ_SYS_STATIC=1
WORKDIR /workdir

Now, let’s build rust-base image:

1
docker build -f Dockerfile.rust-base -t rust-base .

This Dockerfile allows to build different images with different versions of Rust too:

1
docker build -f Dockerfile.base --build-arg RUST_VER=1.39.0 -t rust-base .

Build fully statically linked executable

And we are almost there! The only thing left to do realy is to just build it statically linking all of its dependencies:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
cd /tmp/testssl

# prep the Dockerfile
cat <<EOF > Dockerfile
FROM rust-base as builder
ADD . /workdir
RUN PKG_CONFIG_ALLOW_CROSS=1 cargo build --release --target x86_64-unknown-linux-musl

FROM alpine:3.11.3
COPY --from=builder /workdir/target/x86_64-unknown-linux-musl/release/testssl /usr/local/bin/
ENTRYPOINT ["testssl"]
EOF

# build it
docker build -t testssl .

# extract it
id=$(docker create testssl)
docker cp $id:/usr/local/bin/testssl ./
docker rm -v $id

At this point, if everything went well, you should have ./testssl file available in /tmp/testssl folder. Let’s see if this is exactly what we wanted:

1
2
# inspect shared dependencies
ldd ./testssl

And this time the output looks like this:

1
not a dynamic executable

Notice that testssl docker image is as tiny as it can get too ;)

💪 Bingo!