Skip to content

Commit

Permalink
Try to set a SUID bit on Softnet using Sudo before failing (#421)
Browse files Browse the repository at this point in the history
* Try to set a SUID bit on Softnet using Sudo before failing

* .cirrus.yml: switch to the new Mac machine
  • Loading branch information
edigaryev committed Feb 17, 2023
1 parent 9f1f3f1 commit 2be5eed
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ task:
alias: test
persistent_worker:
labels:
name: scaleway-m1
name: dev-mini
test_script:
- swift test
integration_test_script:
Expand Down
8 changes: 8 additions & 0 deletions Sources/tart/Commands/Run.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down
78 changes: 71 additions & 7 deletions Sources/tart/Network/Softnet.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import Virtualization
import Atomics
import System

enum SoftnetError: Error {
case InitializationFailed(why: String)
Expand All @@ -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<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2)

let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds)
Expand All @@ -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()

Expand Down Expand Up @@ -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")
}
}
}
3 changes: 3 additions & 0 deletions Sources/tart/VMStorageHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)"
}
}
}
Expand Down

0 comments on commit 2be5eed

Please sign in to comment.