Skip to content

Commit 03befb7

Browse files
committed
containertool: Add basic ELF file type detection
1 parent 196a7ce commit 03befb7

File tree

3 files changed

+440
-3
lines changed

3 files changed

+440
-3
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftContainerPlugin open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
struct ArrayField<T: Collection> where T.Element == UInt8 {
16+
var start: Int
17+
var count: Int
18+
}
19+
20+
struct IntField<T: BinaryInteger> {
21+
var start: Int
22+
}
23+
24+
extension Array where Element == UInt8 {
25+
subscript(idx: ArrayField<[UInt8]>) -> [UInt8] {
26+
[UInt8](self[idx.start..<idx.start + idx.count])
27+
}
28+
29+
subscript(idx: IntField<UInt8>) -> UInt8 {
30+
self[idx.start]
31+
}
32+
33+
subscript(idx: IntField<UInt16>, endianness endianness: ELF.Endianness) -> UInt16 {
34+
let (a, b) = (UInt16(self[idx.start]), UInt16(self[idx.start + 1]))
35+
36+
switch endianness {
37+
case .littleEndian:
38+
return a &<< 0 &+ b &<< 8
39+
case .bigEndian:
40+
return a &<< 8 &+ b &<< 0
41+
}
42+
}
43+
}
44+
45+
/// ELF header
46+
///
47+
/// - https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
48+
/// - https://refspecs.linuxbase.org/elf/elf.pdf
49+
///
50+
/// This struct only defines enough fields to identify a valid ELF file
51+
/// and extract the type of object it contains, and the processor
52+
/// architecture and operating system ABI for which that object
53+
/// was created.
54+
struct ELF: Equatable {
55+
/// Multibyte ELF fields are stored in the native endianness of the target system.
56+
/// This field records the endianness of objects in the file.
57+
enum Endianness: UInt8 {
58+
case littleEndian = 0x01
59+
case bigEndian = 0x02
60+
}
61+
62+
/// Offsets (addresses) are stored as 32-bit or 64-bit integers.
63+
/// This field records the offset size used in objects in the file.
64+
/// Variable offset sizes mean that some fields are found at different
65+
/// offsets in 32-bit and 64-bit ELF files.
66+
enum Encoding: UInt8 {
67+
case bits32 = 0x01
68+
case bits64 = 0x02
69+
}
70+
71+
/// ELF files can hold a variety of different object types.
72+
/// This field records type of object in the file.
73+
/// The standard defines a number of fixed types but also
74+
/// reserves ranges of type numbers for to be used by
75+
/// specific operating systems and processors.
76+
enum Object: Equatable {
77+
case none
78+
case relocatable
79+
case executable
80+
case shared
81+
case core
82+
case reservedOS(UInt16)
83+
case reservedCPU(UInt16)
84+
case unknown(UInt16)
85+
86+
init?(rawValue: UInt16) {
87+
switch rawValue {
88+
case 0x0000: self = .none
89+
case 0x0001: self = .relocatable
90+
case 0x0002: self = .executable
91+
case 0x0003: self = .shared
92+
case 0x0004: self = .core
93+
94+
/// Reserved for OS-specific use
95+
case 0xfe00...0xfeff: self = .reservedOS(rawValue)
96+
97+
/// Reserved for CPU-specific use
98+
case 0xff00...0xffff: self = .reservedCPU(rawValue)
99+
100+
default: return nil
101+
}
102+
}
103+
}
104+
105+
/// The ABI used by the object in this ELF file. The standard reserves values for a variety of ABIs and operating systems; only a few are implemented here.
106+
enum ABI: Equatable {
107+
case SysV
108+
case Linux
109+
case unknown(UInt8)
110+
111+
init(rawValue: UInt8) {
112+
switch rawValue {
113+
case 0x00: self = .SysV
114+
case 0x03: self = .Linux
115+
default: self = .unknown(rawValue)
116+
}
117+
}
118+
}
119+
120+
/// The processor architecture used by the object in this ELF file. The standard reserves values for a variety of ISAs; only a few are implemented here.
121+
enum ISA: Equatable {
122+
case x86_64
123+
case aarch64
124+
case unknown(UInt16)
125+
126+
init(rawValue: UInt16) {
127+
switch rawValue {
128+
case 0x003e: self = .x86_64
129+
case 0x00b7: self = .aarch64
130+
default: self = .unknown(rawValue)
131+
}
132+
}
133+
}
134+
135+
var encoding: Encoding
136+
var endianness: Endianness
137+
var ABI: ABI
138+
var object: Object
139+
var ISA: ISA
140+
}
141+
142+
extension ELF {
143+
/// ELF header field addresses
144+
///
145+
/// The ELF format can store binaries for 32-bit and 64-bit systems,
146+
/// using little-endian and big-endian data encoding.
147+
///
148+
/// All multibyte fields are stored using the endianness of the target
149+
/// system. Read the EI_DATA field to find the endianness of the file.
150+
///
151+
/// Some fields are different sizes in 32-bit and 64-bit ELF files, but
152+
/// these occur after all the fields we need to read for basic file type
153+
/// identification, so all our offsets are the same on 32-bit and 64-bit systems.
154+
enum Field {
155+
/// ELF magic number: a string of 4 bytes, not a UInt32; no endianness
156+
static let EI_MAGIC = ArrayField<[UInt8]>(start: 0x0, count: 4)
157+
158+
/// ELF class (word size): 1 byte
159+
static let EI_CLASS = IntField<UInt8>(start: 0x4)
160+
161+
/// Data encoding (endianness): 1 byte
162+
static let EI_DATA = IntField<UInt8>(start: 0x5)
163+
164+
// ELF version: 1 byte
165+
static let EI_VERSION = IntField<UInt8>(start: 0x6)
166+
167+
// Operating system/ABI identification: 1 byte
168+
static let EI_OSABI = IntField<UInt8>(start: 0x7)
169+
170+
// The following fields are multibyte, so endianness must be considered,
171+
// All the fields we need are the same length in 32-bit and 64-bit
172+
// ELF files, so their offsets do not change.
173+
174+
/// Object type: 2 bytes
175+
static let EI_TYPE = IntField<UInt16>(start: 0x10)
176+
177+
//l Machine ISA (processor architecture): 2 bytes
178+
static let EI_MACHINE = IntField<UInt16>(start: 0x12)
179+
}
180+
181+
/// The initial magic number (4 bytes) which identifies an ELF file.
182+
///
183+
/// The ELF magic number is *not* a multibyte integer. It is defined as a
184+
/// string of 4 individual bytes and is the same for little-endian and
185+
/// big-endian ELF files.
186+
static let ELFMagic = Array("\u{7f}ELF".utf8)
187+
188+
/// Read enough of an ELF header from bytes to discover the object type,
189+
/// processor architecture and operating system ABI.
190+
static func read(_ bytes: [UInt8]) -> ELF? {
191+
// An ELF file starts with a magic number which is the same in either endianness.
192+
// The only defined ELF header version is 1.
193+
guard bytes.count > 0x13, bytes[Field.EI_MAGIC] == ELFMagic, bytes[Field.EI_VERSION] == 1 else {
194+
return nil
195+
}
196+
197+
guard
198+
let encoding = Encoding(rawValue: bytes[Field.EI_CLASS]),
199+
let endianness = Endianness(rawValue: bytes[Field.EI_DATA]),
200+
let object = Object(rawValue: bytes[Field.EI_TYPE, endianness: endianness])
201+
else {
202+
return nil
203+
}
204+
205+
return ELF(
206+
encoding: encoding,
207+
endianness: endianness,
208+
ABI: .init(rawValue: bytes[Field.EI_OSABI]),
209+
object: object,
210+
ISA: .init(rawValue: bytes[Field.EI_MACHINE, endianness: endianness])
211+
)
212+
}
213+
}

Sources/containertool/containertool.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
5151
var allowInsecureHttp: AllowHTTP?
5252

5353
@Option(help: "CPU architecture")
54-
private var architecture: String = ProcessInfo.processInfo.environment["CONTAINERTOOL_ARCHITECTURE"] ?? "amd64"
54+
private var architecture: String?
5555

5656
@Option(help: "Base image reference")
5757
private var from: String = ProcessInfo.processInfo.environment["CONTAINERTOOL_BASE_IMAGE"] ?? "swift:slim"
@@ -72,6 +72,9 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
7272
let baseimage = try ImageReference(fromString: from, defaultRegistry: defaultRegistry)
7373
var destination_image = try ImageReference(fromString: repository, defaultRegistry: defaultRegistry)
7474

75+
let executableURL = URL(fileURLWithPath: executable)
76+
let payload = try Data(contentsOf: executableURL)
77+
7578
let authProvider: AuthorizationProvider?
7679
if !netrc {
7780
authProvider = nil
@@ -110,6 +113,14 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
110113

111114
// MARK: Find the base image
112115

116+
let elfheader = ELF.read([UInt8](payload))
117+
let architecture =
118+
architecture
119+
?? ProcessInfo.processInfo.environment["CONTAINERTOOL_ARCHITECTURE"]
120+
?? elfheader?.ISA.containerArchitecture
121+
?? "amd64"
122+
if verbose { log("Base image architecture: \(architecture)") }
123+
113124
let baseimage_manifest: ImageManifest
114125
let baseimage_config: ImageConfiguration
115126
if let source {
@@ -137,8 +148,6 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
137148

138149
// MARK: Build the application layer
139150

140-
let executableURL = URL(fileURLWithPath: executable)
141-
let payload = try Data(contentsOf: executableURL)
142151
let payload_name = executableURL.lastPathComponent
143152
let tardiff = tar(payload, filename: payload_name)
144153
log("Built application layer")
@@ -228,3 +237,13 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
228237
print(destination_image)
229238
}
230239
}
240+
241+
extension ELF.ISA {
242+
var containerArchitecture: String? {
243+
switch self {
244+
case .x86_64: "amd64"
245+
case .aarch64: "arm64"
246+
default: nil
247+
}
248+
}
249+
}

0 commit comments

Comments
 (0)