fix(auth): isolate bundled runtime symbol
This commit is contained in:
parent
91fa60898e
commit
dc92e63019
|
|
@ -21,6 +21,18 @@ been retroactively stripped, encrypted, or described as obfuscated because an
|
|||
in-place binary rewrite could break final linkage. Run
|
||||
`tools/check-op-auth-prebuilt.sh` for the current measured audit.
|
||||
|
||||
The Linux and MSVC artifacts were built as C-facing Rust `staticlib` archives,
|
||||
so they also contain the producing toolchain's Rust runtime. Before a Rust host
|
||||
link, `build.rs` validates the original archive, rechecks that the bytes being
|
||||
staged have the validated digest, and creates a private `OUT_DIR` copy in which
|
||||
the equal-length `rust_eh_personality` symbol name is namespaced to
|
||||
`rust_eh_personalitx`. The one-byte suffix change also preserves the sorted
|
||||
MSVC linker-member index. This updates the definition and its internal
|
||||
references without changing archive offsets, keeps the committed SHA/signature
|
||||
as the trust anchor, and avoids a broad linker multiple-definition exception.
|
||||
Malformed archives, changed bytes, and archives containing both names fail
|
||||
closed.
|
||||
|
||||
Production ABI-v2 artifacts fail closed unless their byte hash, target, ABI,
|
||||
source revision, build id, and `op-auth-hardened-v1` declaration are covered by
|
||||
an Ed25519 signature rooted in `prebuilt/PROVENANCE_PUBKEY`. The private release
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[path = "prebuilt_link_compat.rs"]
|
||||
mod prebuilt_link_compat;
|
||||
#[path = "prebuilt_provenance.rs"]
|
||||
mod prebuilt_provenance;
|
||||
|
||||
|
|
@ -19,6 +21,7 @@ fn main() {
|
|||
println!("cargo:rustc-check-cfg=cfg(op_auth_collab_ticket_prebuilt)");
|
||||
println!("cargo:rustc-check-cfg=cfg(op_auth_development_prebuilt)");
|
||||
println!("cargo:rerun-if-changed=prebuilt");
|
||||
println!("cargo:rerun-if-changed=prebuilt_link_compat.rs");
|
||||
println!("cargo:rerun-if-env-changed={DEV_ARCHIVE_ENV}");
|
||||
println!("cargo:rerun-if-env-changed={DEV_ABI_VERSION_ENV}");
|
||||
|
||||
|
|
@ -33,14 +36,14 @@ fn main() {
|
|||
};
|
||||
|
||||
let development = development_prebuilt(artifact);
|
||||
let (prebuilt_dir, abi_version, development_override, signed_provenance) =
|
||||
let (prebuilt_dir, abi_version, development_override, signed_provenance, expected_sha256) =
|
||||
if let Some((directory, abi_version)) = development {
|
||||
println!(
|
||||
"cargo:warning=using unsigned local op-auth ABI {abi_version} \
|
||||
archive for a debug build"
|
||||
);
|
||||
println!("cargo:rustc-cfg=op_auth_development_prebuilt");
|
||||
(directory, abi_version, true, false)
|
||||
(directory, abi_version, true, false, None)
|
||||
} else {
|
||||
let prebuilt_dir = manifest_dir.join("prebuilt").join(&target);
|
||||
let artifact_path = prebuilt_dir.join(artifact);
|
||||
|
|
@ -65,6 +68,7 @@ fn main() {
|
|||
validated.abi_version,
|
||||
false,
|
||||
validated.signed_provenance,
|
||||
Some(validated.archive_sha256),
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -77,7 +81,8 @@ fn main() {
|
|||
println!("cargo:rustc-cfg=op_auth_collab_ticket_prebuilt");
|
||||
}
|
||||
println!("cargo:rustc-env=OP_AUTH_PREBUILT_ABI_VERSION={abi_version}");
|
||||
println!("cargo:rustc-link-search=native={}", prebuilt_dir.display());
|
||||
let link_dir = rust_host_link_directory(&target, &prebuilt_dir, artifact, expected_sha256);
|
||||
println!("cargo:rustc-link-search=native={}", link_dir.display());
|
||||
// `-bundle`: keep the archive out of this crate's rlib and hand it to
|
||||
// the final link instead. Bundled foreign objects would otherwise be
|
||||
// fed to thin-LTO in release builds, which fails with "failed to get
|
||||
|
|
@ -96,6 +101,36 @@ fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
fn rust_host_link_directory(
|
||||
target: &str,
|
||||
prebuilt_dir: &Path,
|
||||
artifact: &str,
|
||||
expected_sha256: Option<[u8; 32]>,
|
||||
) -> PathBuf {
|
||||
if !target.contains("linux") && !target.ends_with("-pc-windows-msvc") {
|
||||
return prebuilt_dir.to_path_buf();
|
||||
}
|
||||
|
||||
let link_dir = PathBuf::from(
|
||||
env::var("OUT_DIR").expect("Cargo provides OUT_DIR to the op-auth build script"),
|
||||
)
|
||||
.join("rust-host-link");
|
||||
let report = prebuilt_link_compat::stage_archive_for_rust_host(
|
||||
&prebuilt_dir.join(artifact),
|
||||
&link_dir.join(artifact),
|
||||
expected_sha256,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("failed to stage op-auth for Rust host linking: {error}"));
|
||||
if report.renamed_occurrences != 0 {
|
||||
println!(
|
||||
"cargo:warning=isolated {} bundled Rust personality symbol occurrence(s) \
|
||||
in the temporary op-auth link archive",
|
||||
report.renamed_occurrences
|
||||
);
|
||||
}
|
||||
link_dir
|
||||
}
|
||||
|
||||
fn development_prebuilt(artifact: &str) -> Option<(PathBuf, u32)> {
|
||||
let feature_enabled = env::var_os(DEV_FEATURE_ENV).is_some();
|
||||
let archive = env::var_os(DEV_ARCHIVE_ENV);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@ Do not run `strip`, `objcopy`, or an obfuscator in place on these committed
|
|||
files: archive members and cross-object symbols may be required by the final
|
||||
link.
|
||||
|
||||
Linux and MSVC Rust hosts link a temporary Cargo `OUT_DIR` derivative, not
|
||||
these committed bytes. The build bridge gives the archive's bundled
|
||||
`rust_eh_personality` an equal-length private name, including its internal
|
||||
references, because a C-facing Rust `staticlib` otherwise conflicts with the
|
||||
host toolchain's own personality routine. The transformation happens only
|
||||
after provenance validation, preserves archive layout, and rejects ambiguous
|
||||
input; it never weakens the linker's duplicate-symbol checks.
|
||||
|
||||
## ABI-v2 signed provenance
|
||||
|
||||
Every production target directory must contain:
|
||||
|
|
@ -55,6 +63,8 @@ full source revision. At minimum:
|
|||
temporary-directory prefixes;
|
||||
- use one narrow `extern "C"` wrapper and keep every other implementation
|
||||
symbol hidden from the final binary;
|
||||
- namespace any Rust runtime symbols that must remain in the archive so they
|
||||
cannot collide when the C ABI is linked into a different Rust toolchain;
|
||||
- enable fat LTO, one codegen unit, dead-code elimination, and symbol stripping
|
||||
at the final application link;
|
||||
- use `panic=abort` for the static library if the private implementation can
|
||||
|
|
|
|||
318
crates/op-auth-bridge/prebuilt_link_compat.rs
Normal file
318
crates/op-auth-bridge/prebuilt_link_compat.rs
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
//! Deterministic compatibility staging for C-facing Rust static libraries.
|
||||
//!
|
||||
//! A Rust `staticlib` contains its own Rust runtime because it is normally
|
||||
//! linked by a C application. When that archive is linked back into a Rust
|
||||
//! executable, newer ELF/COFF linkers reject the archive's
|
||||
//! `rust_eh_personality` alongside the host toolchain's definition. The
|
||||
//! original, provenance-checked archive stays untouched. Only the private
|
||||
//! Cargo `OUT_DIR` copy is rewritten, using an equal-length private symbol
|
||||
//! name so archive offsets, object symbol indexes, and relocations remain
|
||||
//! unchanged.
|
||||
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
const ARCHIVE_MAGIC: &[u8] = b"!<arch>\n";
|
||||
const HOST_PERSONALITY: &[u8] = b"rust_eh_personality";
|
||||
// Keep the alias adjacent to the original name. The MSVC archive's second
|
||||
// linker member is lexically sorted and some linkers binary-search it; moving
|
||||
// the entry to an `op_auth_*` prefix would invalidate that index.
|
||||
const PRIVATE_PERSONALITY: &[u8] = b"rust_eh_personalitx";
|
||||
const _: [(); HOST_PERSONALITY.len()] = [(); PRIVATE_PERSONALITY.len()];
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct LinkCompatReport {
|
||||
pub renamed_occurrences: usize,
|
||||
pub existing_private_occurrences: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LinkCompatError {
|
||||
Io {
|
||||
operation: &'static str,
|
||||
source: io::Error,
|
||||
},
|
||||
InvalidArchive(&'static str),
|
||||
ConflictingPersonalitySymbols,
|
||||
SourceDigestMismatch,
|
||||
SourceEqualsDestination,
|
||||
}
|
||||
|
||||
impl fmt::Display for LinkCompatError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io { operation, source } => write!(formatter, "{operation}: {source}"),
|
||||
Self::InvalidArchive(reason) => {
|
||||
write!(formatter, "static archive is malformed: {reason}")
|
||||
}
|
||||
Self::ConflictingPersonalitySymbols => formatter
|
||||
.write_str("archive contains both host and private Rust personality symbol names"),
|
||||
Self::SourceDigestMismatch => {
|
||||
formatter.write_str("source archive changed after provenance validation")
|
||||
}
|
||||
Self::SourceEqualsDestination => {
|
||||
formatter.write_str("compatibility staging must not modify the source archive")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LinkCompatError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io { source, .. } => Some(source),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage_archive_for_rust_host(
|
||||
source: &Path,
|
||||
destination: &Path,
|
||||
expected_sha256: Option<[u8; 32]>,
|
||||
) -> Result<LinkCompatReport, LinkCompatError> {
|
||||
if source == destination {
|
||||
return Err(LinkCompatError::SourceEqualsDestination);
|
||||
}
|
||||
|
||||
let mut archive = fs::read(source).map_err(|source| LinkCompatError::Io {
|
||||
operation: "failed to read source archive",
|
||||
source,
|
||||
})?;
|
||||
if expected_sha256
|
||||
.is_some_and(|expected| <[u8; 32]>::from(Sha256::digest(&archive)) != expected)
|
||||
{
|
||||
return Err(LinkCompatError::SourceDigestMismatch);
|
||||
}
|
||||
validate_archive(&archive)?;
|
||||
|
||||
let host_count = count_occurrences(&archive, HOST_PERSONALITY);
|
||||
let private_count = count_occurrences(&archive, PRIVATE_PERSONALITY);
|
||||
if host_count != 0 && private_count != 0 {
|
||||
return Err(LinkCompatError::ConflictingPersonalitySymbols);
|
||||
}
|
||||
|
||||
if host_count != 0 {
|
||||
replace_equal_length(&mut archive, HOST_PERSONALITY, PRIVATE_PERSONALITY);
|
||||
}
|
||||
if count_occurrences(&archive, HOST_PERSONALITY) != 0 {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"host Rust personality symbol remained after staging",
|
||||
));
|
||||
}
|
||||
validate_archive(&archive)?;
|
||||
|
||||
let parent = destination.parent().ok_or(LinkCompatError::InvalidArchive(
|
||||
"staged archive path has no parent",
|
||||
))?;
|
||||
fs::create_dir_all(parent).map_err(|source| LinkCompatError::Io {
|
||||
operation: "failed to create archive staging directory",
|
||||
source,
|
||||
})?;
|
||||
fs::write(destination, archive).map_err(|source| LinkCompatError::Io {
|
||||
operation: "failed to write staged archive",
|
||||
source,
|
||||
})?;
|
||||
|
||||
Ok(LinkCompatReport {
|
||||
renamed_occurrences: host_count,
|
||||
existing_private_occurrences: private_count,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_archive(archive: &[u8]) -> Result<(), LinkCompatError> {
|
||||
if !archive.starts_with(ARCHIVE_MAGIC) {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"missing portable archive magic",
|
||||
));
|
||||
}
|
||||
|
||||
let mut offset = ARCHIVE_MAGIC.len();
|
||||
let mut member_count = 0_usize;
|
||||
let mut linker_member_count = 0_usize;
|
||||
while offset < archive.len() {
|
||||
let header_end = offset
|
||||
.checked_add(60)
|
||||
.ok_or(LinkCompatError::InvalidArchive("member header overflow"))?;
|
||||
let header = archive
|
||||
.get(offset..header_end)
|
||||
.ok_or(LinkCompatError::InvalidArchive("truncated member header"))?;
|
||||
if &header[58..60] != b"`\n" {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"invalid member header terminator",
|
||||
));
|
||||
}
|
||||
|
||||
let size = parse_decimal(&header[48..58])?;
|
||||
let data_end = header_end
|
||||
.checked_add(size)
|
||||
.ok_or(LinkCompatError::InvalidArchive("member size overflow"))?;
|
||||
if data_end > archive.len() {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"member extends beyond archive",
|
||||
));
|
||||
}
|
||||
if is_linker_member_name(&header[..16]) {
|
||||
linker_member_count += 1;
|
||||
if linker_member_count == 2 {
|
||||
validate_msvc_sorted_linker_member(&archive[header_end..data_end])?;
|
||||
}
|
||||
}
|
||||
|
||||
offset = data_end
|
||||
.checked_add(size & 1)
|
||||
.ok_or(LinkCompatError::InvalidArchive("member padding overflow"))?;
|
||||
if offset > archive.len() {
|
||||
return Err(LinkCompatError::InvalidArchive("truncated member padding"));
|
||||
}
|
||||
member_count += 1;
|
||||
}
|
||||
|
||||
if member_count == 0 {
|
||||
return Err(LinkCompatError::InvalidArchive("archive has no members"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_linker_member_name(field: &[u8]) -> bool {
|
||||
let end = field
|
||||
.iter()
|
||||
.rposition(|byte| *byte != b' ')
|
||||
.map_or(0, |index| index + 1);
|
||||
&field[..end] == b"/"
|
||||
}
|
||||
|
||||
fn validate_msvc_sorted_linker_member(member: &[u8]) -> Result<(), LinkCompatError> {
|
||||
let archive_member_count = read_u32_le(member, 0)? as usize;
|
||||
if archive_member_count > u16::MAX as usize {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member has too many archive members",
|
||||
));
|
||||
}
|
||||
let offsets_end =
|
||||
4_usize
|
||||
.checked_add(archive_member_count.checked_mul(4).ok_or(
|
||||
LinkCompatError::InvalidArchive("MSVC linker member offset table overflow"),
|
||||
)?)
|
||||
.ok_or(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member offset table overflow",
|
||||
))?;
|
||||
let symbol_count = read_u32_le(member, offsets_end)? as usize;
|
||||
let indices_start = offsets_end
|
||||
.checked_add(4)
|
||||
.ok_or(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member symbol table overflow",
|
||||
))?;
|
||||
let names_start = indices_start
|
||||
.checked_add(
|
||||
symbol_count
|
||||
.checked_mul(2)
|
||||
.ok_or(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member index table overflow",
|
||||
))?,
|
||||
)
|
||||
.ok_or(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member index table overflow",
|
||||
))?;
|
||||
if names_start > member.len() {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"truncated MSVC linker member index table",
|
||||
));
|
||||
}
|
||||
|
||||
for index in 0..symbol_count {
|
||||
let entry = indices_start + index * 2;
|
||||
let member_index = u16::from_le_bytes([member[entry], member[entry + 1]]) as usize;
|
||||
if member_index == 0 || member_index > archive_member_count {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member index is out of range",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut name_offset = names_start;
|
||||
let mut previous_name: Option<&[u8]> = None;
|
||||
for _ in 0..symbol_count {
|
||||
let remaining = &member[name_offset..];
|
||||
let name_len =
|
||||
remaining
|
||||
.iter()
|
||||
.position(|byte| *byte == 0)
|
||||
.ok_or(LinkCompatError::InvalidArchive(
|
||||
"unterminated MSVC linker member symbol",
|
||||
))?;
|
||||
let name = &remaining[..name_len];
|
||||
if name.is_empty() {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"empty MSVC linker member symbol",
|
||||
));
|
||||
}
|
||||
if previous_name.is_some_and(|previous| previous > name) {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member symbols are not sorted",
|
||||
));
|
||||
}
|
||||
previous_name = Some(name);
|
||||
name_offset =
|
||||
name_offset
|
||||
.checked_add(name_len + 1)
|
||||
.ok_or(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member symbol table overflow",
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_u32_le(bytes: &[u8], offset: usize) -> Result<u32, LinkCompatError> {
|
||||
let end = offset
|
||||
.checked_add(4)
|
||||
.ok_or(LinkCompatError::InvalidArchive(
|
||||
"MSVC linker member integer overflow",
|
||||
))?;
|
||||
let encoded: [u8; 4] = bytes
|
||||
.get(offset..end)
|
||||
.ok_or(LinkCompatError::InvalidArchive(
|
||||
"truncated MSVC linker member integer",
|
||||
))?
|
||||
.try_into()
|
||||
.map_err(|_| LinkCompatError::InvalidArchive("invalid MSVC linker member integer"))?;
|
||||
Ok(u32::from_le_bytes(encoded))
|
||||
}
|
||||
|
||||
fn parse_decimal(field: &[u8]) -> Result<usize, LinkCompatError> {
|
||||
let text = std::str::from_utf8(field)
|
||||
.map_err(|_| LinkCompatError::InvalidArchive("member size is not ASCII"))?
|
||||
.trim();
|
||||
if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||
return Err(LinkCompatError::InvalidArchive(
|
||||
"member size is not decimal",
|
||||
));
|
||||
}
|
||||
text.parse()
|
||||
.map_err(|_| LinkCompatError::InvalidArchive("member size is out of range"))
|
||||
}
|
||||
|
||||
fn count_occurrences(haystack: &[u8], needle: &[u8]) -> usize {
|
||||
haystack
|
||||
.windows(needle.len())
|
||||
.filter(|candidate| *candidate == needle)
|
||||
.count()
|
||||
}
|
||||
|
||||
fn replace_equal_length(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) {
|
||||
debug_assert_eq!(needle.len(), replacement.len());
|
||||
let mut offset = 0_usize;
|
||||
while let Some(relative) = haystack[offset..]
|
||||
.windows(needle.len())
|
||||
.position(|candidate| candidate == needle)
|
||||
{
|
||||
let start = offset + relative;
|
||||
let end = start + needle.len();
|
||||
haystack[start..end].copy_from_slice(replacement);
|
||||
offset = end;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ pub const HARDENING_PROFILE_V1: &str = "op-auth-hardened-v1";
|
|||
pub struct ValidatedPrebuilt {
|
||||
pub abi_version: u32,
|
||||
pub signed_provenance: bool,
|
||||
pub archive_sha256: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -40,7 +41,9 @@ pub fn validate_prebuilt(
|
|||
let artifact_path = target_dir.join(artifact_name);
|
||||
let artifact = fs::read(&artifact_path)
|
||||
.map_err(|_| ProvenanceError("artifact is missing or unreadable"))?;
|
||||
let actual_sha256 = format!("{:x}", Sha256::digest(&artifact));
|
||||
let digest = Sha256::digest(&artifact);
|
||||
let actual_sha256 = format!("{digest:x}");
|
||||
let archive_sha256: [u8; 32] = digest.into();
|
||||
let expected_sha256 = read_trimmed(&target_dir.join("SHA256"), "SHA256 is missing")?;
|
||||
if !valid_hex(&expected_sha256, 64) || !actual_sha256.eq_ignore_ascii_case(&expected_sha256) {
|
||||
return Err(ProvenanceError("artifact SHA-256 does not match"));
|
||||
|
|
@ -56,6 +59,7 @@ pub fn validate_prebuilt(
|
|||
return Ok(ValidatedPrebuilt {
|
||||
abi_version,
|
||||
signed_provenance: false,
|
||||
archive_sha256,
|
||||
});
|
||||
}
|
||||
if version != package_version {
|
||||
|
|
@ -98,6 +102,7 @@ pub fn validate_prebuilt(
|
|||
Ok(ValidatedPrebuilt {
|
||||
abi_version,
|
||||
signed_provenance: true,
|
||||
archive_sha256,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
163
crates/op-auth-bridge/tests/prebuilt_link_compat.rs
Normal file
163
crates/op-auth-bridge/tests/prebuilt_link_compat.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
#[path = "../prebuilt_link_compat.rs"]
|
||||
mod prebuilt_link_compat;
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use prebuilt_link_compat::{stage_archive_for_rust_host, LinkCompatError};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
const HOST_PERSONALITY: &[u8] = b"rust_eh_personality";
|
||||
const PRIVATE_PERSONALITY: &[u8] = b"rust_eh_personalitx";
|
||||
|
||||
static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
struct TempDir(PathBuf);
|
||||
|
||||
impl TempDir {
|
||||
fn new() -> Self {
|
||||
let unique = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"op-auth-link-compat-{}-{unique}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespaces_personality_in_every_elf_and_coff_prebuilt() {
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let targets = [
|
||||
("x86_64-unknown-linux-gnu", "libop_auth.a"),
|
||||
("aarch64-unknown-linux-gnu", "libop_auth.a"),
|
||||
("x86_64-pc-windows-msvc", "op_auth.lib"),
|
||||
("aarch64-pc-windows-msvc", "op_auth.lib"),
|
||||
];
|
||||
|
||||
for (target, artifact) in targets {
|
||||
let source = manifest_dir.join("prebuilt").join(target).join(artifact);
|
||||
let original = fs::read(&source).unwrap();
|
||||
let temp = TempDir::new();
|
||||
let staged = temp.0.join(artifact);
|
||||
let expected_sha256 = Sha256::digest(&original).into();
|
||||
let report = stage_archive_for_rust_host(&source, &staged, Some(expected_sha256)).unwrap();
|
||||
let staged_bytes = fs::read(&staged).unwrap();
|
||||
|
||||
assert!(
|
||||
report.renamed_occurrences > 0,
|
||||
"{target} must exercise the compatibility transform"
|
||||
);
|
||||
assert_eq!(
|
||||
report.existing_private_occurrences, 0,
|
||||
"{target} unexpectedly already contains the private symbol"
|
||||
);
|
||||
assert_eq!(fs::read(&source).unwrap(), original);
|
||||
assert_eq!(staged_bytes.len(), original.len());
|
||||
assert!(!contains(&staged_bytes, HOST_PERSONALITY));
|
||||
assert!(contains(&staged_bytes, PRIVATE_PERSONALITY));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn already_namespaced_archive_is_copied_without_further_changes() {
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let source = manifest_dir
|
||||
.join("prebuilt")
|
||||
.join("x86_64-unknown-linux-gnu")
|
||||
.join("libop_auth.a");
|
||||
let first_temp = TempDir::new();
|
||||
let first = first_temp.0.join("libop_auth.a");
|
||||
stage_archive_for_rust_host(&source, &first, None).unwrap();
|
||||
|
||||
let second_temp = TempDir::new();
|
||||
let second = second_temp.0.join("libop_auth.a");
|
||||
let report = stage_archive_for_rust_host(&first, &second, None).unwrap();
|
||||
assert_eq!(report.renamed_occurrences, 0);
|
||||
assert!(report.existing_private_occurrences > 0);
|
||||
assert_eq!(fs::read(first).unwrap(), fs::read(second).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_or_conflicting_archives_without_output() {
|
||||
let temp = TempDir::new();
|
||||
let malformed = temp.0.join("malformed.a");
|
||||
fs::write(&malformed, b"not an archive").unwrap();
|
||||
let destination = temp.0.join("staged.a");
|
||||
assert!(matches!(
|
||||
stage_archive_for_rust_host(&malformed, &destination, None),
|
||||
Err(LinkCompatError::InvalidArchive(_))
|
||||
));
|
||||
assert!(!destination.exists());
|
||||
|
||||
let conflicting = temp.0.join("conflicting.a");
|
||||
fs::write(
|
||||
&conflicting,
|
||||
archive_with_member([HOST_PERSONALITY, b"\0", PRIVATE_PERSONALITY, b"\0"].concat()),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
stage_archive_for_rust_host(&conflicting, &destination, None),
|
||||
Err(LinkCompatError::ConflictingPersonalitySymbols)
|
||||
));
|
||||
assert!(!destination.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_to_rewrite_the_source_path() {
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let source = manifest_dir
|
||||
.join("prebuilt")
|
||||
.join("x86_64-unknown-linux-gnu")
|
||||
.join("libop_auth.a");
|
||||
assert!(matches!(
|
||||
stage_archive_for_rust_host(&source, &source, None),
|
||||
Err(LinkCompatError::SourceEqualsDestination)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_source_bytes_that_do_not_match_validated_digest() {
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let source = manifest_dir
|
||||
.join("prebuilt")
|
||||
.join("x86_64-unknown-linux-gnu")
|
||||
.join("libop_auth.a");
|
||||
let temp = TempDir::new();
|
||||
let destination = temp.0.join("libop_auth.a");
|
||||
assert!(matches!(
|
||||
stage_archive_for_rust_host(&source, &destination, Some([0_u8; 32])),
|
||||
Err(LinkCompatError::SourceDigestMismatch)
|
||||
));
|
||||
assert!(!destination.exists());
|
||||
}
|
||||
|
||||
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
|
||||
haystack
|
||||
.windows(needle.len())
|
||||
.any(|candidate| candidate == needle)
|
||||
}
|
||||
|
||||
fn archive_with_member(mut payload: Vec<u8>) -> Vec<u8> {
|
||||
let payload_len = payload.len();
|
||||
let mut archive = b"!<arch>\n".to_vec();
|
||||
let header = format!(
|
||||
"{:<16}{:<12}{:<6}{:<6}{:<8}{:<10}`\n",
|
||||
"fixture.o/", 0, 0, 0, 0, payload_len
|
||||
);
|
||||
assert_eq!(header.len(), 60);
|
||||
archive.extend_from_slice(header.as_bytes());
|
||||
archive.append(&mut payload);
|
||||
if !payload_len.is_multiple_of(2) {
|
||||
archive.push(b'\n');
|
||||
}
|
||||
archive
|
||||
}
|
||||
Loading…
Reference in a new issue