Skip to content

containertool: Add basic ELF file type detection #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/interop_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,32 @@ jobs:
docker create --name second --pull always localhost:5000/layering_test
docker cp second:/payload second.payload
grep second second.payload

elf-detection-test:
name: ELF detection test
runs-on: ubuntu-latest
services:
registry:
image: registry:2
ports:
- 5000:5000
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Mark the workspace as safe
# https://github.com/actions/checkout/issues/766
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}

- name: Install the static SDK
run: |
swift sdk install \
https://download.swift.org/swift-6.0.2-release/static-sdk/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz \
--checksum aa5515476a403797223fc2aad4ca0c3bf83995d5427fb297cab1d93c68cee075

# Run the test script
- name: Test ELF detection
run: |
scripts/test-elf-detection.sh
228 changes: 228 additions & 0 deletions Sources/containertool/ELFDetect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftContainerPlugin open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

struct ArrayField<T: Collection> where T.Element == UInt8 {
var start: Int
var count: Int
}

struct IntField<T: BinaryInteger> {
var start: Int
}

extension Array where Element == UInt8 {
subscript(idx: ArrayField<[UInt8]>) -> [UInt8] {
[UInt8](self[idx.start..<idx.start + idx.count])
}

subscript(idx: IntField<UInt8>) -> UInt8 {
self[idx.start]
}

subscript(idx: IntField<UInt16>, endianness endianness: ELF.Endianness) -> UInt16 {
let (a, b) = (UInt16(self[idx.start]), UInt16(self[idx.start + 1]))

switch endianness {
case .littleEndian:
return a &<< 0 &+ b &<< 8
case .bigEndian:
return a &<< 8 &+ b &<< 0
}
}
}

/// ELF header
///
/// - https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
/// - https://refspecs.linuxbase.org/elf/elf.pdf
///
/// This struct only defines enough fields to identify a valid ELF file
/// and extract the type of object it contains, and the processor
/// architecture and operating system ABI for which that object
/// was created.
struct ELF: Equatable {
/// Multibyte ELF fields are stored in the native endianness of the target system.
/// This field records the endianness of objects in the file.
enum Endianness: UInt8 {
case littleEndian = 0x01
case bigEndian = 0x02
}

/// Offsets (addresses) are stored as 32-bit or 64-bit integers.
/// This field records the offset size used in objects in the file.
/// Variable offset sizes mean that some fields are found at different
/// offsets in 32-bit and 64-bit ELF files.
enum Encoding: UInt8 {
case bits32 = 0x01
case bits64 = 0x02
}

/// ELF files can hold a variety of different object types.
/// This field records type of object in the file.
/// The standard defines a number of fixed types but also
/// reserves ranges of type numbers for to be used by
/// specific operating systems and processors.
enum Object: Equatable {
case none
case relocatable
case executable
case shared
case core
case reservedOS(UInt16)
case reservedCPU(UInt16)
case unknown(UInt16)

init?(rawValue: UInt16) {
switch rawValue {
case 0x0000: self = .none
case 0x0001: self = .relocatable
case 0x0002: self = .executable
case 0x0003: self = .shared
case 0x0004: self = .core

/// Reserved for OS-specific use
case 0xfe00...0xfeff: self = .reservedOS(rawValue)

/// Reserved for CPU-specific use
case 0xff00...0xffff: self = .reservedCPU(rawValue)

default: return nil
}
}
}

/// 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.
enum ABI: Equatable {
case SysV
case Linux
case unknown(UInt8)

init(rawValue: UInt8) {
switch rawValue {
case 0x00: self = .SysV
case 0x03: self = .Linux
default: self = .unknown(rawValue)
}
}
}

/// The processor architecture used by the object in this ELF file. Values are reserved for many ISAs;
/// this enum includes cases for the linux-* host types for which Swift can currently be built:
///
/// https://github.com/swiftlang/swift/blob/c6d1060778f35631000911372d7645dbd5cade0a/utils/build-script-impl#L458
enum ISA: Equatable {
case x86
case powerpc
case powerpc64
case s390 // incluing s390x
case arm // up to armv7
case x86_64
case aarch64 // armv8 onwards
case riscv
case unknown(UInt16)

init(rawValue: UInt16) {
switch rawValue {
case 0x0003: self = .x86
case 0x0014: self = .powerpc
case 0x0015: self = .powerpc64
case 0x0016: self = .s390
case 0x0028: self = .arm
case 0x003e: self = .x86_64
case 0x00b7: self = .aarch64
case 0x00f3: self = .riscv
default: self = .unknown(rawValue)
}
}
}

var encoding: Encoding
var endianness: Endianness
var ABI: ABI
var object: Object
var ISA: ISA
}

extension ELF {
/// ELF header field addresses
///
/// The ELF format can store binaries for 32-bit and 64-bit systems,
/// using little-endian and big-endian data encoding.
///
/// All multibyte fields are stored using the endianness of the target
/// system. Read the EI_DATA field to find the endianness of the file.
///
/// Some fields are different sizes in 32-bit and 64-bit ELF files, but
/// these occur after all the fields we need to read for basic file type
/// identification, so all our offsets are the same on 32-bit and 64-bit systems.
enum Field {
/// ELF magic number: a string of 4 bytes, not a UInt32; no endianness
static let EI_MAGIC = ArrayField<[UInt8]>(start: 0x0, count: 4)

/// ELF class (word size): 1 byte
static let EI_CLASS = IntField<UInt8>(start: 0x4)

/// Data encoding (endianness): 1 byte
static let EI_DATA = IntField<UInt8>(start: 0x5)

// ELF version: 1 byte
static let EI_VERSION = IntField<UInt8>(start: 0x6)

// Operating system/ABI identification: 1 byte
static let EI_OSABI = IntField<UInt8>(start: 0x7)

// The following fields are multibyte, so endianness must be considered,
// All the fields we need are the same length in 32-bit and 64-bit
// ELF files, so their offsets do not change.

/// Object type: 2 bytes
static let EI_TYPE = IntField<UInt16>(start: 0x10)

//l Machine ISA (processor architecture): 2 bytes
static let EI_MACHINE = IntField<UInt16>(start: 0x12)
}

/// The initial magic number (4 bytes) which identifies an ELF file.
///
/// The ELF magic number is *not* a multibyte integer. It is defined as a
/// string of 4 individual bytes and is the same for little-endian and
/// big-endian ELF files.
static let ELFMagic = Array("\u{7f}ELF".utf8)

/// Read enough of an ELF header from bytes to discover the object type,
/// processor architecture and operating system ABI.
static func read(_ bytes: [UInt8]) -> ELF? {
// An ELF file starts with a magic number which is the same in either endianness.
// The only defined ELF header version is 1.
guard bytes.count > 0x13, bytes[Field.EI_MAGIC] == ELFMagic, bytes[Field.EI_VERSION] == 1 else {
return nil
}

guard
let encoding = Encoding(rawValue: bytes[Field.EI_CLASS]),
let endianness = Endianness(rawValue: bytes[Field.EI_DATA]),
let object = Object(rawValue: bytes[Field.EI_TYPE, endianness: endianness])
else {
return nil
}

return ELF(
encoding: encoding,
endianness: endianness,
ABI: .init(rawValue: bytes[Field.EI_OSABI]),
object: object,
ISA: .init(rawValue: bytes[Field.EI_MACHINE, endianness: endianness])
)
}
}
25 changes: 22 additions & 3 deletions Sources/containertool/containertool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
var allowInsecureHttp: AllowHTTP?

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

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

let executableURL = URL(fileURLWithPath: executable)
let payload = try Data(contentsOf: executableURL)

let authProvider: AuthorizationProvider?
if !netrc {
authProvider = nil
Expand Down Expand Up @@ -110,6 +113,14 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti

// MARK: Find the base image

let elfheader = ELF.read([UInt8](payload))
let architecture =
architecture
?? ProcessInfo.processInfo.environment["CONTAINERTOOL_ARCHITECTURE"]
?? elfheader?.ISA.containerArchitecture
?? "amd64"
if verbose { log("Base image architecture: \(architecture)") }

let baseimage_manifest: ImageManifest
let baseimage_config: ImageConfiguration
if let source {
Expand Down Expand Up @@ -137,8 +148,6 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti

// MARK: Build the application layer

let executableURL = URL(fileURLWithPath: executable)
let payload = try Data(contentsOf: executableURL)
let payload_name = executableURL.lastPathComponent
let tardiff = tar(payload, filename: payload_name)
log("Built application layer")
Expand Down Expand Up @@ -228,3 +237,13 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
print(destination_image)
}
}

extension ELF.ISA {
var containerArchitecture: String? {
switch self {
case .x86_64: "amd64"
case .aarch64: "arm64"
default: nil
}
}
}
Loading