diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml new file mode 100644 index 00000000..e7c7318c --- /dev/null +++ b/.github/workflows/linux-build.yml @@ -0,0 +1,40 @@ +name: Linux build + +permissions: + contents: read + +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: + - main + - release/* + +jobs: + build: + name: Linux compile check + timeout-minutes: 30 + runs-on: ubuntu-24.04 + container: swift:6.3-noble + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + fetch-depth: 0 + + - name: Install system dependencies + run: apt-get update && apt-get install -y curl make libarchive-dev libbz2-dev liblzma-dev libssl-dev + + - name: Build containerization + run: make containerization + + - name: Build vminitd (glibc) + run: make -C vminitd SWIFT_CONFIGURATION="--disable-automatic-resolution -Xswiftc -warnings-as-errors" + + - name: Install Static Linux SDK + run: make -C vminitd linux-sdk + + - name: Build vminitd (musl) + run: make -C vminitd diff --git a/Sources/CShim/include/linux_shim.h b/Sources/CShim/include/linux_shim.h new file mode 100644 index 00000000..4e730037 --- /dev/null +++ b/Sources/CShim/include/linux_shim.h @@ -0,0 +1,31 @@ +/* + * Copyright © 2026 Apple Inc. and the Containerization project authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// The below fall into two main categories: +// 1. Aren't exposed by Swifts glibc modulemap. +// 2. Don't have syscall wrappers/definitions in glibc/musl. + +#ifndef __LINUX_SHIM_H +#define __LINUX_SHIM_H + +#if defined(__linux__) + +#include +#include + +#endif /* __linux__ */ + +#endif /* __LINUX_SHIM_H */ diff --git a/Sources/ContainerizationOS/Linux/Epoll.swift b/Sources/ContainerizationOS/Linux/Epoll.swift index e3249e38..f239be1f 100644 --- a/Sources/ContainerizationOS/Linux/Epoll.swift +++ b/Sources/ContainerizationOS/Linux/Epoll.swift @@ -14,24 +14,43 @@ // limitations under the License. //===----------------------------------------------------------------------===// +#if os(Linux) + #if canImport(Musl) import Musl +#elseif canImport(Glibc) +import Glibc +#endif +import CShim import Foundation import Synchronization +// On glibc, epoll constants are EPOLL_EVENTS enum values. On musl they're +// plain UInt32. These helpers normalize them to UInt32/Int32. +private func epollMask(_ value: UInt32) -> UInt32 { value } +private func epollMask(_ value: Int32) -> UInt32 { UInt32(bitPattern: value) } +#if canImport(Glibc) +private func epollMask(_ value: EPOLL_EVENTS) -> UInt32 { value.rawValue } +private func epollFlag(_ value: EPOLL_EVENTS) -> Int32 { Int32(bitPattern: value.rawValue) } +#endif + /// Register file descriptors to receive events via Linux's /// epoll syscall surface. public final class Epoll: Sendable { public typealias Mask = Int32 public typealias Handler = (@Sendable (Mask) -> Void) + public static let maskIn: Int32 = Int32(bitPattern: epollMask(EPOLLIN)) + public static let maskOut: Int32 = Int32(bitPattern: epollMask(EPOLLOUT)) + public static let defaultMask: Int32 = maskIn | maskOut + private let epollFD: Int32 private let handlers = SafeMap() private let pipe = Pipe() // to wake up a waiting epoll_wait public init() throws { - let efd = epoll_create1(EPOLL_CLOEXEC) + let efd = epoll_create1(Int32(EPOLL_CLOEXEC)) guard efd > 0 else { throw POSIXError.fromErrno() } @@ -41,14 +60,14 @@ public final class Epoll: Sendable { public func add( _ fd: Int32, - mask: Int32 = EPOLLIN | EPOLLOUT, // HUP is always added + mask: Int32 = Epoll.defaultMask, handler: @escaping Handler ) throws { guard fcntl(fd, F_SETFL, O_NONBLOCK) == 0 else { throw POSIXError.fromErrno() } - let events = EPOLLET | UInt32(bitPattern: mask) + let events = epollMask(EPOLLET) | UInt32(bitPattern: mask) var event = epoll_event() event.events = events @@ -115,7 +134,7 @@ public final class Epoll: Sendable { public func delete(_ fd: Int32) throws { var event = epoll_event() let result = withUnsafeMutablePointer(to: &event) { ptr in - epoll_ctl(self.epollFD, EPOLL_CTL_DEL, fd, ptr) + epoll_ctl(self.epollFD, EPOLL_CTL_DEL, fd, ptr) as Int32 } if result != 0 { if !acceptableDeletionErrno() { @@ -162,20 +181,20 @@ public final class Epoll: Sendable { extension Epoll.Mask { public var isHangup: Bool { - (self & (EPOLLHUP | EPOLLERR)) != 0 + (self & Int32(bitPattern: epollMask(EPOLLHUP) | epollMask(EPOLLERR))) != 0 } public var isRhangup: Bool { - (self & EPOLLRDHUP) != 0 + (self & Int32(bitPattern: epollMask(EPOLLRDHUP))) != 0 } public var readyToRead: Bool { - (self & EPOLLIN) != 0 + (self & Int32(bitPattern: epollMask(EPOLLIN))) != 0 } public var readyToWrite: Bool { - (self & EPOLLOUT) != 0 + (self & Int32(bitPattern: epollMask(EPOLLOUT))) != 0 } } -#endif // canImport(Musl) +#endif // os(Linux) diff --git a/vminitd/Makefile b/vminitd/Makefile index c9401cc2..505bff15 100644 --- a/vminitd/Makefile +++ b/vminitd/Makefile @@ -31,6 +31,7 @@ SWIFT_CONFIGURATION := --swift-sdk $(MUSL_ARCH)-swift-linux-musl $(SWIFT_WARNING SWIFT_VERSION := 6.3 SWIFT_SDK_URL := https://download.swift.org/swift-6.3-release/static-sdk/swift-6.3-RELEASE/swift-6.3-RELEASE_static-linux-0.1.0.artifactbundle.tar.gz +SWIFT_SDK_CHECKSUM := d2078b69bdeb5c31202c10e9d8a11d6f66f82938b51a4b75f032ccb35c4c286c SWIFT_SDK_PATH := /tmp/$(notdir $(SWIFT_SDK_URL)) SYSTEM_TYPE := $(shell uname -s) @@ -64,7 +65,7 @@ all: @install "$(BUILD_BIN_DIR)/vmexec" ./bin/ .PHONY: cross-prep -cross-prep: linux-sdk +cross-prep: swift linux-sdk .PHONY: swiftly swiftly: @@ -84,10 +85,10 @@ swift: swiftly @${SWIFTLY_BIN_DIR}/swiftly install $(SWIFT_VERSION) .PHONY: linux-sdk -linux-sdk: swift +linux-sdk: @echo Installing Static Linux SDK... @curl -L -o $(SWIFT_SDK_PATH) $(SWIFT_SDK_URL) - -@$(SWIFT) sdk install $(SWIFT_SDK_PATH) + -@$(SWIFT) sdk install $(SWIFT_SDK_PATH) --checksum $(SWIFT_SDK_CHECKSUM) @rm $(SWIFT_SDK_PATH) .PHONY: clean diff --git a/vminitd/Package.swift b/vminitd/Package.swift index e2465a66..110414b1 100644 --- a/vminitd/Package.swift +++ b/vminitd/Package.swift @@ -56,6 +56,7 @@ let package = Package( .product(name: "ContainerizationOCI", package: "containerization"), .product(name: "ContainerizationOS", package: "containerization"), .product(name: "SystemPackage", package: "swift-system"), + "LCShim", ] ), .executableTarget( diff --git a/vminitd/Sources/Cgroup/Cgroup2Manager.swift b/vminitd/Sources/Cgroup/Cgroup2Manager.swift index 26c1b254..65ba6869 100644 --- a/vminitd/Sources/Cgroup/Cgroup2Manager.swift +++ b/vminitd/Sources/Cgroup/Cgroup2Manager.swift @@ -26,6 +26,7 @@ import Musl import Glibc #endif +import LCShim import ContainerizationOCI import ContainerizationOS import Foundation diff --git a/vminitd/Sources/LCShim/include/syscall.h b/vminitd/Sources/LCShim/include/syscall.h index f30e3ab2..dbbc8813 100644 --- a/vminitd/Sources/LCShim/include/syscall.h +++ b/vminitd/Sources/LCShim/include/syscall.h @@ -14,25 +14,87 @@ * limitations under the License. */ +// The below fall into two main categories: +// 1. Aren't exposed by Swifts glibc modulemap. +// 2. Don't have syscall wrappers/definitions in glibc/musl. + #ifndef __SYSCALL_H #define __SYSCALL_H #include +#include -int CZ_pivot_root(const char *new_root, const char *put_old); +// CLONE_* flags +#ifndef CLONE_NEWNS +#define CLONE_NEWNS 0x00020000 +#endif +#ifndef CLONE_NEWCGROUP +#define CLONE_NEWCGROUP 0x02000000 +#endif +#ifndef CLONE_NEWUTS +#define CLONE_NEWUTS 0x04000000 +#endif +#ifndef CLONE_NEWIPC +#define CLONE_NEWIPC 0x08000000 +#endif +#ifndef CLONE_NEWUSER +#define CLONE_NEWUSER 0x10000000 +#endif +#ifndef CLONE_NEWPID +#define CLONE_NEWPID 0x20000000 +#endif + +extern int setns(int fd, int nstype); +extern int unshare(int flags); +extern int dup3(int oldfd, int newfd, int flags); +extern int execvpe(const char *file, char *const argv[], char *const envp[]); +extern int unlockpt(int fd); +extern char *ptsname(int fd); +// splice(2) and flags. +extern ssize_t splice(int fd_in, off_t *off_in, int fd_out, off_t *off_out, + size_t len, unsigned int flags); +#ifndef SPLICE_F_MOVE +#define SPLICE_F_MOVE 1 +#endif +#ifndef SPLICE_F_NONBLOCK +#define SPLICE_F_NONBLOCK 2 +#endif + +// RLIMIT constants as plain integers. On glibc these are __rlimit_resource +// enum values which can't be used as Int32 in Swift. +#define CZ_RLIMIT_CPU 0 +#define CZ_RLIMIT_FSIZE 1 +#define CZ_RLIMIT_DATA 2 +#define CZ_RLIMIT_STACK 3 +#define CZ_RLIMIT_CORE 4 +#define CZ_RLIMIT_RSS 5 +#define CZ_RLIMIT_NPROC 6 +#define CZ_RLIMIT_NOFILE 7 +#define CZ_RLIMIT_MEMLOCK 8 +#define CZ_RLIMIT_AS 9 +#define CZ_RLIMIT_LOCKS 10 +#define CZ_RLIMIT_SIGPENDING 11 +#define CZ_RLIMIT_MSGQUEUE 12 +#define CZ_RLIMIT_NICE 13 +#define CZ_RLIMIT_RTPRIO 14 +#define CZ_RLIMIT_RTTIME 15 + +// setrlimit wrapper that accepts plain int for the resource parameter, +// avoiding glibc's __rlimit_resource enum type mismatch in Swift. +int CZ_setrlimit(int resource, unsigned long long soft, unsigned long long hard); + +int CZ_pivot_root(const char *new_root, const char *put_old); int CZ_set_sub_reaper(); #ifndef SYS_pidfd_open #define SYS_pidfd_open 434 #endif - int CZ_pidfd_open(pid_t pid, unsigned int flags); #ifndef SYS_pidfd_getfd #define SYS_pidfd_getfd 438 #endif - int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags); int CZ_prctl_set_no_new_privs(); diff --git a/vminitd/Sources/LCShim/syscall.c b/vminitd/Sources/LCShim/syscall.c index 4070196c..f65c75aa 100644 --- a/vminitd/Sources/LCShim/syscall.c +++ b/vminitd/Sources/LCShim/syscall.c @@ -15,6 +15,7 @@ */ #include +#include #include #include @@ -39,3 +40,11 @@ int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags) { int CZ_prctl_set_no_new_privs() { return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); } + +int CZ_setrlimit(int resource, unsigned long long soft, + unsigned long long hard) { + struct rlimit limit; + limit.rlim_cur = (rlim_t)soft; + limit.rlim_max = (rlim_t)hard; + return setrlimit(resource, &limit); +} diff --git a/vminitd/Sources/vmexec/Console.swift b/vminitd/Sources/vmexec/Console.swift index 9fb25617..b868049f 100644 --- a/vminitd/Sources/vmexec/Console.swift +++ b/vminitd/Sources/vmexec/Console.swift @@ -15,6 +15,7 @@ //===----------------------------------------------------------------------===// import FoundationEssentials +import LCShim #if canImport(Musl) import Musl diff --git a/vminitd/Sources/vmexec/RunCommand.swift b/vminitd/Sources/vmexec/RunCommand.swift index cb79716a..4c9607fb 100644 --- a/vminitd/Sources/vmexec/RunCommand.swift +++ b/vminitd/Sources/vmexec/RunCommand.swift @@ -81,7 +81,7 @@ struct RunCommand: ParsableCommand { guard statfs("/", &s) == 0 else { throw App.Errno(stage: "statfs(/)") } - flags |= s.f_flags + flags |= UInt(s.f_flags) guard mount("", "/", "", flags, "") == 0 else { throw App.Errno(stage: "mount rootfs ro") diff --git a/vminitd/Sources/vmexec/vmexec.swift b/vminitd/Sources/vmexec/vmexec.swift index cf8ba4db..8141be9d 100644 --- a/vminitd/Sources/vmexec/vmexec.swift +++ b/vminitd/Sources/vmexec/vmexec.swift @@ -158,46 +158,45 @@ extension App { static func setRLimits(rlimits: [ContainerizationOCI.POSIXRlimit]) throws { for rl in rlimits { - var limit = rlimit(rlim_cur: rl.soft, rlim_max: rl.hard) let resource: Int32 switch rl.type { case "RLIMIT_AS": - resource = RLIMIT_AS + resource = CZ_RLIMIT_AS case "RLIMIT_CORE": - resource = RLIMIT_CORE + resource = CZ_RLIMIT_CORE case "RLIMIT_CPU": - resource = RLIMIT_CPU + resource = CZ_RLIMIT_CPU case "RLIMIT_DATA": - resource = RLIMIT_DATA + resource = CZ_RLIMIT_DATA case "RLIMIT_FSIZE": - resource = RLIMIT_FSIZE + resource = CZ_RLIMIT_FSIZE case "RLIMIT_LOCKS": - resource = RLIMIT_LOCKS + resource = CZ_RLIMIT_LOCKS case "RLIMIT_MEMLOCK": - resource = RLIMIT_MEMLOCK + resource = CZ_RLIMIT_MEMLOCK case "RLIMIT_MSGQUEUE": - resource = RLIMIT_MSGQUEUE + resource = CZ_RLIMIT_MSGQUEUE case "RLIMIT_NICE": - resource = RLIMIT_NICE + resource = CZ_RLIMIT_NICE case "RLIMIT_NOFILE": - resource = RLIMIT_NOFILE + resource = CZ_RLIMIT_NOFILE case "RLIMIT_NPROC": - resource = RLIMIT_NPROC + resource = CZ_RLIMIT_NPROC case "RLIMIT_RSS": - resource = RLIMIT_RSS + resource = CZ_RLIMIT_RSS case "RLIMIT_RTPRIO": - resource = RLIMIT_RTPRIO + resource = CZ_RLIMIT_RTPRIO case "RLIMIT_RTTIME": - resource = RLIMIT_RTTIME + resource = CZ_RLIMIT_RTTIME case "RLIMIT_SIGPENDING": - resource = RLIMIT_SIGPENDING + resource = CZ_RLIMIT_SIGPENDING case "RLIMIT_STACK": - resource = RLIMIT_STACK + resource = CZ_RLIMIT_STACK default: errno = EINVAL throw App.Errno(stage: "rlimit key unknown") } - guard setrlimit(resource, &limit) == 0 else { + guard CZ_setrlimit(resource, rl.soft, rl.hard) == 0 else { throw App.Errno(stage: "setrlimit()") } } diff --git a/vminitd/Sources/vminitd/AgentCommand.swift b/vminitd/Sources/vminitd/AgentCommand.swift index 0918ed42..76eb7f78 100644 --- a/vminitd/Sources/vminitd/AgentCommand.swift +++ b/vminitd/Sources/vminitd/AgentCommand.swift @@ -192,12 +192,11 @@ struct AgentCommand: AsyncParsableCommand { private static func adjustLimits(_ log: Logger) throws { let nrOpen = try String(contentsOfFile: "/proc/sys/fs/nr_open", encoding: .utf8) .trimmingCharacters(in: .whitespacesAndNewlines) - guard let max = rlim_t(nrOpen) else { + guard let max = UInt64(nrOpen) else { throw POSIXError(.EINVAL) } log.debug("setting RLIMIT_NOFILE to \(max)") - var limits = rlimit(rlim_cur: max, rlim_max: max) - guard setrlimit(RLIMIT_NOFILE, &limits) == 0 else { + guard CZ_setrlimit(CZ_RLIMIT_NOFILE, max, max) == 0 else { throw POSIXError(.init(rawValue: errno)!) } } diff --git a/vminitd/Sources/vminitd/IOPair.swift b/vminitd/Sources/vminitd/IOPair.swift index f8b06623..4b8a78aa 100644 --- a/vminitd/Sources/vminitd/IOPair.swift +++ b/vminitd/Sources/vminitd/IOPair.swift @@ -118,7 +118,7 @@ final class IOPair: Sendable { let readFrom = OSFile(fd: readFromFd) let writeTo = OSFile(fd: writeToFd) - try ProcessSupervisor.default.poller.add(readFromFd, mask: EPOLLIN) { mask in + try ProcessSupervisor.default.poller.add(readFromFd, mask: Epoll.maskIn) { mask in self.io.withLock { io in if io.closed { return diff --git a/vminitd/Sources/vminitd/OSFile+Splice.swift b/vminitd/Sources/vminitd/OSFile+Splice.swift index 1c330b6e..5ba00569 100644 --- a/vminitd/Sources/vminitd/OSFile+Splice.swift +++ b/vminitd/Sources/vminitd/OSFile+Splice.swift @@ -15,6 +15,7 @@ //===----------------------------------------------------------------------===// import Foundation +import LCShim extension OSFile { struct SpliceFile: Sendable { @@ -61,7 +62,7 @@ extension OSFile { while true { while (from.offset - to.offset) < count { let toRead = count - (from.offset - to.offset) - let bytesRead = Foundation.splice(from.fileDescriptor, nil, to.writer, nil, toRead, UInt32(bitPattern: SPLICE_F_MOVE | SPLICE_F_NONBLOCK)) + let bytesRead = LCShim.splice(from.fileDescriptor, nil, to.writer, nil, toRead, UInt32(bitPattern: LCShim.SPLICE_F_MOVE | LCShim.SPLICE_F_NONBLOCK)) if bytesRead == -1 { if errno != EAGAIN && errno != EIO { throw POSIXError(.init(rawValue: errno)!) @@ -81,7 +82,7 @@ extension OSFile { } while to.offset < from.offset { let toWrite = from.offset - to.offset - let bytesWrote = Foundation.splice(to.reader, nil, to.fileDescriptor, nil, toWrite, UInt32(bitPattern: SPLICE_F_MOVE | SPLICE_F_NONBLOCK)) + let bytesWrote = LCShim.splice(to.reader, nil, to.fileDescriptor, nil, toWrite, UInt32(bitPattern: LCShim.SPLICE_F_MOVE | LCShim.SPLICE_F_NONBLOCK)) if bytesWrote == -1 { if errno != EAGAIN && errno != EIO { throw POSIXError(.init(rawValue: errno)!) diff --git a/vminitd/Sources/vminitd/VsockProxy.swift b/vminitd/Sources/vminitd/VsockProxy.swift index 9e92d9fa..fabdc353 100644 --- a/vminitd/Sources/vminitd/VsockProxy.swift +++ b/vminitd/Sources/vminitd/VsockProxy.swift @@ -17,6 +17,7 @@ import ContainerizationIO import ContainerizationOS import Foundation +import LCShim import Logging actor VsockProxy { @@ -243,7 +244,7 @@ extension VsockProxy { c.resume() } - try! ProcessSupervisor.default.poller.add(clientFile.fileDescriptor, mask: EPOLLIN | EPOLLOUT) { mask in + try! ProcessSupervisor.default.poller.add(clientFile.fileDescriptor) { mask in if mask.readyToRead && !eofFromClient { let (fromEof, toEof) = Self.transferData( fromFile: &clientFile, @@ -274,7 +275,7 @@ extension VsockProxy { // we should see no more EPOLLIN events on the client fd // and no more EPOLLOUT events on the server fd eofFromClient = true - if shutdown(serverFile.fileDescriptor, SHUT_WR) != 0 { + if shutdown(serverFile.fileDescriptor, Int32(SHUT_WR)) != 0 { self.log?.warning( "failed to shut down client reads", metadata: [ @@ -295,7 +296,7 @@ extension VsockProxy { } } - try! ProcessSupervisor.default.poller.add(serverFile.fileDescriptor, mask: EPOLLIN | EPOLLOUT) { mask in + try! ProcessSupervisor.default.poller.add(serverFile.fileDescriptor) { mask in if mask.readyToRead && !eofFromServer { let (fromEof, toEof) = Self.transferData( fromFile: &serverFile, @@ -326,7 +327,7 @@ extension VsockProxy { // we should see no more EPOLLIN events on the server fd // and no more EPOLLOUT events on the client fd eofFromServer = true - if shutdown(clientFile.fileDescriptor, SHUT_WR) != 0 { + if shutdown(clientFile.fileDescriptor, Int32(SHUT_WR)) != 0 { self.log?.warning( "failed to shut down server reads", metadata: [ @@ -375,7 +376,7 @@ extension VsockProxy { // half close, shut down client to server transfer // we should see no more EPOLLIN events on the client fd // and no more EPOLLOUT events on the server fd - if shutdown(toFile.fileDescriptor, SHUT_WR) != 0 { + if shutdown(toFile.fileDescriptor, Int32(SHUT_WR)) != 0 { log?.warning( "failed to shut down reads", metadata: [