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-registrywill read ourCargo.lockfile 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!