Skip to content

Commit 455c070

Browse files
committed
Improve validatation for networks.yaml settings
Signed-off-by: Jan Dubois <[email protected]>
1 parent 6a4e200 commit 455c070

File tree

4 files changed

+137
-73
lines changed

4 files changed

+137
-73
lines changed

cmd/limactl/sudoers.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ func newSudoersCommand() *cobra.Command {
1616
Args: cobra.MaximumNArgs(1),
1717
RunE: sudoersAction,
1818
}
19+
configFile, _ := networks.ConfigFile()
1920
sudoersCommand.Flags().Bool("check", false,
20-
"check that the sudoers file is up-to-date with $LIMA_HOME/_config/networks.yaml")
21+
fmt.Sprintf("check that the sudoers file is up-to-date with %q", configFile))
2122
return sudoersCommand
2223
}
2324

@@ -30,39 +31,44 @@ func sudoersAction(cmd *cobra.Command, args []string) error {
3031
return err
3132
}
3233
if check {
33-
var file string
34-
switch len(args) {
35-
case 0:
36-
config, err := networks.Config()
37-
if err != nil {
38-
return err
39-
}
40-
file = config.Paths.Sudoers
41-
if file == "" {
42-
return errors.New("no sudoers file defined in ~/.lima/_config/networks.yaml")
43-
}
44-
case 1:
45-
file = args[0]
46-
default:
47-
return errors.New("can check only a single sudoers file")
48-
}
34+
return verifySudoAccess(args)
35+
}
36+
sudoers, err := networks.Sudoers()
37+
if err != nil {
38+
return err
39+
}
40+
fmt.Print(sudoers)
41+
return nil
42+
}
43+
44+
func verifySudoAccess(args []string) error {
45+
var file string
46+
switch len(args) {
47+
case 0:
4948
config, err := networks.Config()
5049
if err != nil {
5150
return err
5251
}
53-
if err := config.Validate(); err != nil {
54-
return err
55-
}
56-
if err := config.VerifySudoAccess(file); err != nil {
57-
return err
52+
file = config.Paths.Sudoers
53+
if file == "" {
54+
configFile, _ := networks.ConfigFile()
55+
return fmt.Errorf("no sudoers file defined in %q", configFile)
5856
}
59-
fmt.Printf("%q is up-to-date (or sudo doesn't require a password)\n", file)
60-
return nil
57+
case 1:
58+
file = args[0]
59+
default:
60+
return errors.New("can check only a single sudoers file")
6161
}
62-
sudoers, err := networks.Sudoers()
62+
config, err := networks.Config()
6363
if err != nil {
6464
return err
6565
}
66-
fmt.Print(sudoers)
66+
if err := config.Validate(); err != nil {
67+
return err
68+
}
69+
if err := config.VerifySudoAccess(file); err != nil {
70+
return err
71+
}
72+
fmt.Printf("%q is up-to-date (or sudo doesn't require a password)\n", file)
6773
return nil
68-
}
74+
}

pkg/networks/config.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,28 @@ var cache struct {
2323
err error
2424
}
2525

26-
// load() loads the _config/networks.yaml file.
27-
func load() {
26+
func ConfigFile() (string, error) {
27+
configDir, err := dirnames.LimaConfigDir()
28+
if err != nil {
29+
return "", err
30+
}
31+
return filepath.Join(configDir, filenames.NetworksConfig), nil
32+
}
33+
34+
// loadCache loads the _config/networks.yaml file into the cache.
35+
func loadCache() {
2836
cache.Do(func() {
29-
var configDir string
30-
configDir, cache.err = dirnames.LimaConfigDir()
37+
var configFile string
38+
configFile, cache.err = ConfigFile()
3139
if cache.err != nil {
3240
return
3341
}
34-
configFile := filepath.Join(configDir, filenames.NetworksConfig)
3542
_, cache.err = os.Stat(configFile)
3643
if cache.err != nil {
3744
if !errors.Is(cache.err, os.ErrNotExist) {
3845
return
3946
}
47+
configDir := filepath.Dir(configFile)
4048
cache.err = os.MkdirAll(configDir, 0700)
4149
if cache.err != nil {
4250
cache.err = fmt.Errorf("could not create %q directory: %w", configDir, cache.err)
@@ -61,15 +69,15 @@ func load() {
6169

6270
// Config returns the network config from the _config/networks.yaml file.
6371
func Config() (NetworksConfig, error) {
64-
if runtime.GOOS == "darwin" {
65-
load()
66-
return cache.config, cache.err
72+
if runtime.GOOS != "darwin" {
73+
return NetworksConfig{}, errors.New("networks.yaml configuration is only supported on macOS right now")
6774
}
68-
return NetworksConfig{}, errors.New("networks.yaml configuration is only supported on macOS right now")
75+
loadCache()
76+
return cache.config, cache.err
6977
}
7078

7179
func VDESock(name string) (string, error) {
72-
load()
80+
loadCache()
7381
if cache.err != nil {
7482
return "", cache.err
7583
}

pkg/networks/reconcile/reconcile.go

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"runtime"
1010
"strings"
1111
"sync"
12+
"syscall"
13+
"time"
1214

1315
"github.com/lima-vm/lima/pkg/networks"
1416
"github.com/lima-vm/lima/pkg/store"
@@ -24,12 +26,10 @@ func Reconcile(ctx context.Context, newInst string) error {
2426
if err != nil {
2527
return err
2628
}
27-
2829
instances, err := store.Instances()
2930
if err != nil {
3031
return err
3132
}
32-
3333
activeNetwork := make(map[string]bool, 3)
3434
for _, instName := range instances {
3535
instance, err := store.Inspect(instName)
@@ -80,17 +80,50 @@ func sudo(user, group, command string) error {
8080
return nil
8181
}
8282

83-
func startDaemon(config *networks.NetworksConfig, ctx context.Context, name, daemon string) error {
83+
func makeVarRun(config *networks.NetworksConfig) error {
8484
err := sudo("root", "wheel", config.MkdirCmd())
8585
if err != nil {
8686
return err
8787
}
8888

89-
networksDir, _ := dirnames.LimaNetworksDir()
89+
// Check that VarRun is daemon-group writable. If we don't report it here, the error would only be visible
90+
// in the vde_switch daemon log. This has not been checked by networks.Validate() because only the VarRun
91+
// directory itself needs to be daemon-group writable, any parents just need to be daemon-group executable.
92+
fi, err := os.Stat(config.Paths.VarRun)
93+
if err != nil {
94+
return err
95+
}
96+
stat, ok := fi.Sys().(*syscall.Stat_t)
97+
if !ok {
98+
// should never happen
99+
return fmt.Errorf("could not retrieve stat buffer for %q", config.Paths.VarRun)
100+
}
101+
if fi.Mode()&020 == 0 || stat.Gid != networks.DaemonGID {
102+
return fmt.Errorf("%q doesn't seem to be writable by the daemon (gid:%d) group",
103+
config.Paths.VarRun, networks.DaemonGID)
104+
}
105+
return nil
106+
}
107+
108+
func startDaemon(config *networks.NetworksConfig, ctx context.Context, name, daemon string) error {
109+
if err := makeVarRun(config); err != nil {
110+
return err
111+
}
112+
networksDir, err := dirnames.LimaNetworksDir()
113+
if err != nil {
114+
return err
115+
}
90116
if err := os.MkdirAll(networksDir, 0755); err != nil {
91117
return err
92118
}
93119

120+
args := []string{"--user", config.DaemonUser(daemon), "--group", config.DaemonGroup(daemon), "--non-interactive"}
121+
args = append(args, strings.Split(config.StartCmd(name, daemon), " ")...)
122+
cmd := exec.CommandContext(ctx, "sudo", args...)
123+
// set directory to a path the daemon user has read access to because vde_switch calls getcwd() which
124+
// can fail when called from directories like ~/Downloads, which has 700 permissions
125+
cmd.Dir = config.Paths.VarRun
126+
94127
stdoutPath := config.LogFile(name, daemon, "stdout")
95128
stderrPath := config.LogFile(name, daemon, "stderr")
96129
if err := os.RemoveAll(stdoutPath); err != nil {
@@ -99,25 +132,16 @@ func startDaemon(config *networks.NetworksConfig, ctx context.Context, name, dae
99132
if err := os.RemoveAll(stderrPath); err != nil {
100133
return err
101134
}
102-
stdoutW, err := os.Create(stdoutPath)
135+
136+
cmd.Stdout, err = os.Create(stdoutPath)
103137
if err != nil {
104138
return err
105139
}
106-
// no defer stdoutW.Close()
107-
stderrW, err := os.Create(stderrPath)
140+
cmd.Stderr, err = os.Create(stderrPath)
108141
if err != nil {
109142
return err
110143
}
111-
// no defer stderrW.Close()
112144

113-
args := []string{"--user", config.DaemonUser(daemon), "--group", config.DaemonGroup(daemon), "--non-interactive"}
114-
args = append(args, strings.Split(config.StartCmd(name, daemon), " ")...)
115-
cmd := exec.CommandContext(ctx, "sudo", args...)
116-
// set directory to a path the daemon user has read access to because vde_switch calls getcwd() which
117-
// can fail when called from directories like ~/Downloads, which has 700 permissions
118-
cmd.Dir = config.Paths.VarRun
119-
cmd.Stdout = stdoutW
120-
cmd.Stderr = stderrW
121145
logrus.Debugf("Starting %q daemon for %q network: %v", daemon, name, cmd.Args)
122146
if err := cmd.Start(); err != nil {
123147
return err
@@ -174,7 +198,20 @@ func stopNetwork(config *networks.NetworksConfig, name string) error {
174198
return err
175199
}
176200
}
177-
// TODO: wait for VMNet to terminate before stopping Switch, otherwise the socket may not get deleted
201+
// wait for VMNet to terminate (up to 5s) before stopping Switch, otherwise the socket may not get deleted
202+
if daemon == networks.VMNet {
203+
startWaiting := time.Now()
204+
for {
205+
if pid, _ := store.ReadPIDFile(config.PIDFile(name, daemon)); pid == 0 {
206+
break
207+
}
208+
if time.Since(startWaiting) > 5 * time.Second {
209+
logrus.Infof("%q daemon for %q network still running after 5 seconds", daemon, name)
210+
break
211+
}
212+
time.Sleep(500 * time.Millisecond)
213+
}
214+
}
178215
}
179216
return nil
180217
}

pkg/networks/validate.go

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (config *NetworksConfig) Validate() error {
2525
if name == "varRun" {
2626
path = findBaseDirectory(path)
2727
}
28-
err := validatePath(path)
28+
err := validatePath(path, name == "varRun")
2929
if err != nil {
3030
// sudoers file does not need to exist; otherwise `limactl sudoers` couldn't bootstrap
3131
if name == "sudoers" && errors.Is(err, os.ErrNotExist) {
@@ -34,21 +34,27 @@ func (config *NetworksConfig) Validate() error {
3434
return fmt.Errorf("networks.yaml field `paths.%s` error: %w", name, err)
3535
}
3636
}
37-
// TODO: validate network definitions
37+
// TODO(jandubois): validate network definitions
3838
return nil
3939
}
4040

4141
// findBaseDirectory removes non-existing directories from the end of the path.
4242
func findBaseDirectory(path string) string {
43-
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
43+
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
4444
if path != "/" {
4545
return findBaseDirectory(filepath.Dir(path))
4646
}
4747
}
4848
return path
4949
}
5050

51-
func validatePath(path string) error {
51+
const (
52+
RootUID = 0
53+
WheelGID = 0
54+
DaemonGID = 1
55+
)
56+
57+
func validatePath(path string, allowDaemonGroupWritable bool) error {
5258
if path == "" {
5359
return nil
5460
}
@@ -67,28 +73,35 @@ func validatePath(path string) error {
6773
file = "dir"
6874
}
6975
// TODO: should we allow symlinks when both the link and the target are secure?
70-
// E.g. on macOS /var is a symlink to /private/var
76+
// E.g. on macOS /var is a symlink to /private/var, /etc to /private/etc
7177
if (fi.Mode() & fs.ModeSymlink) != 0 {
7278
return fmt.Errorf("%s %q is a symlink", file, path)
7379
}
74-
if stat, ok := fi.Sys().(*syscall.Stat_t); ok {
75-
if stat.Uid != 0 {
76-
return fmt.Errorf("%s %q is not owned by 0 (root), but by %d", file, path, stat.Uid)
77-
}
78-
if (fi.Mode() & 020) != 0 {
79-
if stat.Gid != 0 && stat.Gid != 1 {
80-
return fmt.Errorf("%s %q is group-writable and group is neither 0 (wheel) nor 1 (daemon), but is %d", file, path, stat.Gid)
81-
}
82-
}
83-
if (fi.Mode() & 002) != 0 {
84-
return fmt.Errorf("%s %q is world-writable", file, path)
85-
}
86-
} else {
80+
stat, ok := fi.Sys().(*syscall.Stat_t)
81+
if !ok {
8782
// should never happen
8883
return fmt.Errorf("could not retrieve stat buffer for %q", path)
8984
}
85+
if stat.Uid != RootUID {
86+
return fmt.Errorf(`%s %q is not owned by root (uid: %d), but by uid %d`, file, path, RootUID, stat.Uid)
87+
}
88+
if allowDaemonGroupWritable {
89+
if fi.Mode()&020 != 0 && stat.Gid != WheelGID && stat.Gid != DaemonGID {
90+
return fmt.Errorf(`%s %q is group-writable and group is neither "wheel" (gid: %d) nor "daemon" (guid: %d), but is gid: %d`,
91+
file, path, WheelGID, DaemonGID, stat.Gid)
92+
}
93+
if fi.Mode().IsDir() && fi.Mode()&1 == 0 && (fi.Mode()&0010 == 0 || stat.Gid != DaemonGID) {
94+
return fmt.Errorf(`%s %q is not executable by the "daemon" (gid: %d)" group`, file, path, DaemonGID)
95+
}
96+
} else if fi.Mode()&020 != 0 && stat.Gid != WheelGID {
97+
return fmt.Errorf(`%s %q is group-writable and group is not "wheel" (gid: %d), but is gid: %d`,
98+
file, path, WheelGID, stat.Gid)
99+
}
100+
if fi.Mode()&002 != 0 {
101+
return fmt.Errorf("%s %q is world-writable", file, path)
102+
}
90103
if path != "/" {
91-
return validatePath(filepath.Dir(path))
104+
return validatePath(filepath.Dir(path), allowDaemonGroupWritable)
92105
}
93106
return nil
94107
}

0 commit comments

Comments
 (0)