lib/tldr/src/formats/elf/view.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const root = @import("../../root.zig");
  3 const format = @import("format.zig");
  4 
  5 const model = root.model;
  6 const ehdr_size = format.ehdr_size;
  7 const shdr_size = format.shdr_size;
  8 const sym_size = format.sym_size;
  9 const Header = format.Header;
 10 const SectionHeader = format.SectionHeader;
 11 const SymbolRecord = format.SymbolRecord;
 12 
 13 /// A section header found by name, with the file index of that header, returned
 14 /// by `find` so the caller gets the header and the index that names it.
 15 pub const Found = struct {
 16     index: u16,
 17     header: SectionHeader,
 18 };
 19 
 20 /// A view (a checked read of one ELF64 little-endian file) held in borrowed
 21 /// bytes, so tests read back objects and executables through it by section name
 22 /// or index. `parse` validates the file header, where the section table lies,
 23 /// and the section name table up front. Each read after that returns a slice of
 24 /// `bytes` checked against its bounds and allocates nothing. The caller keeps
 25 /// `bytes` alive and unchanged while it uses the view.
 26 pub const View = struct {
 27     bytes: []const u8,
 28     header: Header,
 29     names: []const u8,
 30 
 31     /// Validates the file header and section table of `bytes` and returns a
 32     /// view (a checked read of one ELF64 little-endian file) that borrows them.
 33     /// Because every read goes through that table, a file with no section table
 34     /// returns `error.MissingSection` here, so no later read checks for it
 35     /// again. The call returns `error.InvalidElfHeader` when the section name
 36     /// table index is past the table, and `error.InvalidRange` when the table
 37     /// or the name table lies outside `bytes`.
 38     pub fn parse(bytes: []const u8) model.Error!View {
 39         const header = try format.readHeader(bytes);
 40         if (header.shnum == 0) return error.MissingSection;
 41         if (header.shstrndx >= header.shnum) return error.InvalidElfHeader;
 42         try format.requireRange(bytes, header.shoff, @as(u64, header.shnum) * shdr_size);
 43 
 44         const name_table_offset = header.shoff + @as(u64, header.shstrndx) * shdr_size;
 45         const name_table = format.readSectionHeader(bytes, name_table_offset);
 46         return .{
 47             .bytes = bytes,
 48             .header = header,
 49             .names = try format.sectionBytes(bytes, name_table),
 50         };
 51     }
 52 
 53     pub fn sectionCount(self: View) u16 {
 54         return self.header.shnum;
 55     }
 56 
 57     /// Reads the section header at `index`, for example the string table a
 58     /// symbol table links to. `parse` already proved the whole table lies
 59     /// within the file, so the one requirement left is `index` below the
 60     /// section count, which a debug assertion checks.
 61     pub fn section(self: View, index: u16) SectionHeader {
 62         std.debug.assert(index < self.header.shnum);
 63         const offset = self.header.shoff + @as(u64, index) * shdr_size;
 64         return format.readSectionHeader(self.bytes, offset);
 65     }
 66 
 67     pub fn sectionName(self: View, header: SectionHeader) model.Error![]const u8 {
 68         return format.stringFromTable(self.names, header.name_offset);
 69     }
 70 
 71     /// Returns the first section named `name` with its index, or null, so tests
 72     /// can look sections up by name. The scan runs in file order, so when two
 73     /// sections share a name the lower index wins. The call returns
 74     /// `error.InvalidStringTable` when a section's name starts outside the
 75     /// section name table or runs to its end with no terminating zero byte.
 76     pub fn find(self: View, name: []const u8) model.Error!?Found {
 77         var index: u16 = 0;
 78         while (index < self.header.shnum) : (index += 1) {
 79             const header = self.section(index);
 80             if (std.mem.eql(u8, try self.sectionName(header), name)) {
 81                 return .{ .index = index, .header = header };
 82             }
 83         }
 84         return null;
 85     }
 86 
 87     /// Returns the file bytes of a section as a slice of `bytes`, so tests can
 88     /// compare a section's contents with what they wrote. The call returns an
 89     /// empty slice for a NOBITS section, because it has no bytes in the file.
 90     /// The call returns `error.InvalidRange` when the section extends past the
 91     /// file.
 92     pub fn sectionBytes(self: View, header: SectionHeader) model.Error![]const u8 {
 93         return format.sectionBytes(self.bytes, header);
 94     }
 95 
 96     pub fn symbolCount(self: View, symtab: SectionHeader) model.Error!usize {
 97         return (try self.sectionBytes(symtab)).len / sym_size;
 98     }
 99 
100     pub fn symbol(self: View, symtab: SectionHeader, index: usize) model.Error!SymbolRecord {
101         const table = try self.sectionBytes(symtab);
102         if (index >= table.len / sym_size) return error.InvalidRange;
103         return format.readSymbolRecord(table[index * sym_size ..][0..sym_size]);
104     }
105 
106     pub fn string(self: View, strtab: SectionHeader, offset: u32) model.Error![]const u8 {
107         return format.stringFromTable(try self.sectionBytes(strtab), offset);
108     }
109 };
110 
111 const witness_text = "\x55\x48\x89\xe5";
112 const witness_strtab = "\x00main\x00";
113 const witness_shstrtab = "\x00.text\x00.symtab\x00.strtab\x00.shstrtab\x00";
114 const witness_text_offset = ehdr_size;
115 const witness_symtab_offset = witness_text_offset + 8;
116 const witness_symbol_count = 2;
117 const witness_symtab_size = witness_symbol_count * sym_size;
118 const witness_strtab_offset = witness_symtab_offset + witness_symtab_size;
119 const witness_shstrtab_offset = witness_strtab_offset + witness_strtab.len;
120 const witness_shoff = 160;
121 const witness_shnum = 5;
122 const witness_size = witness_shoff + witness_shnum * shdr_size;
123 
124 /// Writes a minimal relocatable object using the package's own header, section
125 /// and symbol record writers, so each test of a view (a borrowed, checked read
126 /// of an ELF64 file) starts from the object it writes. The object comes from
127 /// the same writers whose output the view reads, so a failing view test means
128 /// the writers and the view disagree about the format.
129 fn writeWitness(buffer: *[witness_size]u8) void {
130     @memset(buffer, 0);
131     (Header{
132         .shoff = witness_shoff,
133         .shnum = witness_shnum,
134         .shstrndx = 4,
135     }).write(buffer);
136     @memcpy(buffer[witness_text_offset..][0..witness_text.len], witness_text);
137     (SymbolRecord{}).write(buffer, witness_symtab_offset);
138     (SymbolRecord{
139         .name_offset = 1,
140         .info = format.elfSymbolInfo(std.elf.STB_GLOBAL, std.elf.STT_FUNC),
141         .section_index = 1,
142         .size = witness_text.len,
143     }).write(buffer, witness_symtab_offset + sym_size);
144     @memcpy(buffer[witness_strtab_offset..][0..witness_strtab.len], witness_strtab);
145     @memcpy(buffer[witness_shstrtab_offset..][0..witness_shstrtab.len], witness_shstrtab);
146 
147     (SectionHeader{}).write(buffer, witness_shoff);
148     (SectionHeader{
149         .name_offset = 1,
150         .section_type = std.elf.SHT_PROGBITS,
151         .flags = std.elf.SHF_ALLOC | std.elf.SHF_EXECINSTR,
152         .offset = witness_text_offset,
153         .size = witness_text.len,
154         .alignment = 1,
155     }).write(buffer, witness_shoff + shdr_size);
156     (SectionHeader{
157         .name_offset = 7,
158         .section_type = std.elf.SHT_SYMTAB,
159         .offset = witness_symtab_offset,
160         .size = witness_symtab_size,
161         .link = 3,
162         .info = 1,
163         .alignment = 8,
164         .entry_size = sym_size,
165     }).write(buffer, witness_shoff + 2 * shdr_size);
166     (SectionHeader{
167         .name_offset = 15,
168         .section_type = std.elf.SHT_STRTAB,
169         .offset = witness_strtab_offset,
170         .size = witness_strtab.len,
171         .alignment = 1,
172     }).write(buffer, witness_shoff + 3 * shdr_size);
173     (SectionHeader{
174         .name_offset = 23,
175         .section_type = std.elf.SHT_STRTAB,
176         .offset = witness_shstrtab_offset,
177         .size = witness_shstrtab.len,
178         .alignment = 1,
179     }).write(buffer, witness_shoff + 4 * shdr_size);
180 }
181 
182 test "ELF view reads back the records written into an object" {
183     var buffer: [witness_size]u8 = undefined;
184     writeWitness(&buffer);
185     const view = try View.parse(&buffer);
186 
187     try std.testing.expectEqual(@as(u16, witness_shnum), view.sectionCount());
188     try std.testing.expectEqual(std.elf.ET.REL, view.header.type);
189     try std.testing.expectEqualStrings("", try view.sectionName(view.section(0)));
190 
191     const text = (try view.find(".text")).?;
192     try std.testing.expectEqual(@as(u16, 1), text.index);
193     try std.testing.expectEqual(@as(u32, std.elf.SHT_PROGBITS), text.header.section_type);
194     try std.testing.expectEqualStrings(witness_text, try view.sectionBytes(text.header));
195     try std.testing.expectEqualStrings(".text", try view.sectionName(text.header));
196 
197     const symtab = (try view.find(".symtab")).?;
198     const strtab = view.section(@intCast(symtab.header.link));
199     const counted = try view.symbolCount(symtab.header);
200     try std.testing.expectEqual(@as(usize, witness_symbol_count), counted);
201 
202     const null_symbol = try view.symbol(symtab.header, 0);
203     try std.testing.expectEqual(SymbolRecord{}, null_symbol);
204     try std.testing.expectEqualStrings("", try view.string(strtab, null_symbol.name_offset));
205 
206     const main_symbol = try view.symbol(symtab.header, 1);
207     try std.testing.expectEqual(@as(u16, 1), main_symbol.section_index);
208     try std.testing.expectEqual(@as(u64, witness_text.len), main_symbol.size);
209     try std.testing.expectEqualStrings("main", try view.string(strtab, main_symbol.name_offset));
210 
211     try std.testing.expect((try view.find(".rodata")) == null);
212 }
213 
214 test "ELF view borrows nothing from a NOBITS section" {
215     var buffer: [witness_size]u8 = undefined;
216     writeWitness(&buffer);
217     const view = try View.parse(&buffer);
218 
219     const reserved = SectionHeader{
220         .section_type = std.elf.SHT_NOBITS,
221         .offset = witness_size * 4,
222         .size = 4096,
223     };
224     try std.testing.expectEqual(@as(usize, 0), (try view.sectionBytes(reserved)).len);
225 }
226 
227 test "ELF view refuses reads the file cannot satisfy" {
228     var buffer: [witness_size]u8 = undefined;
229     writeWitness(&buffer);
230 
231     try std.testing.expectError(error.InvalidRange, View.parse(buffer[0 .. witness_size - 1]));
232 
233     const view = try View.parse(&buffer);
234     const symtab = (try view.find(".symtab")).?.header;
235     const strtab = view.section(3);
236     try std.testing.expectError(error.InvalidRange, view.symbol(symtab, witness_symbol_count));
237     try std.testing.expectError(error.InvalidStringTable, view.string(strtab, witness_strtab.len));
238 
239     writeWitness(&buffer);
240     (Header{
241         .shoff = witness_shoff,
242         .shnum = witness_shnum,
243         .shstrndx = witness_shnum,
244     }).write(&buffer);
245     try std.testing.expectError(error.InvalidElfHeader, View.parse(&buffer));
246 
247     writeWitness(&buffer);
248     (Header{ .shoff = witness_shoff }).write(&buffer);
249     try std.testing.expectError(error.MissingSection, View.parse(&buffer));
250 }