package runtime import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/url" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" "browser.local/run/protocol" ) const ( maxDependencyDownloadBytes = int64(512 * 1024 * 1024) dependencyCommandTimeout = 10 * time.Minute ) var ( dependencyTokenPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:+@/-]{0,119}$`) dependencyVersionPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:+~-]{0,79}$`) ) type DependencyDownloader interface { Download(context.Context, string, string, int64) (int64, string, error) } type HTTPDependencyDownloader struct { Client *http.Client } func (downloader HTTPDependencyDownloader) Download(ctx context.Context, sourceURL, destination string, maxBytes int64) (int64, string, error) { parsed, err := validateDependencyDownloadURL(sourceURL) if err != nil { return 0, "", err } client := downloader.Client if client == nil { client = &http.Client{Timeout: 10 * time.Minute, CheckRedirect: func(request *http.Request, via []*http.Request) error { if len(via) >= 3 { return fmt.Errorf("dependency download redirect limit exceeded") } _, err := validateDependencyDownloadURL(request.URL.String()) return err }} } request, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil) if err != nil { return 0, "", err } response, err := client.Do(request) if err != nil { return 0, "", err } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode >= 300 { return 0, "", fmt.Errorf("dependency download returned status %d", response.StatusCode) } if response.ContentLength > maxBytes { return 0, "", fmt.Errorf("dependency download exceeds size limit") } if err := os.MkdirAll(filepath.Dir(destination), 0o700); err != nil { return 0, "", err } temporary := destination + ".partial" file, err := os.OpenFile(temporary, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) if err != nil { return 0, "", err } remove := true defer func() { _ = file.Close() if remove { _ = os.Remove(temporary) } }() hash := sha256.New() written, err := io.Copy(io.MultiWriter(file, hash), io.LimitReader(response.Body, maxBytes+1)) if err != nil { return 0, "", err } if written > maxBytes { return 0, "", fmt.Errorf("dependency download exceeds size limit") } if err := file.Sync(); err != nil { return 0, "", err } if err := file.Close(); err != nil { return 0, "", err } if err := os.Rename(temporary, destination); err != nil { return 0, "", err } remove = false return written, "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil } type dependencyJournal struct { Version int `json:"version"` JobID string `json:"jobId"` Attempt int `json:"attempt"` PlanDigest string `json:"planDigest"` CompletedSteps []int `json:"completedSteps,omitempty"` State string `json:"state"` UpdatedAt time.Time `json:"updatedAt"` } func (worker *Worker) executeDependencyJob(ctx context.Context, assignment protocol.RunJobAssignment) LifecycleExecutionResult { if err := protocol.ValidateRunJobAssignment(assignment); err != nil { return lifecycleFailure("unsafe_dependency_job", err.Error()) } runState, err := worker.registeredState() if err != nil { return lifecycleFailure("dependency_unregistered", "Run worker is not registered") } input, err := worker.client.GetDependencyExecutionInput(ctx, protocol.DependencyExecutionInputRequest{RunEndpointID: runState.RunEndpointID, SessionToken: runState.SessionToken, JobID: assignment.JobID, LeaseToken: assignment.LeaseToken, Attempt: assignment.Attempt}) if err != nil { return lifecycleFailure("dependency_input_failed", "could not load fenced dependency input") } if err := validateDependencyInput(assignment, input); err != nil { return lifecycleFailure("unsafe_dependency_input", err.Error()) } journalPath := filepath.Join(worker.cfg.WorkspaceRoot, "dependency-journals", safeWorkspaceName(assignment.JobID)+".json") journal, err := loadDependencyJournal(journalPath, assignment, input.PlanDigest) if err != nil { return lifecycleFailure("dependency_journal_failed", err.Error()) } if assignment.Capability == protocol.RunCapabilityDependenciesCheck { state, evidence, probeErr := worker.executor.runDependencyProbe(ctx, input.Probe, input.Bindings) if probeErr != nil { if errors.Is(probeErr, context.Canceled) || errors.Is(probeErr, context.DeadlineExceeded) { return LifecycleExecutionResult{State: lifecycleResultStateCancelled, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "dependency probe cancelled"}, Message: "dependency probe cancelled", ErrorCode: "dependency_probe_cancelled"} } return lifecycleFailure("dependency_probe_failed", probeErr.Error()) } journal.State = state journal.UpdatedAt = time.Now().UTC() if err := persistDependencyJournal(journalPath, journal); err != nil { return lifecycleFailure("dependency_journal_failed", err.Error()) } return dependencySuccess(assignment, input, state, evidence, 0) } completed := map[int]bool{} for _, index := range journal.CompletedSteps { completed[index] = true } for index, step := range input.Plan.Steps { if completed[index] { continue } if cancelled, ok := checkContextCancelled(ctx, "dependency installation cancelled", "dependency_install_cancelled"); ok { return cancelled } if err := worker.executor.runDependencyInstallStep(ctx, assignment, input, step, index); err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return LifecycleExecutionResult{State: lifecycleResultStateCancelled, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "dependency installation cancelled"}, Message: "dependency installation cancelled", ErrorCode: "dependency_install_cancelled"} } return lifecycleFailure("dependency_install_failed", err.Error()) } journal.CompletedSteps = append(journal.CompletedSteps, index) journal.State = "installing" journal.UpdatedAt = time.Now().UTC() if err := persistDependencyJournal(journalPath, journal); err != nil { return lifecycleFailure("dependency_journal_failed", err.Error()) } } state, evidence, probeErr := worker.executor.runDependencyProbe(ctx, input.Probe, input.Bindings) if probeErr != nil { return lifecycleFailure("dependency_verify_failed", probeErr.Error()) } if state != "present" { return lifecycleFailure("dependency_verify_missing", "dependency remains missing after install plan") } journal.State = "present" journal.UpdatedAt = time.Now().UTC() if err := persistDependencyJournal(journalPath, journal); err != nil { return lifecycleFailure("dependency_journal_failed", err.Error()) } return dependencySuccess(assignment, input, "present", evidence, len(journal.CompletedSteps)) } func validateDependencyInput(assignment protocol.RunJobAssignment, input protocol.DependencyExecutionInputResponse) error { if input.JobID != assignment.JobID || input.ServerInstanceID != assignment.ServerInstanceID || input.RunEndpointID != assignment.RunEndpointID { return fmt.Errorf("dependency input scope does not match job") } if input.TargetOS != runtime.GOOS || input.TargetArch != runtime.GOARCH { return fmt.Errorf("dependency input target does not match Run") } if !validSHA256(input.PlanDigest) || !protocol.ValidLogicalFileKey(input.ProfileKey) { return fmt.Errorf("dependency input digest or profile is unsafe") } if !protocol.ValidLogicalFileKey(input.Probe.Key) || !protocol.ValidLogicalFileKey(input.Probe.TargetKey) { return fmt.Errorf("dependency probe is unsafe") } if assignment.Capability == protocol.RunCapabilityDependenciesCheck { if assignment.TargetKey != "dependencies/"+input.Probe.Key || input.Plan.Key != "" { return fmt.Errorf("dependency check declaration does not match job") } return nil } if assignment.Capability != protocol.RunCapabilityDependenciesInstall || assignment.TargetKey != "dependencies/install/"+input.Plan.Key || !protocol.ValidLogicalFileKey(input.Plan.Key) || len(input.Plan.Steps) == 0 || len(input.Plan.Steps) > 64 { return fmt.Errorf("dependency install declaration does not match job") } for _, step := range input.Plan.Steps { if !validDependencyInstallStepType(step.Type) { return fmt.Errorf("dependency install step type is unsupported") } } return nil } func validDependencyInstallStepType(value string) bool { switch value { case "package", "verified-download", "manual": return true default: return false } } func (executor LifecycleExecutor) runDependencyProbe(ctx context.Context, probe protocol.DependencyProbe, bindings map[string]string) (string, string, error) { target := strings.TrimSpace(bindings[probe.TargetKey]) if target == "" { target = probe.TargetKey } switch probe.Kind { case "file.exists", "steam.app": info, err := os.Lstat(target) if err != nil { if errors.Is(err, os.ErrNotExist) { return "missing", "declared target is not present", nil } return "", "", err } if info.Mode()&os.ModeSymlink != 0 { return "", "", fmt.Errorf("dependency target cannot be a symlink") } return "present", "declared target is present", nil case "package.installed": if err := validateDependencyExecutable(target); err != nil { return "", "", err } if filepath.IsAbs(target) { if _, err := os.Stat(target); err != nil { return "missing", "declared package executable is not present", nil } } else if _, err := exec.LookPath(target); err != nil { return "missing", "declared package executable is not present", nil } return "present", "declared package executable is present", nil case "command.version", "java.version", "docker.available": if err := validateDependencyExecutable(target); err != nil { return "", "", err } args := []string{"--version"} if probe.Kind == "java.version" { args = []string{"-version"} } result, err := executor.dependencyRunner.Run(ctx, ProcessCommand{Args: append([]string{target}, args...), Timeout: 30 * time.Second, Capability: "dependency.probe", Action: probe.Kind}) if err != nil || result.ExitCode != 0 { if ctx.Err() != nil { return "", "", ctx.Err() } return "missing", "declared executable is not available", nil } version := dependencyVersionEvidence(processOutputEvidence(result.Stdout, result.Stderr)) if probe.MinimumVersion != "" && !dependencyVersionAtLeast(version, probe.MinimumVersion) { return "missing", "declared executable version is below minimum", nil } return "present", version, nil case "service.exists": if !dependencyTokenPattern.MatchString(target) { return "", "", fmt.Errorf("dependency service target is unsafe") } name, args := serviceProbeCommand(runtime.GOOS, target) if name == "" { return "", "", fmt.Errorf("service probe is unsupported on this platform") } result, err := executor.dependencyRunner.Run(ctx, ProcessCommand{Args: append([]string{name}, args...), Timeout: 30 * time.Second, Capability: "dependency.probe", Action: probe.Kind}) if err != nil || result.ExitCode != 0 { if ctx.Err() != nil { return "", "", ctx.Err() } return "missing", "declared service is not present", nil } return "present", "declared service is present", nil default: return "", "", fmt.Errorf("dependency probe kind is unsupported") } } func (executor LifecycleExecutor) runDependencyInstallStep(ctx context.Context, assignment protocol.RunJobAssignment, input protocol.DependencyExecutionInputResponse, step protocol.DependencyInstallStep, index int) error { if !protocol.ValidLogicalFileKey(step.TargetKey) { return fmt.Errorf("dependency install target is unsafe") } switch step.Type { case "package": name, args, err := packageInstallCommand(step.PackageManager, step.PackageName, step.Version) if err != nil { return err } result, runErr := executor.dependencyRunner.Run(ctx, ProcessCommand{Args: append([]string{name}, args...), Timeout: dependencyCommandTimeout, JobID: assignment.JobID, Capability: assignment.Capability, Action: "dependency.package"}) if runErr != nil || result.ExitCode != 0 { if ctx.Err() != nil { return ctx.Err() } return fmt.Errorf("typed package adapter failed") } return nil case "verified-download": if !validSHA256(step.Checksum) { return fmt.Errorf("verified download checksum is required") } if _, err := validateDependencyDownloadURL(step.DownloadRef); err != nil { return err } destination := filepath.Join(executor.workspaceRoot, "dependency-files", safeWorkspaceName(assignment.ServerInstanceID), safeWorkspaceName(step.TargetKey)) size, checksum, err := executor.dependencyDownloader.Download(ctx, step.DownloadRef, destination, maxDependencyDownloadBytes) if err != nil { return err } if size <= 0 || checksum != strings.ToLower(step.Checksum) { _ = os.Remove(destination) return fmt.Errorf("verified dependency download checksum mismatch") } return os.Chmod(destination, 0o700) case "manual": return fmt.Errorf("manual dependency step requires operator action") default: return fmt.Errorf("dependency install step type is unsupported") } } func packageInstallCommand(manager, packageName, version string) (string, []string, error) { if !dependencyTokenPattern.MatchString(packageName) || version != "" && !dependencyVersionPattern.MatchString(version) { return "", nil, fmt.Errorf("package name or version is unsafe") } spec := packageName switch manager { case "apt": if version != "" { spec += "=" + version } return "apt-get", []string{"install", "-y", "--no-install-recommends", spec}, nil case "yum", "dnf": if version != "" { spec += "-" + version } return manager, []string{"install", "-y", spec}, nil case "pacman": return "pacman", []string{"-S", "--noconfirm", spec}, nil case "zypper": return "zypper", []string{"--non-interactive", "install", spec}, nil case "brew": if version != "" { spec += "@" + version } return "brew", []string{"install", spec}, nil case "winget": args := []string{"install", "--id", packageName, "--exact", "--silent", "--accept-package-agreements", "--accept-source-agreements"} if version != "" { args = append(args, "--version", version) } return "winget", args, nil case "choco": args := []string{"install", packageName, "-y", "--no-progress"} if version != "" { args = append(args, "--version", version) } return "choco", args, nil case "scoop": if version != "" { spec += "@" + version } return "scoop", []string{"install", spec}, nil default: return "", nil, fmt.Errorf("package manager is unsupported") } } func validateDependencyExecutable(target string) error { if strings.TrimSpace(target) != target || target == "" || strings.ContainsAny(target, "\r\n\x00") || containsUnsafeRuntimeText(target) { return fmt.Errorf("dependency executable target is unsafe") } if filepath.IsAbs(target) { info, err := os.Lstat(target) if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || info.Mode()&0o111 == 0 { return fmt.Errorf("dependency executable target is not a regular executable") } return nil } if !commandNamePattern.MatchString(target) { return fmt.Errorf("dependency executable name is unsafe") } if _, forbidden := disallowedExecutables[strings.ToLower(target)]; forbidden { return fmt.Errorf("dependency executable cannot be a shell") } return nil } func validateDependencyDownloadURL(raw string) (*url.URL, error) { parsed, err := url.ParseRequestURI(strings.TrimSpace(raw)) if err != nil || parsed.Scheme != "https" || parsed.Hostname() == "" || parsed.User != nil || parsed.Fragment != "" { return nil, fmt.Errorf("dependency download URL is not approved") } host := strings.ToLower(parsed.Hostname()) if host == "localhost" || strings.HasSuffix(host, ".localhost") { return nil, fmt.Errorf("dependency download host is not approved") } if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() || ip.IsLinkLocalUnicast()) { return nil, fmt.Errorf("dependency download host is not approved") } return parsed, nil } func serviceProbeCommand(targetOS, service string) (string, []string) { switch targetOS { case "linux": return "systemctl", []string{"status", service, "--no-pager"} case "windows": return "sc", []string{"query", service} case "darwin": return "launchctl", []string{"print", "system/" + service} default: return "", nil } } func dependencySuccess(assignment protocol.RunJobAssignment, input protocol.DependencyExecutionInputResponse, state, evidence string, completed int) LifecycleExecutionResult { payload, _ := json.Marshal(protocol.DependencyExecutionEvidence{ProbeKey: input.Probe.Key, PlanKey: input.Plan.Key, PlanDigest: input.PlanDigest, State: state, Evidence: evidence, CompletedSteps: completed}) kind := "dependency.check" message := "dependency probe completed" if assignment.Capability == protocol.RunCapabilityDependenciesInstall { kind = "dependency.install" message = "dependency install plan completed and verified" } return LifecycleExecutionResult{State: lifecycleResultStateSucceeded, Progress: protocol.RunJobProgressReport{Percent: 100, Message: message}, ResultRef: fmt.Sprintf("artifact://jobs/%s/dependencies-result", url.PathEscape(assignment.JobID)), Message: message, ExecutionResult: protocol.RunJobExecutionResult{Kind: kind, Checksum: input.PlanDigest, Summary: message, Content: string(payload)}} } func processOutputEvidence(stdout string, stderr string) string { if stdout == "" { return stderr } if stderr == "" { return stdout } return stdout + "\n" + stderr } func loadDependencyJournal(path string, assignment protocol.RunJobAssignment, digest string) (dependencyJournal, error) { journal := dependencyJournal{Version: 1, JobID: assignment.JobID, Attempt: assignment.Attempt, PlanDigest: digest, State: "pending", UpdatedAt: time.Now().UTC()} body, err := os.ReadFile(path) if errors.Is(err, os.ErrNotExist) { return journal, nil } if err != nil { return dependencyJournal{}, err } if err := json.Unmarshal(body, &journal); err != nil { return dependencyJournal{}, fmt.Errorf("decode dependency journal: %w", err) } if journal.Version != 1 || journal.JobID != assignment.JobID || journal.PlanDigest != digest { return dependencyJournal{}, fmt.Errorf("dependency journal does not match immutable plan") } if journal.Attempt > assignment.Attempt { return dependencyJournal{}, fmt.Errorf("dependency journal attempt is newer than assignment") } journal.Attempt = assignment.Attempt return journal, nil } func persistDependencyJournal(path string, journal dependencyJournal) error { body, err := json.MarshalIndent(journal, "", " ") if err != nil { return err } return writeRuntimeAtomicFile(path, body, 0o600) } func writeRuntimeAtomicFile(path string, body []byte, mode os.FileMode) error { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return err } temporary := path + ".tmp" file, err := os.OpenFile(temporary, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode) if err != nil { return err } if _, err := file.Write(body); err != nil { _ = file.Close() _ = os.Remove(temporary) return err } if err := file.Sync(); err != nil { _ = file.Close() _ = os.Remove(temporary) return err } if err := file.Close(); err != nil { _ = os.Remove(temporary) return err } if err := os.Rename(temporary, path); err != nil { _ = os.Remove(temporary) return err } return os.Chmod(path, mode) } func validSHA256(value string) bool { if len(value) != len("sha256:")+64 || !strings.HasPrefix(value, "sha256:") { return false } _, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:")) return err == nil } func dependencyVersionEvidence(output string) string { if output == "" { return "version available" } return output } func dependencyVersionAtLeast(actual, minimum string) bool { numbers := func(value string) []int { parts := regexp.MustCompile(`[0-9]+`).FindAllString(value, -1) out := make([]int, len(parts)) for i, part := range parts { out[i], _ = strconv.Atoi(part) } return out } a, b := numbers(actual), numbers(minimum) for i := 0; i < len(a) || i < len(b); i++ { av, bv := 0, 0 if i < len(a) { av = a[i] } if i < len(b) { bv = b[i] } if av != bv { return av > bv } } return len(a) > 0 }