RustBinSign - Build old crates like they're new
Lately, I have been working on adding a signature provider for rustbinsign that would not require IDA pro to work.
To do my tests, I tried to sign Krustyloader, as I did a year ago (see my previous blog post). Doing that, I stumbled upon some issue: I couldn’t compile many of the tests from multiple dependencies.
For example, Krustyloader (816754f6eaf72d2e9c69fe09dcbe50576f7a052a1a450c2a19f01f57a6e13c17
) has tokio-1.29.0
as a dependency, and got compiled with rustc 1.70.0
(see rbi result).
While trying to sign this sample, rustbinsign
downloads tokio-1.29.0
from git, and tries to compile its tests
, benches
and examples
, to solve Monomorphization and LTO issues (see this blogpost).
So I let the code ran, and I figured out that the output was quite small: 10 executables got compiled, at most, which is unusual. The typical tokio tests/benches/examples compilation result in ~250 executables. While debugging this, I found that tests and benches failed to compile with the following error:
$ cargo +1.70.0 build --tests --features=full
error: package `termtree v0.5.1` cannot be built because it requires rustc 1.74 or newer, while the currently active rustc version is 1.70.0
Either upgrade to rustc 1.74 or newer, or use
cargo update -p termtree@0.5.1 --precise ver
where `ver` is the latest version of `termtree` supporting rustc 1.70.0
The error is explicit: building tests requires termtree@0.5.1
, which requires rustc 1.74 to build.
How could this be ? tokio-tests
defines rust-version = "1.56"
in its Cargo.toml
! Plus, rustc 1.74 did not exist when tokio-1.29.0 came out ! I know for sure that I was able to compile those just fine just a year ago. What happened since then that prevents me from building those tests again ?
Hunting the bad dependency
Surely some dependency got updated. I ran cargo tree
to find the “invalid” dependency:
stress-test v0.1.0 (/tmp/rustbinsign/tokio/stress-test)
└── tokio v1.29.0 (/tmp/rustbinsign/tokio/tokio)
...
[build-dependencies]
└── autocfg v1.4.0
[dev-dependencies]
├── async-stream v0.3.6
│ ├── async-stream-impl v0.3.6 (proc-macro)
│ │ ├── proc-macro2 v1.0.94 (*)
│ │ ├── quote v1.0.39 (*)
│ │ └── syn v2.0.99 (*)
│ ├── futures-core v0.3.31
│ └── pin-project-lite v0.2.16
├── futures v0.3.31
...
├── libc v0.2.170
├── mockall v0.11.4
...
│ ├── predicates v2.1.5
...
│ └── predicates-tree v1.0.12
│ ├── predicates-core v1.0.9
│ └── termtree v0.5.1
...
└── tokio-test v0.4.2 (/tmp/rustbinsign/tokio/tokio-test) (*)
...
It looks like termtree0.5.1
is required to build stress-test
, which is defined in tokio’s workspaces
:
$ cd tokio-1.29.0
$ cat Cargo.toml
[workspace]
resolver = "2"
members = [ "tokio", "tokio-macros", "tokio-test", "tokio-stream", "tokio-util", "examples", "benches", "stress-test", "tests-build", "tests-integration",]
By reading dependencies, when can find that:
dev-dependencies
requires mockall v0.11.4
, which requires predicates-tree v1.0.12
, which itself requires termtree v0.5.1
.
A quick look at crates.io informs us that predicates-tree v1.0.12
requires rustc v1.74.0.
The reason for this is that rust-version
field was purely indicative until rust 1.84.0
!
Solution
There are two ways to solve this:
- recreate the old crates.io index from the time tokio-1.29.0 could be built using rust 1.70.0 ;
- don’t do anything, and executables will gradually be using rust >= 1.84.0 over time, solving the issue by itself.
The first solution is doable (I PoC-ed it), but could be a bit tedius to automate. It also requires pulling a 4GB git repository.
I might implement this in the future, if I have the use for it, or if anyone manifests the use for it.
PoC-ed implementation
Actual crates.io
is splitted into two repositories:
- https://github.com/rust-lang/crates.io-index
- https://github.com/rust-lang/crates.io-index-archive.git
The former maintains a valid index for the last few months. The latter archives old indexes, inside “snapshot” branches:
$ git clone https://github.com/rust-lang/crates.io-index-archive.git
$ git branch -a
remotes/origin/main
remotes/origin/snapshot-2018-09-26
remotes/origin/snapshot-2019-10-17
remotes/origin/snapshot-2020-03-25
remotes/origin/snapshot-2020-08-04
remotes/origin/snapshot-2020-11-20
remotes/origin/snapshot-2021-05-05
remotes/origin/snapshot-2021-07-02
remotes/origin/snapshot-2021-09-24
remotes/origin/snapshot-2021-12-21
remotes/origin/snapshot-2022-03-02
remotes/origin/snapshot-2022-07-06
remotes/origin/snapshot-2022-08-31
remotes/origin/snapshot-2022-12-19
remotes/origin/snapshot-2023-01-12
remotes/origin/snapshot-2023-04-03
remotes/origin/snapshot-2023-06-30
remotes/origin/snapshot-2023-12-03
remotes/origin/snapshot-2024-03-11
remotes/origin/snapshot-2024-05-18
remotes/origin/snapshot-2024-09-08
remotes/origin/snapshot-2024-11-27
rustbininfo
can help us figure out which branch to checkout:
$ rustbininfo -d 816754f6eaf72d2e9c69fe09dcbe50576f7a052a1a450c2a19f01f57a6e13c17
Latest dependency was added between 2023-06-27 20:37:41.423254+00:00 and 2023-06-29 22:04:27.541298+00:00
snapshot-2023-06-30
was the first archived branch after the latest dependency got added to my target sample.
Now that we have all the pieces:
$ export XDG_DATA_HOME=$HOME/.local/share
$ mkdir -p $XDG_DATA_HOME/rbs/vendor/index
$ git clone https://github.com/rust-lang/crates.io-index-archive.git $XDG_DATA_HOME/rbs/vendor/index
$ cd $XDG_DATA_HOME/rbs/vendor/index
$ git checkout snapshot-2023-06-30
$ cd /tmp/tokio-1.29.0/
$ mkdir .cargo
$ echo "
[source.crates-io]
registry = 'sparse+https://index.crates.io/'
replace-with = 'local-registry'
[source.local-registry]
local-registry = '$XDG_DATA_HOME/rbs/vendor/'
" > .cargo/config.toml
$ cargo generate-lockfile # mandatory to prevent the following command to update the lock file and bypass the index
$ cargo local-registry --sync Cargo.lock $XDG_DATA_HOME/rbs/vendor
$ cargo +1.70.0 build --tests --features=full --release
Here is a breakdown of the above commands:
- pull crates.io index archive and fetch the index from 30 June 2023
- add this index to current cargo.toml configuration of tokio
- lock the file (using the “old” crates.io index we just pulled)
- download the relevant crates with
cargo local-registry
(having an index only specifies available crates and how to download them, but it does not hold the crates themselves. Thus, we need to download the crates we need.cargo local-registry
will read ourCargo.lock
file and download required crates.) - build the crate.
And voilà! tokio-1.29.0
can build its tests and benches just fine using rust 1.70.0, like it could a year ago!