Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: unified mkdir api with options #14408

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion asserts/gpgkeypairmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ func ensureGPGHomeDirectory() (string, error) {
homedir = filepath.Join(real.HomeDir, ".snap", "gnupg")
}

if err := osutil.MkdirAllChown(homedir, 0700, uid, gid); err != nil {
if err := osutil.Mkdir(homedir, 0700, &osutil.MkdirOptions{
MakeParents: true,
ExistOK: true,
Chmod: true,
Chown: true,
UserID: uid,
GroupID: gid,
}); err != nil {
return "", err
}
return homedir, nil
Expand Down
9 changes: 8 additions & 1 deletion cmd/snap/cmd_warnings.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,14 @@ func writeWarningTimestamp(t time.Time) error {
}

filename := warnFilename(user.HomeDir)
if err := osutil.MkdirAllChown(filepath.Dir(filename), 0700, uid, gid); err != nil {
if err := osutil.Mkdir(filepath.Dir(filename), 0700, &osutil.MkdirOptions{
MakeParents: true,
ExistOK: true,
Chmod: true,
Chown: true,
UserID: uid,
GroupID: gid,
}); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ require (
gopkg.in/yaml.v3 v3.0.1
)

require go.etcd.io/bbolt v1.3.9
require go.etcd.io/bbolt v1.3.6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note-to-self: check if this is packaged

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is a downgrade? What is this about? It looks entirely unrelated.


require (
github.com/canonical/go-sp800.108-kdf v0.0.0-20210314145419-a3359f2d21b9 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ github.com/snapcore/secboot v0.0.0-20240411101434-f3ad7c92552a h1:yzzVi0yUosDYkj
github.com/snapcore/secboot v0.0.0-20240411101434-f3ad7c92552a/go.mod h1:72paVOkm4sJugXt+v9ItmnjXgO921D8xqsbH2OekouY=
github.com/snapcore/snapd v0.0.0-20201005140838-501d14ac146e/go.mod h1:3xrn7QDDKymcE5VO2rgWEQ5ZAUGb9htfwlXnoel6Io8=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
Expand All @@ -68,9 +70,11 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
Expand Down
164 changes: 164 additions & 0 deletions osutil/mkdir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package osutil

import (
"os"
"path/filepath"
"sync"
"syscall"

"github.com/snapcore/snapd/osutil/sys"
)

// XXX: we need to come back and fix this; this is a hack to unblock us.
// Have a lock so that if one goroutine tries to Mkdir /foo/bar, and
// another tries to Mkdir /foo/baz, they can't both decide they need
// to make /foo and then have one fail.
var mu sync.Mutex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of forcing synchronization, maybe simply document that for the caller? although as a consumer of the API I would not expect anything different than the racy behavior, which I think is consistent with how things generally work across different languages/libraries.

And in fact you cannot do anything about racing with other processes, so I'm not sure it's worth to even worry about this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for pointing it out, but this logic is in the current code base, see here.

This PR only refactors the API interface and does not add new or change existing logic. The API is renamed, and so is the file name, making it hard to show a diff for reviewing.


// MkdirOptions holds the options for a call to Mkdir.
type MkdirOptions struct {
// If false (the default), return an error if the parent directory is missing.
// If true, any missing parent directories of the given path are created
// (with the same permissions and owner/group as the leaf directory).
MakeParents bool

// If false (the default), return an error if the target directory already exists.
// If true, don't return an error if the target directory already exists (unless
// it's not a directory).
ExistOK bool

// If false (the default), no explicit chmod is performed. In this case, the permission
// of the created directories will be affected by umask settings.
//
// If true, perform an explicit chmod on any directories created.
Chmod bool

// If false (the default), no explicit chown is performed.
// If true, perform an explicit chown on any directories created, using the UserID
// and GroupID provided.
Chown bool
UserID sys.UserID
GroupID sys.GroupID
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add the mutex here, so that any goroutines that want to synchronize, can do so explicitly. Alternatively ExistOK could be a function that grabs a mutex and returns false, alternatively ExistOK could be default, since we have other tools for making temporary directories.

Having said that, I'm not sure what's the right approach


// Mkdir creates a directory at path with the permissions perm and the options provided.
// If options is nil, it is treated as &MkdirOptions{}.
func Mkdir(path string, perm os.FileMode, options *MkdirOptions) error {
if options == nil {
options = &MkdirOptions{}
}
mu.Lock()
defer mu.Unlock()

path = filepath.Clean(path)

if s, err := os.Stat(path); err == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a descending openat + mkdirat tree. This is just racy with the outside world and is not correct.

// If path exists but not as a directory, return a "not a directory" error.
if !s.IsDir() {
return &os.PathError{
Op: "mkdir",
Path: path,
Err: syscall.ENOTDIR,
}
}

// If path exists as a directory, and ExistOK option is set, do nothing.
if options.ExistOK {
return nil
}

// If path exists but ExistOK option isn't set, return a "file exists" error.
return &os.PathError{
Op: "mkdir",
Path: path,
Err: syscall.EEXIST,
}
}

// If path doesn't exist, create it.
return mkdirAll(path, perm, options)
}

// create directories recursively
func mkdirAll(path string, perm os.FileMode, options *MkdirOptions) error {
// if path exists
if s, err := os.Stat(path); err == nil {
if s.IsDir() {
return nil
}

// If path exists but not as a directory, return a "not a directory" error.
return &os.PathError{
Op: "mkdir",
Path: path,
Err: syscall.ENOTDIR,
}
}

// If path doesn't exist, and MakeParents is specified in options,
// create all directories recursively.
if options.MakeParents {
parent := filepath.Dir(path)
if parent != "/" {
if err := mkdirAll(parent, perm, options); err != nil {
return err
}
}
}

// If path doesn't exist, and MakeParents isn't specified in options,
// create a single directory.
return mkdir(path, perm, options)
}

// Create a single directory and perform chown/chmod operations according to options.
func mkdir(path string, perm os.FileMode, options *MkdirOptions) error {
cand := path + ".mkdir-new"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is safe. If we don't want someone to see the new directory with old permissions or old ownership, we don't really prevent that. Perhaps mkdir zero permissions and set to non-zero after doing chown?


if err := os.Mkdir(cand, perm); err != nil && !os.IsExist(err) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beyond this point the candidate is left behind on errors, isn't it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I did not quite understand your question, what do you mean by "the candidate is left behind on errors"? Could you elaborate? (FYI, this logic too is from the existing code.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that a temporary directory <name>.mkdir-new gets created here, but there is no defererred cleanup on errors, so should any error occur later in Chmod/Chown/Rename, that directory will be left behind.

And yes, it' looks like the problem exists in the code we have in master, so even more reason to fix when introducing a new API.

return err
}

if options.Chown {
if err := sys.ChownPath(cand, options.UserID, options.GroupID); err != nil {
return err
}
}

if options.Chmod {
if err := os.Chmod(cand, perm); err != nil {
return err
}
}

if err := os.Rename(cand, path); err != nil {
return err
}

fd, err := os.Open(filepath.Dir(path))
if err != nil {
return err
}
defer fd.Close()

return fd.Sync()
}
Loading
Loading