Skip to content

Commit 12b9294

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

File tree

3 files changed

+371
-3
lines changed

3 files changed

+371
-3
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
func uint16(_ a: UInt8, _ b: UInt8, endianness: ELF.Endianness) -> UInt16 {
16+
switch endianness {
17+
case .littleEndian:
18+
return UInt16(a) &<< 0 &+ UInt16(b) &<< 8
19+
case .bigEndian:
20+
return UInt16(a) &<< 8 &+ UInt16(b) &<< 0
21+
}
22+
}
23+
24+
struct ELF: Equatable {
25+
enum Endianness: UInt8 {
26+
case littleEndian = 0x01
27+
case bigEndian = 0x02
28+
}
29+
30+
enum Encoding: UInt8 {
31+
case bits32 = 0x01
32+
case bits64 = 0x02
33+
}
34+
35+
enum ObjectType: Equatable {
36+
case none
37+
case relocatable
38+
case executable
39+
case shared
40+
case core
41+
case reservedOS(UInt16)
42+
case reservedCPU(UInt16)
43+
case unknown(UInt16)
44+
45+
init?(rawValue: UInt16) {
46+
switch rawValue {
47+
case 0x0000: self = .none
48+
case 0x0001: self = .relocatable
49+
case 0x0002: self = .executable
50+
case 0x0003: self = .shared
51+
case 0x0004: self = .core
52+
53+
// Reserved for OS-specific use
54+
case 0xfe00...0xfeff: self = .reservedOS(rawValue)
55+
56+
// Reserved for CPU-specific use
57+
case 0xff00...0xffff: self = .reservedCPU(rawValue)
58+
59+
default: return nil
60+
}
61+
}
62+
}
63+
64+
enum ABI: Equatable {
65+
case SysV
66+
case Linux
67+
case unknown(UInt8)
68+
69+
init(rawValue: UInt8) {
70+
switch rawValue {
71+
case 0x00: self = .SysV
72+
case 0x03: self = .Linux
73+
default: self = .unknown(rawValue)
74+
}
75+
}
76+
}
77+
78+
enum ISA: Equatable {
79+
case x86_64
80+
case aarch64
81+
case unknown(UInt16)
82+
83+
init(rawValue: UInt16) {
84+
switch rawValue {
85+
case 0x003e: self = .x86_64
86+
case 0x00b7: self = .aarch64
87+
default: self = .unknown(rawValue)
88+
}
89+
}
90+
}
91+
92+
var encoding: Encoding
93+
var endianness: Endianness
94+
var ABI: ABI
95+
var objectType: ObjectType
96+
var ISA: ISA
97+
98+
static func read(_ bytes: [UInt8]) -> ELF? {
99+
// ELF header field addresses
100+
//
101+
// The ELF format can store binaries for 32-bit and 64-bit systems,
102+
// using little-endian and big-endian data encoding.
103+
//
104+
// All multibyte fields are stored using the endianness of the target
105+
// system. Read the EI_DATA field to find the endianness of the file.
106+
//
107+
// The ELF magic number is *not* a multibyte field. It is defined as a
108+
// string of 4 individual bytes and is the same for little-endian and
109+
// big-endian systems.
110+
//
111+
// Some fields are different sizes in 32-bit and 64-bit ELF files, but
112+
// these occur after all the fields we need to read for basic file type
113+
// identification, so all our offsets are the same on 32-bit and 64-bit systems.
114+
115+
enum Field {
116+
static let EI_MAGIC = 0x0...0x3 // ELF magic number: string of 4 bytes, not a UInt32; no endianness
117+
static let EI_CLASS = 0x4 // ELF class (word size): 1 byte
118+
static let EI_DATA = 0x5 // Data encoding (endianness): 1 byte
119+
static let EI_VERSION = 0x6 // ELF version: 1 byte
120+
static let EI_OSABI = 0x7 // Operating system/ABI identification: 1 byte
121+
122+
// These fields are multibyte, so endianness must be considered,
123+
// but all the fields we need are the same length in 32-bit and 64-bit
124+
// ELF files, so addresses do not change.
125+
static let EI_TYPE = 0x10...0x11 // Object type: 2 bytes
126+
static let EI_MACHINE = 0x12...0x13 // Machine ISA: 2 bytes
127+
}
128+
129+
guard bytes.count > 0x13 else {
130+
return nil
131+
}
132+
133+
// An ELF file starts with a magic number which is the same in either endianness.
134+
// The only defined ELF header version is 1.
135+
guard Array(bytes[Field.EI_MAGIC]) == Array("\u{7f}ELF".utf8) && bytes[Field.EI_VERSION] == 1 else {
136+
return nil
137+
}
138+
139+
// Offsets in an ELF file may be either 32-bit or 64-bit.
140+
// None of the fields we read are offsets, but we may still want to distinguish between 32-bit and 64-bit executables.
141+
guard let encoding = ELF.Encoding(rawValue: bytes[Field.EI_CLASS]) else {
142+
return nil
143+
}
144+
145+
// Object type and machine ISA are multibyte fields, so we must be prepared to handle endianness.
146+
guard let endianness = ELF.Endianness(rawValue: bytes[Field.EI_DATA]) else {
147+
return nil
148+
}
149+
150+
guard let objectType = ELF.ObjectType(rawValue: uint16(bytes[0x10], bytes[0x11], endianness: endianness)) else {
151+
return nil
152+
}
153+
154+
return ELF(
155+
encoding: encoding,
156+
endianness: endianness,
157+
ABI: .init(rawValue: bytes[Field.EI_OSABI]),
158+
objectType: objectType,
159+
ISA: .init(rawValue: uint16(bytes[0x12], bytes[0x13], endianness: endianness))
160+
)
161+
}
162+
}

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)