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