Mining/pkg/mining/ttminer_start.go
snider 4072bdaf0d fix: Address 16 security findings from parallel code review
Critical fixes (6):
- CRIT-001/002: Add safeKeyPrefix() to prevent panic on short public keys
- CRIT-003/004: Add sync.Once pattern for thread-safe singleton initialization
- CRIT-005: Harden console ANSI parser with length limits and stricter validation
- CRIT-006: Add client-side input validation for profile creation

High priority fixes (10):
- HIGH-001: Add secondary timeout in TTMiner to prevent goroutine leak
- HIGH-002: Verify atomic flag prevents timeout middleware race
- HIGH-004: Add LimitReader (100MB) to prevent decompression bombs
- HIGH-005: Add Lines parameter validation (max 10000) in worker
- HIGH-006: Add TLS 1.2+ config with secure cipher suites
- HIGH-007: Add pool URL format and wallet length validation
- HIGH-008: Add SIGHUP handling and force cleanup on Stop() failure
- HIGH-009: Add WebSocket message size limit and event type validation
- HIGH-010: Refactor to use takeUntil(destroy$) for observable cleanup
- HIGH-011: Add sanitizeErrorDetails() with debug mode control

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 17:44:49 +00:00

235 lines
5.9 KiB
Go

package mining
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/Snider/Mining/pkg/logging"
)
// Start launches the TT-Miner with the given configuration.
func (m *TTMiner) Start(config *Config) error {
// Check installation BEFORE acquiring lock (CheckInstallation takes its own locks)
m.mu.RLock()
needsInstallCheck := m.MinerBinary == ""
m.mu.RUnlock()
if needsInstallCheck {
if _, err := m.CheckInstallation(); err != nil {
return err // Propagate the detailed error from CheckInstallation
}
}
m.mu.Lock()
defer m.mu.Unlock()
if m.Running {
return errors.New("miner is already running")
}
if m.API != nil && config.HTTPPort != 0 {
m.API.ListenPort = config.HTTPPort
} else if m.API != nil && m.API.ListenPort == 0 {
return errors.New("miner API port not assigned")
}
// Build command line arguments for TT-Miner
args := m.buildArgs(config)
logging.Info("executing TT-Miner command", logging.Fields{"binary": m.MinerBinary, "args": strings.Join(args, " ")})
m.cmd = exec.Command(m.MinerBinary, args...)
// Create stdin pipe for console commands
stdinPipe, err := m.cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to create stdin pipe: %w", err)
}
m.stdinPipe = stdinPipe
// Always capture output to LogBuffer
if m.LogBuffer != nil {
m.cmd.Stdout = m.LogBuffer
m.cmd.Stderr = m.LogBuffer
}
// Also output to console if requested
if config.LogOutput {
m.cmd.Stdout = io.MultiWriter(m.LogBuffer, os.Stdout)
m.cmd.Stderr = io.MultiWriter(m.LogBuffer, os.Stderr)
}
if err := m.cmd.Start(); err != nil {
stdinPipe.Close()
return fmt.Errorf("failed to start TT-Miner: %w", err)
}
m.Running = true
// Capture cmd locally to avoid race with Stop()
cmd := m.cmd
go func() {
// Use a channel to detect if Wait() completes
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
// Wait with timeout to prevent goroutine leak on zombie processes
var err error
select {
case err = <-done:
// Normal exit
case <-time.After(5 * time.Minute):
// Process didn't exit after 5 minutes - force cleanup
logging.Warn("TT-Miner process wait timeout, forcing cleanup")
if cmd.Process != nil {
cmd.Process.Kill()
}
// Wait for inner goroutine with secondary timeout to prevent leak
select {
case err = <-done:
// Inner goroutine completed
case <-time.After(10 * time.Second):
logging.Error("TT-Miner process cleanup timed out after kill", logging.Fields{"miner": m.Name})
err = nil
}
}
m.mu.Lock()
// Only clear if this is still the same command (not restarted)
if m.cmd == cmd {
m.Running = false
m.cmd = nil
}
m.mu.Unlock()
if err != nil {
logging.Debug("TT-Miner exited with error", logging.Fields{"error": err})
} else {
logging.Debug("TT-Miner exited normally")
}
}()
return nil
}
// buildArgs constructs the command line arguments for TT-Miner
func (m *TTMiner) buildArgs(config *Config) []string {
var args []string
// Pool configuration
if config.Pool != "" {
args = append(args, "-P", config.Pool)
}
// Wallet/user configuration
if config.Wallet != "" {
args = append(args, "-u", config.Wallet)
}
// Password
if config.Password != "" {
args = append(args, "-p", config.Password)
} else {
args = append(args, "-p", "x")
}
// Algorithm selection
if config.Algo != "" {
args = append(args, "-a", config.Algo)
}
// API binding for stats collection
if m.API != nil && m.API.Enabled {
args = append(args, "-b", fmt.Sprintf("%s:%d", m.API.ListenHost, m.API.ListenPort))
}
// GPU device selection (if specified)
if config.Devices != "" {
args = append(args, "-d", config.Devices)
}
// Intensity (if specified)
if config.Intensity > 0 {
args = append(args, "-i", fmt.Sprintf("%d", config.Intensity))
}
// Additional CLI arguments
addTTMinerCliArgs(config, &args)
return args
}
// addTTMinerCliArgs adds any additional CLI arguments from config
func addTTMinerCliArgs(config *Config, args *[]string) {
// Add any extra arguments passed via CLIArgs
if config.CLIArgs != "" {
extraArgs := strings.Fields(config.CLIArgs)
for _, arg := range extraArgs {
// Skip potentially dangerous arguments
if isValidCLIArg(arg) {
*args = append(*args, arg)
} else {
logging.Warn("skipping invalid CLI argument", logging.Fields{"arg": arg})
}
}
}
}
// isValidCLIArg validates CLI arguments to prevent injection or dangerous patterns.
// Uses a combination of allowlist patterns and blocklist for security.
func isValidCLIArg(arg string) bool {
// Empty or whitespace-only args are invalid
if strings.TrimSpace(arg) == "" {
return false
}
// Must start with dash (standard CLI argument format)
// This is an allowlist approach - only accept valid argument patterns
if !strings.HasPrefix(arg, "-") {
// Allow values for flags (e.g., the "3" in "-i 3")
// Values must not contain shell metacharacters
return isValidArgValue(arg)
}
// Block shell metacharacters and dangerous patterns
if !isValidArgValue(arg) {
return false
}
// Block arguments that could override security-related settings
blockedPrefixes := []string{
"--api-access-token", "--api-worker-id", // TT-Miner API settings
"--config", // Could load arbitrary config
"--log-file", // Could write to arbitrary locations
"--coin-file", // Could load arbitrary coin configs
"-o", "--out", // Output redirection
}
lowerArg := strings.ToLower(arg)
for _, blocked := range blockedPrefixes {
if lowerArg == blocked || strings.HasPrefix(lowerArg, blocked+"=") {
return false
}
}
return true
}
// isValidArgValue checks if a value contains dangerous patterns
func isValidArgValue(arg string) bool {
// Block shell metacharacters and command injection patterns
dangerousPatterns := []string{
";", "|", "&", "`", "$", "(", ")", "{", "}",
"<", ">", "\n", "\r", "\\", "'", "\"", "!",
}
for _, p := range dangerousPatterns {
if strings.Contains(arg, p) {
return false
}
}
return true
}