Skip to content
Open
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import PackageDescription
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
let builderShimVersion = "0.12.0"
let scVersion = "0.33.1"
let scVersion = "0.33.2"

let package = Package(
name: "container",
Expand Down
135 changes: 128 additions & 7 deletions Sources/ContainerResource/Container/PublishSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,148 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerizationError
import Foundation
import SystemPackage

/// Represents a socket that should be published from container to host.
///
/// - Deprecated: New for 1.0.0, path types changed from `URL` to `FilePath`.
/// - Note: Decoder handles `FilePath` and `URL` for persistent data compatibility;
/// this compatibility will be removed in a later release.
public struct PublishSocket: Sendable, Codable {
Comment thread
chrisgeo marked this conversation as resolved.
/// The path to the socket in the container.
public var containerPath: URL
/// Absolute path to the socket inside the container.
public var containerPath: FilePath

/// The path where the socket should appear on the host.
public var hostPath: URL
/// Absolute path where the socket appears on the host.
public var hostPath: FilePath

/// File permissions for the socket on the host.
public var permissions: FilePermissions?

/// Creates a `PublishSocket` with validated absolute paths.
///
/// - Parameters:
/// - containerPath: Absolute path to the socket inside the container.
/// Must begin with `/`.
/// - hostPath: Absolute path where the socket appears on the host.
/// Must begin with `/`.
/// - permissions: File permissions applied to the socket on the host.
/// - Throws: `ContainerizationError` with code `.invalidArgument` if
/// either path is not absolute.
public init(
containerPath: URL,
hostPath: URL,
containerPath: FilePath,
hostPath: FilePath,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What constraints/invariants exist on these paths today? If any do exist, should we enforce them in init() so that PublishSocket objects are correct by construction?

We should docc the parameters here and if there are constraints, describe them as well.

permissions: FilePermissions? = nil
) {
) throws {
guard containerPath.isAbsolute else {
throw ContainerizationError(
.invalidArgument,
message: "containerPath must be absolute: \(containerPath)"
)
}
guard hostPath.isAbsolute else {
throw ContainerizationError(
.invalidArgument,
message: "hostPath must be absolute: \(hostPath)"
)
}
self.containerPath = containerPath
self.hostPath = hostPath
self.permissions = permissions
}

private enum CodingKeys: String, CodingKey {
case containerPath
case hostPath
case permissions
}

/// Encodes each path as its plain absolute string (e.g. `"/var/run/docker.sock"`).
///
/// Pre-1.0 wire-format change from the prior `URL`-typed encoding which
/// emitted `URL.absoluteString` (`"file:///var/run/docker.sock"`). The
/// decoder accepts both forms for compatibility with persisted bundles
/// from earlier releases; that compatibility will be removed in a later
/// release.
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(containerPath.string, forKey: .containerPath)
try container.encode(hostPath.string, forKey: .hostPath)
try container.encodeIfPresent(permissions, forKey: .permissions)
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let containerPath = try Self.decodePath(from: container, forKey: .containerPath)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about enforcing absolute path here (or in decodePath, since it's a private func dedicated to just these paths) as well? Legacy should always be absolute, but we should protect against bad inputs should someone muck about with a config manually.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a valid path to cover until it's phased out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 01ec891. decodePath now validates absoluteness directly and throws DecodingError.dataCorrupted ("decoded socket path must be absolute") for non-absolute decoded paths, covering both the plain-path and legacy file: branches. The by-construction check in init() stays as defense in depth, but the private helper is now self-validating so a manually edited/corrupt config is rejected at the decode layer.

let hostPath = try Self.decodePath(from: container, forKey: .hostPath)
let permissions = try container.decodeIfPresent(FilePermissions.self, forKey: .permissions)
do {
try self.init(
containerPath: containerPath,
hostPath: hostPath,
permissions: permissions
)
} catch let error as ContainerizationError {
throw DecodingError.dataCorruptedError(
forKey: .containerPath,
in: container,
debugDescription: String(describing: error)
)
}
}

/// Decodes a `FilePath` accepting either the new plain-path form
/// (`"/var/run/docker.sock"`) or the legacy file-URL form emitted by
/// older releases (`"file:///var/run/docker.sock"`). Throws
/// `DecodingError.dataCorrupted` on a malformed file URL, empty input, or a
/// non-absolute path — validating decoded paths here guards against
/// manually edited or corrupt persisted configs, complementing the
/// by-construction check in `init(containerPath:hostPath:permissions:)`.
private static func decodePath(
from container: KeyedDecodingContainer<CodingKeys>,
forKey key: CodingKeys
) throws -> FilePath {
let raw = try container.decode(String.self, forKey: key)

let path: String
if raw.hasPrefix("file:") {
guard let url = URL(string: raw), url.isFileURL else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "malformed file URL: \(raw)"
)
}
if let host = url.host(), !host.isEmpty, host != "localhost" {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "file URL host must be empty or 'localhost': \(raw)"
)
}
path = url.path(percentEncoded: false)
} else {
path = raw
}

guard !path.isEmpty else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "decoded socket path is empty: \(raw)"
)
}

let filePath = FilePath(path)
guard filePath.isAbsolute else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "decoded socket path must be absolute: \(raw)"
)
}

return filePath
}
}
34 changes: 11 additions & 23 deletions Sources/Services/ContainerAPIService/Client/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ContainerizationExtras
import ContainerizationOCI
import ContainerizationOS
import Foundation
import SystemPackage

/// A parsed volume specification from user input
public struct ParsedVolume {
Expand Down Expand Up @@ -739,7 +740,6 @@ public struct Parser {
let hostPath = String(parts[0])
let containerPath = String(parts[1])

// Validate paths are not empty
if hostPath.isEmpty {
throw ContainerizationError(
.invalidArgument, message: "host socket path cannot be empty")
Expand All @@ -749,46 +749,34 @@ public struct Parser {
.invalidArgument, message: "container socket path cannot be empty")
}

// Ensure container path must start with /
if !containerPath.hasPrefix("/") {
throw ContainerizationError(
.invalidArgument,
message: "container socket path must be absolute: \(containerPath)")
}

// Convert host path to absolute path for consistency
let hostURL = URL(fileURLWithPath: hostPath)
let absoluteHostPath = hostURL.absoluteURL.path
let absoluteHostPath = FilePathOps.absolutePath(FilePath(hostPath))

// Check if host socket already exists and might be in use
if FileManager.default.fileExists(atPath: absoluteHostPath) {
if FileManager.default.fileExists(atPath: absoluteHostPath.string) {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath)
let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath.string)
if let fileType = attrs[.type] as? FileAttributeType, fileType == .typeSocket {
throw ContainerizationError(
.invalidArgument,
message: "host socket \(absoluteHostPath) already exists and may be in use")
}
// If it exists but is not a socket, we can remove it and create socket
try FileManager.default.removeItem(atPath: absoluteHostPath)
try FileManager.default.removeItem(atPath: absoluteHostPath.string)
} catch let error as ContainerizationError {
throw error
} catch {
// For other file system errors, continue with creation
}
}

// Create host directory if it doesn't exist
let hostDir = hostURL.deletingLastPathComponent()
if !FileManager.default.fileExists(atPath: hostDir.path) {
let hostDir = absoluteHostPath.removingLastComponent()
if !FileManager.default.fileExists(atPath: hostDir.string) {
try FileManager.default.createDirectory(
at: hostDir, withIntermediateDirectories: true)
atPath: hostDir.string, withIntermediateDirectories: true)
}

// Create and return PublishSocket object with validated paths
return PublishSocket(
containerPath: URL(fileURLWithPath: containerPath),
hostPath: URL(fileURLWithPath: absoluteHostPath),
return try PublishSocket(
containerPath: FilePath(containerPath),
hostPath: absoluteHostPath,
permissions: nil
)

Expand Down
5 changes: 3 additions & 2 deletions Sources/Services/RuntimeLinux/Server/RuntimeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -999,9 +999,10 @@ public actor RuntimeService {
}

for publishedSocket in config.publishedSockets {
// UnixSocketConfiguration (Containerization) takes URL; convert from FilePath at the boundary.
let socketConfig = UnixSocketConfiguration(
source: publishedSocket.containerPath,
destination: publishedSocket.hostPath,
source: URL(filePath: publishedSocket.containerPath.string),
destination: URL(filePath: publishedSocket.hostPath.string),
permissions: publishedSocket.permissions,
direction: .outOf
)
Expand Down
Loading