github.com/MerlinKodo/gvisor@v0.0.0-20231110090155-957f62ecf90e/runsc/cmd/mitigate.go (about)

     1  // Copyright 2021 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cmd
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"runtime"
    23  
    24  	"github.com/MerlinKodo/gvisor/pkg/log"
    25  	"github.com/MerlinKodo/gvisor/runsc/cmd/util"
    26  	"github.com/MerlinKodo/gvisor/runsc/flag"
    27  	"github.com/MerlinKodo/gvisor/runsc/mitigate"
    28  	"github.com/google/subcommands"
    29  )
    30  
    31  const (
    32  	// cpuInfo is the path used to parse CPU info.
    33  	cpuInfo = "/proc/cpuinfo"
    34  	// Path to enable/disable SMT.
    35  	smtPath = "/sys/devices/system/cpu/smt/control"
    36  )
    37  
    38  // Mitigate implements subcommands.Command for the "mitigate" command.
    39  type Mitigate struct {
    40  	// Run the command without changing the underlying system.
    41  	dryRun bool
    42  	// Reverse mitigate by turning on all CPU cores.
    43  	reverse bool
    44  	// Extra data for post mitigate operations.
    45  	data string
    46  	// Control to mitigate/reverse smt.
    47  	control machineControl
    48  }
    49  
    50  // Name implements subcommands.command.name.
    51  func (*Mitigate) Name() string {
    52  	return "mitigate"
    53  }
    54  
    55  // Synopsis implements subcommands.Command.Synopsis.
    56  func (*Mitigate) Synopsis() string {
    57  	return "mitigate mitigates the underlying system against side channel attacks"
    58  }
    59  
    60  // Usage implements Usage for cmd.Mitigate.
    61  func (m *Mitigate) Usage() string {
    62  	return fmt.Sprintf(`mitigate [flags]
    63  
    64  mitigate mitigates a system to the "MDS" vulnerability by writing "off" to %q. CPUs can be restored by writing "on" to the same file or rebooting your system.
    65  
    66  The command can be reversed with --reverse, which writes "on" to the file above.%s`, smtPath, m.usage())
    67  }
    68  
    69  // SetFlags sets flags for the command Mitigate.
    70  func (m *Mitigate) SetFlags(f *flag.FlagSet) {
    71  	f.BoolVar(&m.dryRun, "dryrun", false, "run the command without changing system")
    72  	f.BoolVar(&m.reverse, "reverse", false, "reverse mitigate by enabling all CPUs")
    73  	m.setFlags(f)
    74  }
    75  
    76  // Execute implements subcommands.Command.Execute.
    77  func (m *Mitigate) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus {
    78  	if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
    79  		log.Warningf("As ARM is not affected by MDS, mitigate does not support ARM machines.")
    80  		// Set reverse flag so that we still perform post mitigate operations. mitigate reverse is a noop in this case.
    81  		m.reverse = true
    82  	}
    83  
    84  	if f.NArg() != 0 {
    85  		f.Usage()
    86  		return subcommands.ExitUsageError
    87  	}
    88  	m.control = &machineControlImpl{}
    89  	return m.execute()
    90  }
    91  
    92  // execute executes mitigate operations. Seperate from Execute method for
    93  // easier mocking.
    94  func (m *Mitigate) execute() subcommands.ExitStatus {
    95  	beforeSet, err := m.control.getCPUs()
    96  	if err != nil {
    97  		return util.Errorf("Get before CPUSet failed: %v", err)
    98  	}
    99  	log.Infof("CPUs before: %s", beforeSet.String())
   100  
   101  	if err := m.doEnableDisable(beforeSet); err != nil {
   102  		return util.Errorf("Enabled/Disable action failed on %q: %v", smtPath, err)
   103  	}
   104  
   105  	afterSet, err := m.control.getCPUs()
   106  	if err != nil {
   107  		return util.Errorf("Get after CPUSet failed: %v", err)
   108  	}
   109  	log.Infof("CPUs after: %s", afterSet.String())
   110  
   111  	if err = m.postMitigate(afterSet); err != nil {
   112  		return util.Errorf("Post Mitigate failed: %v", err)
   113  	}
   114  
   115  	return subcommands.ExitSuccess
   116  }
   117  
   118  // doEnableDisable does either enable or disable operation based on flags.
   119  func (m *Mitigate) doEnableDisable(set mitigate.CPUSet) error {
   120  	if m.reverse {
   121  		if m.dryRun {
   122  			log.Infof("Skipping reverse action because dryrun is set.")
   123  			return nil
   124  		}
   125  		return m.control.enable()
   126  	}
   127  	if m.dryRun {
   128  		log.Infof("Skipping mitigate action because dryrun is set.")
   129  		return nil
   130  	}
   131  	if set.IsVulnerable() {
   132  		return m.control.disable()
   133  	}
   134  	log.Infof("CPUs not vulnerable. Skipping disable call.")
   135  	return nil
   136  }
   137  
   138  // Interface to wrap interactions with underlying machine. Done
   139  // so testing with mocks can be done hermetically.
   140  type machineControl interface {
   141  	enable() error
   142  	disable() error
   143  	isEnabled() (bool, error)
   144  	getCPUs() (mitigate.CPUSet, error)
   145  }
   146  
   147  // Implementation of SMT control interaction with the underlying machine.
   148  type machineControlImpl struct{}
   149  
   150  func (*machineControlImpl) enable() error {
   151  	return checkFileExistsOnWrite("enable", "on")
   152  }
   153  
   154  func (*machineControlImpl) disable() error {
   155  	return checkFileExistsOnWrite("disable", "off")
   156  }
   157  
   158  // Writes data to SMT control. If file not found, logs file not exist error and returns nil
   159  // error, which is done because machines without the file pointed to by smtPath only have one
   160  // thread per core in the first place. Otherwise returns error from ioutil.WriteFile.
   161  func checkFileExistsOnWrite(op, data string) error {
   162  	err := ioutil.WriteFile(smtPath, []byte(data), 0644)
   163  	if err != nil && os.IsExist(err) {
   164  		log.Infof("File %q does not exist for operation %s. This machine probably has no smt control.", smtPath, op)
   165  		return nil
   166  	}
   167  	return err
   168  }
   169  
   170  func (*machineControlImpl) isEnabled() (bool, error) {
   171  	data, err := ioutil.ReadFile(cpuInfo)
   172  	return string(data) == "on", err
   173  }
   174  
   175  func (*machineControlImpl) getCPUs() (mitigate.CPUSet, error) {
   176  	data, err := ioutil.ReadFile(cpuInfo)
   177  	if err != nil {
   178  		return nil, fmt.Errorf("failed to read %s: %w", cpuInfo, err)
   179  	}
   180  	set, err := mitigate.NewCPUSet(string(data))
   181  	if err != nil {
   182  		return nil, fmt.Errorf("getCPUs: %v", err)
   183  	}
   184  	return set, nil
   185  }