From 2be5eedeb67359cd26e2974c1d10651fa2e21f92 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Fri, 17 Feb 2023 08:50:57 +0400 Subject: [PATCH] Try to set a SUID bit on Softnet using Sudo before failing (#421) * Try to set a SUID bit on Softnet using Sudo before failing * .cirrus.yml: switch to the new Mac machine --- .cirrus.yml | 2 +- Sources/tart/Commands/Run.swift | 8 +++ Sources/tart/Network/Softnet.swift | 78 +++++++++++++++++++++++++++--- Sources/tart/VMStorageHelper.swift | 3 ++ 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index dff0023..62b1dfa 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -5,7 +5,7 @@ task: alias: test persistent_worker: labels: - name: scaleway-m1 + name: dev-mini test_script: - swift test integration_test_script: diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index 2ab472c..c0107fd 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -98,6 +98,10 @@ struct Run: AsyncParsableCommand { func run() async throws { let vmDir = try VMStorageLocal().open(name) + if netSoftnet && isInteractiveSession() { + try Softnet.configureSUIDBitIfNeeded() + } + let additionalDiskAttachments = try additionalDiskAttachments() // Error out if the disk is locked by the host (e.g. it was mounted in Finder), @@ -194,6 +198,10 @@ struct Run: AsyncParsableCommand { } } + func isInteractiveSession() -> Bool { + isatty(STDOUT_FILENO) == 1 + } + func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? { if netSoftnet { let config = try VMConfig.init(fromURL: vmDir.configURL) diff --git a/Sources/tart/Network/Softnet.swift b/Sources/tart/Network/Softnet.swift index 50bb308..770b975 100644 --- a/Sources/tart/Network/Softnet.swift +++ b/Sources/tart/Network/Softnet.swift @@ -1,6 +1,7 @@ import Foundation import Virtualization import Atomics +import System enum SoftnetError: Error { case InitializationFailed(why: String) @@ -15,12 +16,6 @@ class Softnet: Network { let vmFD: Int32 init(vmMACAddress: String) throws { - let binaryName = "softnet" - - guard let executableURL = resolveBinaryPath(binaryName) else { - throw SoftnetError.InitializationFailed(why: "\(binaryName) not found in PATH") - } - let fds = UnsafeMutablePointer.allocate(capacity: MemoryLayout.stride * 2) let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds) @@ -34,11 +29,21 @@ class Softnet: Network { try setSocketBuffers(vmFD, 1 * 1024 * 1024); try setSocketBuffers(softnetFD, 1 * 1024 * 1024); - process.executableURL = executableURL + process.executableURL = try Self.softnetExecutableURL() process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress] process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false) } + static func softnetExecutableURL() throws -> URL { + let binaryName = "softnet" + + guard let executableURL = resolveBinaryPath(binaryName) else { + throw SoftnetError.InitializationFailed(why: "\(binaryName) not found in PATH") + } + + return executableURL + } + func run(_ sema: DispatchSemaphore) throws { try process.run() @@ -91,4 +96,63 @@ class Softnet: Network { let fh = FileHandle.init(fileDescriptor: vmFD) return VZFileHandleNetworkDeviceAttachment(fileHandle: fh) } + + static func configureSUIDBitIfNeeded() throws { + // Obtain the Softnet executable path + // + // It's important to use resolvingSymlinksInPath() here, because otherwise + // we will get something like "/opt/homebrew/bin/softnet" instead of + // "/opt/homebrew/Cellar/softnet/0.6.2/bin/softnet" + let softnetExecutablePath = try Softnet.softnetExecutableURL().resolvingSymlinksInPath().path + + // Check if the SUID bit is already configured + let info = try FileManager.default.attributesOfItem(atPath: softnetExecutablePath) as NSDictionary + if info.fileOwnerAccountID() == 0 && (info.filePosixPermissions() & Int(S_ISUID)) != 0 { + return + } + + // Check if the passwordless Sudo is already configured for Softnet + let sudoBinaryName = "sudo" + + guard let sudoExecutableURL = resolveBinaryPath(sudoBinaryName) else { + throw SoftnetError.InitializationFailed(why: "\(sudoBinaryName) not found in PATH") + } + + var process = Process() + process.executableURL = sudoExecutableURL + process.arguments = ["--non-interactive", "softnet", "--help"] + process.standardInput = nil + process.standardOutput = nil + process.standardError = nil + try process.run() + process.waitUntilExit() + if process.terminationStatus == 0 { + return + } + + // Configure the SUID bit by spawning the Sudo process in interactive mode + // and asking the user for password required to run chown & chmod + fputs("Softnet requires a Sudo password to set the SUID bit on the Softnet executable, please enter it below.\n", + stderr) + + process = try Process.run(sudoExecutableURL, arguments: [ + "sh", + "-c", + "chown root \(softnetExecutablePath) && chmod u+s \(softnetExecutablePath)", + ]) + + // Set TTY's foreground process group to that of the Sudo process, + // otherwise it will get stopped by a SIGTTIN once user input arrives + if tcsetpgrp(STDIN_FILENO, process.processIdentifier) == -1 { + let details = Errno(rawValue: CInt(errno)) + + throw RuntimeError.SoftnetFailed("tcsetpgrp(2) failed: \(details)") + } + + process.waitUntilExit() + + if process.terminationStatus != 0 { + throw RuntimeError.SoftnetFailed("failed to configure SUID bit on Softnet executable with Sudo") + } + } } diff --git a/Sources/tart/VMStorageHelper.swift b/Sources/tart/VMStorageHelper.swift index 5afec1a..48c2398 100644 --- a/Sources/tart/VMStorageHelper.swift +++ b/Sources/tart/VMStorageHelper.swift @@ -55,6 +55,7 @@ enum RuntimeError : Error { case VMDirectoryAlreadyInitialized(_ message: String) case ExportFailed(_ message: String) case ImportFailed(_ message: String) + case SoftnetFailed(_ message: String) } protocol HasExitCode { @@ -92,6 +93,8 @@ extension RuntimeError : CustomStringConvertible { return "VM export failed: \(message)" case .ImportFailed(let message): return "VM import failed: \(message)" + case .SoftnetFailed(let message): + return "Softnet failed: \(message)" } } }