1use ab_aligned_buffer::SharedAlignedBuffer;
4use ab_aligned_buffer::alignment::Align16;
5use ab_contract_file::{CONTRACT_FILE_MAGIC, ContractFileHeader, ContractFileMethodMetadata};
6use ab_contracts_common::metadata::decode::MetadataDecoder;
7use ab_contracts_common::{HOST_CALL_FN, HOST_CALL_FN_IMPORT, METADATA_STATIC_NAME_PREFIX};
8use ab_io_type::trivial_type::TrivialType;
9use anyhow::Context;
10use object::elf::{
11 EF_RISCV_RVC, ELFCLASS64, ELFDATA2LSB, ELFMAG, ELFOSABI_GNU, EM_RISCV, ET_DYN, FileHeader64,
12 FileVersion, Ident, R_RISCV_JUMP_SLOT, STB_GLOBAL, STV_DEFAULT,
13};
14use object::read::elf::{ElfFile, ElfFile64, FileHeader};
15use object::{
16 CompressedData, CompressionFormat, LittleEndian, Object, ObjectSection, ObjectSymbol,
17 ObjectSymbolTable, RelocationFlags, RelocationTarget, SymbolKind, SymbolSection, U16, U32, U64,
18 Wrap,
19};
20use std::collections::HashMap;
21use std::iter;
22use tracing::{debug, trace};
23
24fn is_correct_header(header: &FileHeader64<LittleEndian>) -> bool {
25 let expected_header = FileHeader64 {
26 e_ident: Ident {
27 magic: ELFMAG,
28 class: ELFCLASS64,
29 data: ELFDATA2LSB,
30 version: FileVersion::from_inner(1),
31 os_abi: ELFOSABI_GNU,
32 abi_version: 0,
33 padding: [0; _],
34 },
35 e_type: U16::new(LittleEndian, ET_DYN),
36 e_machine: U16::new(LittleEndian, EM_RISCV),
37 e_version: U32::new(LittleEndian, 1),
38 e_entry: U64::new(LittleEndian, 0),
39 e_phoff: header.e_phoff,
40 e_shoff: header.e_shoff,
41 e_flags: U32::new(LittleEndian, header.e_flags(LittleEndian) & EF_RISCV_RVC),
42 e_ehsize: U16::new(LittleEndian, 64),
43 e_phentsize: header.e_phentsize,
44 e_phnum: header.e_phnum,
45 e_shentsize: header.e_shentsize,
46 e_shnum: header.e_shnum,
47 e_shstrndx: header.e_shstrndx,
48 };
49
50 object::pod::bytes_of(header) == object::pod::bytes_of(&expected_header)
52}
53
54fn check_relocations(elf: &ElfFile<'_, FileHeader64<LittleEndian>>) -> anyhow::Result<()> {
55 let mut dynamic_relocations = elf.dynamic_relocations().into_iter().flatten();
56 let maybe_first_relocation = dynamic_relocations.next();
57
58 if dynamic_relocations.next().is_some() {
59 return Err(anyhow::anyhow!(
60 "Only a single PLT relocation for host function call import is allowed, make sure to \
61 build an optimized cdylib"
62 ));
63 }
64
65 let Some((address, relocation)) = maybe_first_relocation else {
66 return Ok(());
67 };
68
69 debug!(
70 %address,
71 ?relocation,
72 "Found a single relocation"
73 );
74
75 if relocation.flags()
78 != (RelocationFlags::Elf {
79 r_type: R_RISCV_JUMP_SLOT,
80 })
81 {
82 return Err(anyhow::anyhow!("Unexpected relocation: {relocation:?}"));
83 }
84
85 let RelocationTarget::Symbol(symbol_index) = relocation.target() else {
86 return Err(anyhow::anyhow!(
87 "Only a single PLT relocation for host function call import is allowed, make sure to \
88 build an optimized cdylib"
89 ));
90 };
91
92 let sym = elf
93 .dynamic_symbol_table()
94 .context("Failed to get dynamic symbol table")?
95 .symbol_by_index(symbol_index)
96 .context("Failed to get relocation symbol by its index")?;
97
98 let name = sym
99 .name()
100 .with_context(|| format!("Failed to get relocation symbol name: {relocation:?} {sym:?}"))?;
101 debug!(
102 %name,
103 "PLT relocation name"
104 );
105
106 if name != HOST_CALL_FN_IMPORT {
107 return Err(anyhow::anyhow!(
108 "Unexpected PLT relocation name {name}: {relocation:?} {sym:?}"
109 ));
110 }
111
112 if relocation.addend() != 0 || relocation.has_implicit_addend() {
113 return Err(anyhow::anyhow!(
114 "Unexpected PLT relocation {name}: {relocation:?} {sym:?}"
115 ));
116 }
117
118 Ok(())
119}
120
121#[derive(Debug, Copy, Clone)]
122struct ParsedSections {
123 metadata_section_offset: u64,
126 metadata_section_size: u64,
128 rodata_section_offset: u64,
130 rodata_section_size: u64,
132 metadata_rodata_padding: u64,
134 ro_memory_size: u64,
137 code_section_offset: u64,
139 code_section_size: u64,
141}
142
143fn parse_sections(elf: &ElfFile<'_, FileHeader64<LittleEndian>>) -> anyhow::Result<ParsedSections> {
144 let mut maybe_metadata_section = None;
145 let mut maybe_rodata_section = None;
146 let mut maybe_code_section = None;
147
148 for section in elf.sections() {
149 trace!(name = %section.name().unwrap_or_default(), "Processing section");
153
154 match section.name().context("Failed to get section name")? {
155 "ab-contract-metadata" => {
156 let CompressedData {
157 format,
158 data: _,
159 uncompressed_size,
160 } = section
161 .compressed_data()
162 .context("Failed to get section data")?;
163 if !matches!(format, CompressionFormat::None) {
164 return Err(anyhow::anyhow!(
165 "Section `ab-contract-metadata` is compressed with {format:?}, but shouldn't be"
166 ));
167 }
168 if uncompressed_size != section.size() {
169 return Err(anyhow::anyhow!(
170 "Section `ab-contract-metadata` has unexpected paddings: file size \
171 {uncompressed_size} != in-memory size {}",
172 section.size()
173 ));
174 }
175 maybe_metadata_section.replace(section);
176 }
177 ".rodata" => {
178 let CompressedData {
179 format,
180 data: _,
181 uncompressed_size,
182 } = section
183 .compressed_data()
184 .context("Failed to get section data")?;
185 if !matches!(format, CompressionFormat::None) {
186 return Err(anyhow::anyhow!(
187 "Section `.rodata` is compressed with {format:?}, but shouldn't be"
188 ));
189 }
190 if uncompressed_size != section.size() {
191 return Err(anyhow::anyhow!(
192 "Section `.rodata` has unexpected paddings: file size \
193 {uncompressed_size} != in-memory size {}",
194 section.size()
195 ));
196 }
197 maybe_rodata_section.replace(section);
198 }
199 ".text" => {
200 let CompressedData {
201 format,
202 data: _,
203 uncompressed_size,
204 } = section
205 .compressed_data()
206 .context("Failed to get section data")?;
207 if !matches!(format, CompressionFormat::None) {
208 return Err(anyhow::anyhow!(
209 "Section `.text` is compressed with {format:?}, but shouldn't be"
210 ));
211 }
212 if uncompressed_size != section.size() {
213 return Err(anyhow::anyhow!(
214 "Section `.text` has unexpected paddings: file size \
215 {uncompressed_size} != in-memory size {}",
216 section.size()
217 ));
218 }
219 maybe_code_section.replace(section);
220 }
221 _ => {
222 }
224 }
225 }
226
227 let Some(metadata_section) = maybe_metadata_section else {
228 return Err(anyhow::anyhow!("Section `ab-contract-metadata` not found"));
229 };
230 let Some(code_section) = maybe_code_section else {
231 return Err(anyhow::anyhow!("Section `.text` not found"));
232 };
233
234 let metadata_section_address = metadata_section.address();
235 let (metadata_section_offset, metadata_section_size) = metadata_section
236 .file_range()
237 .context("Failed to get `ab-contract-metadata` section range")?;
238 let code_section_address = code_section.address();
239 let (code_section_offset, code_section_size) = code_section
240 .file_range()
241 .context("Failed to get `.text` section range")?;
242
243 let (rodata_section_address, (rodata_section_offset, rodata_section_size)) =
244 match maybe_rodata_section {
245 Some(rodata_section) => (
246 rodata_section.address(),
247 rodata_section
248 .file_range()
249 .context("Failed to get `.rodata` section range")?,
250 ),
251 None => (metadata_section_address, (metadata_section_offset, 0)),
252 };
253
254 if metadata_section_offset.max(rodata_section_offset) > code_section_offset {
255 return Err(anyhow::anyhow!(
256 "`.text` section must be after `.rodata` and `ab-contract-metadata` sections: \
257 metadata_section_offset={metadata_section_offset}, \
258 rodata_section_offset={rodata_section_offset}, \
259 code_section_offset={code_section_offset}"
260 ));
261 }
262
263 let Some(ro_memory_size) =
267 code_section_address.checked_sub(metadata_section_address.min(rodata_section_address))
268 else {
269 return Err(anyhow::anyhow!(
270 "`.text` section must be after `.rodata` and `ab-contract-metadata` sections: \
271 metadata_section_address={metadata_section_address}, \
272 rodata_section_address={rodata_section_address}, \
273 code_section_address={code_section_address}"
274 ));
275 };
276
277 let metadata_rodata_padding = if metadata_section_address < rodata_section_address {
278 (rodata_section_address - metadata_section_address) - metadata_section_size
279 } else {
280 (metadata_section_address - rodata_section_address) - rodata_section_size
281 };
282
283 Ok(ParsedSections {
284 metadata_section_offset,
285 metadata_section_size,
286 rodata_section_offset,
287 rodata_section_size,
288 metadata_rodata_padding,
289 ro_memory_size,
290 code_section_offset,
291 code_section_size,
292 })
293}
294
295fn check_imports(elf: &ElfFile<'_, FileHeader64<LittleEndian>>) -> anyhow::Result<()> {
296 let mut imports = elf.imports().context("Failed to get imports")?;
297
298 let maybe_host_call_fn_import = imports.next().transpose()?;
299 if let Some(import) = maybe_host_call_fn_import
300 && import.name().into_name() != Some(HOST_CALL_FN_IMPORT.as_bytes())
301 {
302 return Err(anyhow::anyhow!(
303 "Expected import `{HOST_CALL_FN_IMPORT}`, got `{}`",
304 String::from_utf8_lossy(import.name().into_name().unwrap_or_default())
305 ));
306 }
307
308 if imports.next().is_some() {
309 return Err(anyhow::anyhow!(
310 "Expected at most one import, got {}",
311 imports.count() + 1 + usize::from(maybe_host_call_fn_import.is_some())
312 ));
313 }
314
315 Ok(())
316}
317
318#[derive(Debug, Copy, Clone)]
319struct ParsedExport {
320 offset: u64,
321 size: u64,
322}
323
324fn parse_exports<'a>(
325 elf: &'a ElfFile<'a, FileHeader64<LittleEndian>>,
326) -> anyhow::Result<HashMap<&'a str, ParsedExport>> {
327 elf.dynamic_symbols()
328 .enumerate()
329 .filter_map(|(index, symbol)| {
330 let name = match symbol.name() {
339 Ok(name) => name,
340 Err(error) => return Some(Err(error).context("Failed to get symbol name")),
341 };
342 let elf_symbol = symbol.elf_symbol();
343
344 if elf_symbol.st_bind() != STB_GLOBAL {
345 return Some(Err(anyhow::anyhow!(
346 "Non-STB_GLOBAL symbol {name}: {symbol:?}"
347 )));
348 }
349 if elf_symbol.st_visibility() != STV_DEFAULT {
350 return Some(Err(anyhow::anyhow!(
351 "Non-STV_DEFAULT symbol {name}: {symbol:?}"
352 )));
353 }
354 if elf_symbol.st_shndx.get(LittleEndian).is_reserved() {
355 return Some(Err(anyhow::anyhow!(
356 "Unexpected reserved section index for symbol {name}: {symbol:?}"
357 )));
358 }
359
360 match symbol.kind() {
361 SymbolKind::Unknown => {
362 if !(symbol.size() == 0 && name == HOST_CALL_FN_IMPORT) {
363 return Some(Err(anyhow::anyhow!(
364 "Unexpected unknown symbol {name}: {symbol:?}"
365 )));
366 }
367
368 None
369 }
370 SymbolKind::Text => {
371 let SymbolSection::Section(section_index) = symbol.section() else {
372 return Some(Err(anyhow::anyhow!(
373 "Unexpected section type for symbol {name}: {symbol:?}"
374 )));
375 };
376 let section = match elf.section_by_index(section_index) {
377 Ok(section) => section,
378 Err(error) => {
379 return Some(Err(error).context(format!(
380 "Failed to get section {section_index} for symbol {name}"
381 )));
382 }
383 };
384 let Some(offset_within_section) =
385 symbol.address().checked_sub(section.address())
386 else {
387 return Some(Err(anyhow::anyhow!(
388 "Invalid offset calculation for symbol {name}: \
389 address {} < section address {}",
390 symbol.address(),
391 section.address()
392 )));
393 };
394
395 let Some((section_offset, _section_size)) = section.file_range() else {
396 return Some(Err(anyhow::anyhow!(
397 "Failed to get file range for section {section_index} for symbol {name}"
398 )));
399 };
400 let offset = section_offset + offset_within_section;
401 let size = symbol.size();
402 debug!(
403 %index,
404 %name,
405 %offset,
406 %size,
407 "Found export function"
408 );
409
410 Some(Ok((name, ParsedExport { offset, size })))
411 }
412 SymbolKind::Data => {
413 if !name.starts_with(METADATA_STATIC_NAME_PREFIX) {
414 return Some(Err(anyhow::anyhow!(
415 "Unexpected STT_OBJECT {name}: {symbol:?}"
416 )));
417 }
418
419 None
420 }
421 _ => Some(Err(anyhow::anyhow!("Unexpected symbol {name}: {symbol:?}"))),
422 }
423 })
424 .collect()
425}
426
427fn extract_host_call_fn_offset(
428 input_file: &[u8],
429 parsed_exports: &mut HashMap<&str, ParsedExport>,
430) -> anyhow::Result<u64> {
431 let Some(host_call_fn) = parsed_exports.remove(HOST_CALL_FN) else {
432 return Ok(0);
433 };
434
435 if ![size_of::<u16>() as u64, size_of::<u32>() as u64].contains(&host_call_fn.size) {
436 return Err(anyhow::anyhow!(
437 "Host call function {HOST_CALL_FN} has invalid size {}",
438 host_call_fn.size
439 ));
440 }
441 let host_call_fn_offset = host_call_fn.offset;
442 input_file
443 .get(host_call_fn_offset as usize..)
444 .with_context(|| {
445 format!(
446 "Host call address {host_call_fn_offset} out of range of input file ({} bytes)",
447 input_file.len()
448 )
449 })?
450 .get(..host_call_fn.size as usize)
451 .context("Not enough bytes to get instructions of host call function")?;
452
453 Ok(host_call_fn_offset)
454}
455
456fn parse_metadata_methods(
457 parsed_exports: &mut HashMap<&str, ParsedExport>,
458 metadata_bytes: &[u8],
459) -> anyhow::Result<Vec<ParsedExport>> {
460 let mut metadata_methods = Vec::new();
461
462 let mut metadata_decoder = MetadataDecoder::new(metadata_bytes);
463
464 while let Some(maybe_metadata_item) = metadata_decoder.decode_next() {
465 let metadata_item = maybe_metadata_item.map_err(|error| {
466 anyhow::Error::msg(error.to_string()).context("Failed to decode metadata item")
467 })?;
468 debug!(?metadata_item, "Decoded metadata item");
469
470 let mut methods_metadata_decoder = metadata_item.into_decoder();
471 while let Some(method_metadata_decoder) = methods_metadata_decoder.decode_next() {
472 let (_, method_metadata_item) =
473 method_metadata_decoder.decode_next().map_err(|error| {
474 anyhow::Error::msg(error.to_string())
475 .context("Failed to decode method metadata")
476 })?;
477
478 trace!(?method_metadata_item, "Decoded method metadata item");
479
480 let method_name =
481 str::from_utf8(method_metadata_item.method_name).with_context(|| {
482 format!(
483 "Non-UTF-8 method name: {:?}",
484 method_metadata_item.method_name
485 )
486 })?;
487 let symbol = parsed_exports
488 .remove(method_name)
489 .with_context(|| anyhow::anyhow!("Method {method_name} not found in symbols"))?;
490
491 metadata_methods.push(symbol);
492 }
493 }
494
495 Ok(metadata_methods)
496}
497
498pub fn convert(input_file: &[u8]) -> anyhow::Result<Vec<u8>> {
500 let buffer = SharedAlignedBuffer::<Align16>::from_bytes(input_file);
501 let elf =
502 ElfFile64::<LittleEndian>::parse(buffer.as_slice()).context("Failed to parse ELF file")?;
503
504 if !is_correct_header(elf.elf_header()) {
505 return Err(anyhow::anyhow!(
506 "Invalid ELF header: {:?}",
507 elf.elf_header()
508 ));
509 }
510
511 check_relocations(&elf)?;
512 let ParsedSections {
513 metadata_section_offset,
514 metadata_section_size,
515 rodata_section_offset,
516 rodata_section_size,
517 metadata_rodata_padding,
518 ro_memory_size,
519 code_section_offset,
520 code_section_size,
521 } = parse_sections(&elf)?;
522
523 if metadata_section_size == 0 {
524 return Err(anyhow::anyhow!("Metadata not found"));
525 }
526
527 check_imports(&elf)?;
528
529 let mut parsed_exports = parse_exports(&elf)?;
530
531 let host_call_fn_offset = extract_host_call_fn_offset(input_file, &mut parsed_exports)?;
532
533 if host_call_fn_offset != 0 && host_call_fn_offset < code_section_offset {
534 return Err(anyhow::anyhow!(
535 "Host call function offset {host_call_fn_offset} is before `.text` section offset \
536 {code_section_offset}"
537 ));
538 }
539
540 let metadata_bytes = input_file
541 .get(metadata_section_offset as usize..)
542 .with_context(|| {
543 format!(
544 "Metadata offset {metadata_section_offset} out of range of input file ({} bytes)",
545 input_file.len()
546 )
547 })?
548 .get(..metadata_section_size as usize)
549 .with_context(|| format!("Metadata size {metadata_section_size} is invalid"))?;
550
551 let metadata_methods = parse_metadata_methods(&mut parsed_exports, metadata_bytes)?;
552
553 if !parsed_exports.is_empty() {
554 return Err(anyhow::anyhow!("Found unused exports: {parsed_exports:?}"));
555 }
556
557 let header_size = size_of::<ContractFileHeader>();
558 let methods_metadata_size = size_of::<ContractFileMethodMetadata>() * metadata_methods.len();
559 let header_with_methods_metadata_size = (header_size + methods_metadata_size) as u64;
560
561 let mut output_file = Vec::new();
562
563 let contract_file_header = ContractFileHeader {
565 magic: CONTRACT_FILE_MAGIC,
566 read_only_section_file_size: (metadata_section_size
567 + rodata_section_size
568 + metadata_rodata_padding)
569 .try_into()
570 .context("Read-only section size is over 32-bit")?,
571 read_only_section_memory_size: ro_memory_size
572 .try_into()
573 .context("Read-only section size is over 32-bit")?,
574 metadata_offset: {
575 let metadata_offset = if metadata_section_offset < rodata_section_offset {
576 header_with_methods_metadata_size
577 } else {
578 header_with_methods_metadata_size + rodata_section_size + metadata_rodata_padding
579 };
580
581 metadata_offset
582 .try_into()
583 .context("Metadata offset is over 32-bit")?
584 },
585 metadata_size: metadata_section_size
586 .try_into()
587 .context("Metadata size is over 16-bit")?,
588 num_methods: metadata_methods
589 .len()
590 .try_into()
591 .context("Number of methods is over 16-bit")?,
592 host_call_fn_offset: {
593 let host_call_fn_offset = if host_call_fn_offset == 0 {
594 0
595 } else {
596 header_with_methods_metadata_size
597 + (metadata_section_size + rodata_section_size + metadata_rodata_padding)
598 + (host_call_fn_offset - code_section_offset)
599 };
600
601 host_call_fn_offset
602 .try_into()
603 .context("Host call offset is over 32-bit")?
604 },
605 };
606 output_file.extend_from_slice(contract_file_header.as_bytes());
607
608 for metadata_method in metadata_methods {
610 let offset = header_with_methods_metadata_size
611 + (metadata_section_size + rodata_section_size + metadata_rodata_padding)
612 + (metadata_method.offset - code_section_offset);
613 let contract_file_function_metadata = ContractFileMethodMetadata {
614 offset: offset.try_into().context("Method offset is over 32-bit")?,
615 size: metadata_method
616 .size
617 .try_into()
618 .context("Method size is over 32-bit")?,
619 };
620 output_file.extend_from_slice(contract_file_function_metadata.as_bytes());
621 }
622
623 if metadata_section_offset < rodata_section_offset {
625 output_file.extend_from_slice(
626 &input_file[metadata_section_offset as usize..][..metadata_section_size as usize],
627 );
628 output_file.extend(iter::repeat_n(0, metadata_rodata_padding as usize));
629 output_file.extend_from_slice(
630 &input_file[rodata_section_offset as usize..][..rodata_section_size as usize],
631 );
632 } else {
633 output_file.extend_from_slice(
634 &input_file[rodata_section_offset as usize..][..rodata_section_size as usize],
635 );
636 output_file.extend(iter::repeat_n(0, metadata_rodata_padding as usize));
637 output_file.extend_from_slice(
638 &input_file[metadata_section_offset as usize..][..metadata_section_size as usize],
639 );
640 }
641
642 output_file.extend_from_slice(
644 &input_file[code_section_offset as usize..][..code_section_size as usize],
645 );
646
647 Ok(output_file)
650}