#[cfg(feature = "opencl")]
use ocl::{
builders::DeviceSpecifier::TypeFlags,
flags::{DeviceType, MemFlags},
prm::Uint16,
Buffer, Context, Kernel, Platform, Program, Queue,
};
use sha1::{
compress,
digest::{
generic_array::{ArrayLength, GenericArray},
BlockInput, Digest,
},
Sha1,
};
#[cfg(feature = "opencl")]
use std::convert::TryInto;
use std::{
cmp::Ord,
fmt::Debug,
ops::Range,
slice::from_raw_parts_mut,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
thread::{spawn, JoinHandle},
};
#[derive(Debug, PartialEq)]
pub struct HashSearchWorker {
processed_commit: ProcessedCommit,
desired_prefix: HashPrefix,
search_space: Range<u64>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct HashPrefix {
data: [u32; 5],
mask: [u32; 5],
}
#[derive(Debug, PartialEq, Clone)]
struct ProcessedCommit {
data: Vec<u8>,
commit_range: Range<usize>,
num_static_blocks: usize,
}
#[derive(Debug)]
struct PartiallyHashedCommit<'a> {
intermediate_sha1_state: [u32; 5],
dynamic_blocks: &'a mut [GenericArray<u8, <Sha1 as BlockInput>::BlockSize>],
}
const SHA1_INITIAL_STATE: [u32; 5] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
#[derive(Debug, PartialEq)]
pub struct HashedCommit {
pub commit: Vec<u8>,
pub hash: String,
}
impl HashSearchWorker {
pub fn new(current_commit: &[u8], desired_prefix: HashPrefix) -> Self {
Self {
processed_commit: ProcessedCommit::new(current_commit),
desired_prefix,
search_space: 0..(1 << 48),
}
}
pub fn with_capped_search_space(mut self, workload: u64) -> Self {
self.search_space = self.search_space.start
..Ord::min(self.search_space.end, self.search_space.start + workload);
self
}
fn split_search_space(self, divisor: u64) -> impl Iterator<Item = Self> {
let amount_per_worker = (self.search_space.end - self.search_space.start) / divisor;
(0..divisor).map(move |index| {
let range_start = index * amount_per_worker + self.search_space.start;
let range_end = if index < divisor - 1 {
range_start + amount_per_worker
} else {
self.search_space.end
};
Self {
processed_commit: self.processed_commit.clone(),
desired_prefix: self.desired_prefix.clone(),
search_space: range_start..range_end,
}
})
}
pub fn search(self) -> Option<HashedCommit> {
#[cfg(feature = "opencl")]
if Self::gpus_available() {
return self.search_with_gpu().unwrap();
}
self.search_with_cpus()
}
#[cfg(feature = "opencl")]
fn gpus_available() -> bool {
Platform::first().is_ok()
&& !TypeFlags(DeviceType::GPU)
.to_device_list(None::<Platform>)
.unwrap()
.is_empty()
}
#[allow(clippy::needless_collect)]
fn search_with_cpus(self) -> Option<HashedCommit> {
let thread_count = num_cpus::get_physical();
let lame_duck_cancel_signal = Arc::new(AtomicBool::new(false));
let (shared_sender, receiver) = mpsc::channel();
let _handles = self
.split_search_space(thread_count as u64)
.map(|worker| {
let result_sender = shared_sender.clone();
let worker_cancel_signal = Arc::clone(&lame_duck_cancel_signal);
spawn(move || {
let _ = result_sender
.send(worker.search_with_cpu_single_threaded(worker_cancel_signal));
})
})
.collect::<Vec<JoinHandle<()>>>();
for _ in 0..thread_count {
if let Some(result) = receiver.recv().unwrap() {
lame_duck_cancel_signal.store(true, Ordering::Relaxed);
#[cfg(debug_assertions)]
_handles
.into_iter()
.map(JoinHandle::join)
.collect::<Result<Vec<_>, _>>()
.unwrap();
return Some(result);
}
}
None
}
#[inline(never)]
fn search_with_cpu_single_threaded(
self,
lame_duck_cancel_signal: Arc<AtomicBool>,
) -> Option<HashedCommit> {
let HashSearchWorker {
search_space,
desired_prefix,
mut processed_commit,
} = self;
let mut partially_hashed_commit = processed_commit.as_partially_hashed_commit();
let lame_duck_check_interval = Ord::min(search_space.end - search_space.start, 1 << 20);
for base_padding_specifier in search_space.step_by(lame_duck_check_interval as usize) {
for index_in_interval in 0..lame_duck_check_interval {
partially_hashed_commit.scatter_padding(base_padding_specifier + index_in_interval);
if desired_prefix.matches(&partially_hashed_commit.current_hash()) {
return Some(HashedCommit::new(processed_commit.commit()));
}
}
if lame_duck_cancel_signal.load(Ordering::Relaxed) {
break;
}
}
None
}
#[cfg(feature = "opencl")]
fn search_with_gpu(self) -> ocl::Result<Option<HashedCommit>> {
let HashSearchWorker {
search_space,
desired_prefix,
mut processed_commit,
} = self;
let mut partially_hashed_commit = processed_commit.as_partially_hashed_commit();
let num_threads = *[
desired_prefix.estimated_hashes_needed().saturating_mul(4),
search_space.end - search_space.start,
1 << 22,
]
.iter()
.min()
.unwrap() as usize;
assert!(num_threads < u32::MAX as usize);
let devices = TypeFlags(DeviceType::GPU).to_device_list(Some(Platform::default()))?[0];
let context = Context::builder().devices(devices).build()?;
let queue = Queue::new(&context, devices, None)?;
let mut successful_match_receiver_host_handle = [u32::MAX];
let successful_match_receiver = Buffer::builder()
.queue(queue.clone())
.len(1)
.flags(MemFlags::WRITE_ONLY)
.copy_host_slice(&successful_match_receiver_host_handle)
.build()?;
let mut kernel = Kernel::builder()
.name("scatter_padding_and_find_match")
.program(
&Program::builder()
.binaries(&[
&include_bytes!(concat!(env!("OUT_DIR"), "/sha1_prefix_matcher"))[..],
])
.build(&context)?,
)
.arg(
&Buffer::builder()
.queue(queue.clone())
.len(desired_prefix.data.len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(&desired_prefix.data[..])
.build()?,
)
.arg(
&Buffer::builder()
.queue(queue.clone())
.len(desired_prefix.mask.len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(&desired_prefix.mask[..])
.build()?,
)
.arg(
&Buffer::builder()
.queue(queue.clone())
.len(partially_hashed_commit.intermediate_sha1_state.len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(&partially_hashed_commit.intermediate_sha1_state)
.build()?,
)
.arg(
&Buffer::builder()
.queue(queue.clone())
.len(partially_hashed_commit.dynamic_blocks.len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(
&partially_hashed_commit
.dynamic_blocks
.iter()
.map(|&block| encode_into_opencl_vector(block))
.collect::<Vec<_>>(),
)
.build()?,
)
.arg(partially_hashed_commit.dynamic_blocks.len())
.arg(&successful_match_receiver)
.queue(queue)
.global_work_size(num_threads)
.build()?;
for base_padding_specifier in search_space.step_by(num_threads) {
kernel.set_default_global_work_offset((base_padding_specifier,).into());
unsafe {
kernel.enq()?;
}
successful_match_receiver
.read(&mut successful_match_receiver_host_handle[..])
.enq()?;
if successful_match_receiver_host_handle[0] != u32::MAX {
let successful_padding_specifier =
base_padding_specifier + (successful_match_receiver_host_handle[0] as u64);
partially_hashed_commit.scatter_padding(successful_padding_specifier);
assert!(
desired_prefix.matches(&partially_hashed_commit.current_hash()),
"\
A GPU search reported a commit with a successful match, but when that \
commit was hashed in postprocessing, it didn't match the desired prefix. \
This is a bug. The most likely explanation is that the two implementations of \
`scatter_padding` in Rust and OpenCL (or the implementations of SHA1) have diverged \
from each other.\n\npartial commit:\n\t{:?}\ndesired prefix:\n\t{:?}\ncommit hash \
produced during postprocessing:{:?}\n\tpadding specifier: {}",
partially_hashed_commit,
desired_prefix,
partially_hashed_commit.current_hash(),
successful_padding_specifier,
);
return Ok(Some(HashedCommit::new(processed_commit.commit())));
}
}
Ok(None)
}
}
impl ProcessedCommit {
const DYNAMIC_PADDING_LENGTH: usize = 48;
fn new(original_commit: &[u8]) -> Self {
let padding_insertion_point = Self::get_padding_insertion_point(original_commit);
let replaceable_padding_size = original_commit[padding_insertion_point..]
.iter()
.take_while(|&&byte| byte == b' ' || byte == b'\t')
.count();
let static_padding_length = Self::compute_static_padding_length(
padding_insertion_point,
original_commit.len() - replaceable_padding_size + Self::DYNAMIC_PADDING_LENGTH,
);
let commit_length = original_commit.len() - replaceable_padding_size
+ static_padding_length
+ Self::DYNAMIC_PADDING_LENGTH;
let mut data = format!("commit {}\0", commit_length).into_bytes();
let commit_range = data.len()..(data.len() + commit_length);
data.extend(&original_commit[..padding_insertion_point]);
data.resize(data.len() + static_padding_length, b' ');
assert_eq!(data.len() % 64, 0);
let num_static_blocks = data.len() / 64;
data.resize(data.len() + Self::DYNAMIC_PADDING_LENGTH, b'\t');
data.extend(&original_commit[padding_insertion_point + replaceable_padding_size..]);
assert_eq!(data.len(), commit_range.end);
data.push(0x80);
data.resize(
data.len() + (56 - data.len() as isize).rem_euclid(64) as usize,
0,
);
data.extend_from_slice(&(commit_range.end as u64 * 8).to_be_bytes());
assert_eq!(data.len() % 64, 0);
Self {
data,
commit_range,
num_static_blocks,
}
}
fn get_padding_insertion_point(commit: &[u8]) -> usize {
let mut found_gpgsig_line = false;
const SIGNATURE_MARKER: &[u8] = b"-----END PGP SIGNATURE-----";
for index in 0..commit.len() {
if commit[index..].starts_with(b"\ngpgsig ") {
found_gpgsig_line = true;
} else if !found_gpgsig_line && commit[index..].starts_with(b"\n\n") {
break;
} else if found_gpgsig_line && commit[index..].starts_with(SIGNATURE_MARKER) {
return index + SIGNATURE_MARKER.len();
}
}
commit.len()
- commit
.iter()
.rev()
.take_while(|&&byte| byte == b' ' || byte == b'\t' || byte == b'\n')
.count()
}
fn compute_static_padding_length(
commit_length_before_static_padding: usize,
commit_length_excluding_static_padding: usize,
) -> usize {
let compute_alignment = |padding_len: usize| {
(format!(
"commit {}\0",
commit_length_excluding_static_padding + padding_len
)
.len()
+ commit_length_before_static_padding
+ padding_len)
% 64
};
let prefix_length_estimate = format!("commit {}\0", commit_length_excluding_static_padding)
.len()
+ commit_length_before_static_padding;
let initial_padding_length_guess = (64 - prefix_length_estimate % 64) % 64;
let static_padding_length = if compute_alignment(initial_padding_length_guess) == 0 {
initial_padding_length_guess
} else if compute_alignment(initial_padding_length_guess - 1) == 0 {
initial_padding_length_guess - 1
} else {
initial_padding_length_guess + 63
};
assert_eq!(0, compute_alignment(static_padding_length));
debug_assert!((0..static_padding_length).all(|len| compute_alignment(len) != 0));
static_padding_length
}
fn commit(&self) -> &[u8] {
&self.data[self.commit_range.clone()]
}
fn as_partially_hashed_commit(&mut self) -> PartiallyHashedCommit {
let (static_blocks, dynamic_blocks) =
as_chunks_mut::<u8, <Sha1 as BlockInput>::BlockSize>(&mut self.data[..])
.0
.split_at_mut(self.num_static_blocks);
let mut intermediate_sha1_state = SHA1_INITIAL_STATE;
compress(&mut intermediate_sha1_state, static_blocks);
PartiallyHashedCommit {
intermediate_sha1_state,
dynamic_blocks,
}
}
}
impl<'a> PartiallyHashedCommit<'a> {
#[inline(always)]
fn dynamic_padding_mut(&mut self) -> &mut [u8] {
&mut self.dynamic_blocks[0][..48]
}
#[inline(always)]
fn scatter_padding(&mut self, padding_specifier: u64) {
for (padding_chunk, &padding_specifier_byte) in self
.dynamic_padding_mut()
.chunks_exact_mut(8)
.zip(padding_specifier.to_le_bytes().iter())
{
padding_chunk.copy_from_slice(&PADDING_CHUNKS[padding_specifier_byte as usize]);
}
}
#[inline(always)]
fn current_hash(&self) -> [u32; 5] {
let mut sha1_hash = self.intermediate_sha1_state;
compress(&mut sha1_hash, self.dynamic_blocks);
sha1_hash
}
}
impl HashPrefix {
pub fn new(prefix: &str) -> Option<Self> {
if prefix.len() > 40 {
return None;
}
let contains_only_valid_characters = prefix.chars().all(|c| {
('0'..='9').contains(&c) || ('a'..='f').contains(&c) || ('A'..='F').contains(&c)
});
if !contains_only_valid_characters {
return None;
}
let mut data = [0u32; 5];
let mut mask = [0u32; 5];
for (i, chunk) in prefix.as_bytes().chunks(8).enumerate() {
let value =
u32::from_str_radix(&String::from_utf8(chunk.to_vec()).unwrap(), 16).unwrap();
let num_unset_bits = 32 - 4 * chunk.len();
data[i] = value << num_unset_bits;
mask[i] = u32::MAX >> num_unset_bits << num_unset_bits;
}
Some(HashPrefix { data, mask })
}
#[inline(always)]
fn matches(&self, hash: &[u32; 5]) -> bool {
hash.iter()
.zip(&self.mask)
.map(|(&hash_word, &mask_word)| hash_word & mask_word)
.zip(&self.data)
.all(|(masked_hash_word, &desired_prefix_word)| masked_hash_word == desired_prefix_word)
}
#[cfg(feature = "opencl")]
fn estimated_hashes_needed(&self) -> u64 {
2u64.saturating_pow(self.mask.iter().map(|word| word.count_ones()).sum())
}
}
impl Default for HashPrefix {
fn default() -> Self {
HashPrefix::new("0000000").unwrap()
}
}
impl HashedCommit {
fn new(commit: &[u8]) -> Self {
Self {
commit: commit.to_vec(),
hash: hash_git_commit(commit),
}
}
}
#[cfg(feature = "opencl")]
fn encode_into_opencl_vector(data: GenericArray<u8, <Sha1 as BlockInput>::BlockSize>) -> Uint16 {
let words: [u32; 16] = data
.chunks(4)
.map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap()))
.collect::<Vec<_>>()
.try_into()
.unwrap();
words.into()
}
pub fn hash_git_commit(commit: &[u8]) -> String {
Sha1::new()
.chain(format!("commit {}\0", commit.len()).as_bytes())
.chain(commit)
.finalize()
.iter()
.map(|&byte| format!("{:02x}", byte))
.collect::<String>()
}
fn as_chunks_mut<T, N: ArrayLength<T>>(slice: &mut [T]) -> (&mut [GenericArray<T, N>], &mut [T]) {
let chunk_size = N::to_usize();
assert_ne!(chunk_size, 0);
let len = slice.len() / chunk_size;
let (multiple_of_n, remainder) = slice.split_at_mut(len * chunk_size);
let array_slice =
unsafe { from_raw_parts_mut(multiple_of_n.as_mut_ptr().cast(), len) };
(array_slice, remainder)
}
static PADDING_CHUNKS: [[u8; 8]; 256] = {
let mut padding_chunks = [[0; 8]; 256];
let mut i = 0;
while i < 256 {
let mut j = 0;
while j < 8 {
padding_chunks[i][j] = if i & (0x80 >> j) == 0 { b' ' } else { b'\t' };
j += 1;
}
i += 1;
}
padding_chunks
};
#[cfg(test)]
mod tests;