all: fix more bugs

This commit is contained in:
Eugene Burkov 2024-10-28 16:24:56 +03:00
parent a22b0d265e
commit 993e351712
6 changed files with 209 additions and 134 deletions

View file

@ -9,7 +9,6 @@ import (
"path/filepath" "path/filepath"
"unsafe" "unsafe"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@ -17,7 +16,7 @@ import (
// fileInfo is a Windows implementation of [fs.FileInfo], that contains the // fileInfo is a Windows implementation of [fs.FileInfo], that contains the
// filemode converted from the security descriptor. // filemode converted from the security descriptor.
type fileInfo struct { type fileInfo struct {
// fs.FileInfo is embedded to provide the default implementations and info // fs.FileInfo is embedded to provide the default implementations and data
// successfully retrieved by [os.Stat]. // successfully retrieved by [os.Stat].
fs.FileInfo fs.FileInfo
@ -33,38 +32,21 @@ func (fi *fileInfo) Mode() (mode fs.FileMode) { return fi.mode }
// stat is a Windows implementation of [Stat]. // stat is a Windows implementation of [Stat].
func stat(name string) (fi os.FileInfo, err error) { func stat(name string) (fi os.FileInfo, err error) {
fi, err = os.Stat(name) absName, err := filepath.Abs(name)
if err != nil { if err != nil {
return nil, fmt.Errorf("computing absolute path: %w", err)
}
fi, err = os.Stat(absName)
if err != nil {
// Don't wrap the error, since it's informative enough as is.
return nil, err return nil, err
} }
const objectType windows.SE_OBJECT_TYPE = windows.SE_FILE_OBJECT dacl, owner, group, err := retrieveDACL(absName)
secInfo := windows.SECURITY_INFORMATION(
windows.OWNER_SECURITY_INFORMATION |
windows.GROUP_SECURITY_INFORMATION |
windows.DACL_SECURITY_INFORMATION |
windows.PROTECTED_DACL_SECURITY_INFORMATION,
)
sd, err := windows.GetNamedSecurityInfo(fi.Name(), objectType, secInfo)
if err != nil { if err != nil {
return nil, fmt.Errorf("getting security descriptor: %w", err) // Don't wrap the error, since it's informative enough as is.
} return nil, err
dacl, _, err := sd.DACL()
if err != nil {
return nil, fmt.Errorf("getting discretionary access control list: %w", err)
}
owner, _, err := sd.Owner()
if err != nil {
return nil, fmt.Errorf("getting owner sid: %w", err)
}
group, _, err := sd.Group()
if err != nil {
return nil, fmt.Errorf("getting group sid: %w", err)
} }
var ownerMask, groupMask, otherMask windows.ACCESS_MASK var ownerMask, groupMask, otherMask windows.ACCESS_MASK
@ -86,18 +68,53 @@ func stat(name string) (fi os.FileInfo, err error) {
} }
} }
mode := masksToPerm(ownerMask, groupMask, otherMask) | (fi.Mode().Perm() & ^fs.ModePerm) mode := fi.Mode()
perm := masksToPerm(ownerMask, groupMask, otherMask, mode.IsDir())
return &fileInfo{ return &fileInfo{
FileInfo: fi, FileInfo: fi,
mode: mode, // Use the file mode from the security descriptor, but use the
// calculated permission bits.
mode: perm | mode&^fs.FileMode(0o777),
}, nil }, nil
} }
// retrieveDACL retrieves the discretionary access control list, owner, and
// group from the security descriptor of the file with the specified absolute
// name.
func retrieveDACL(absName string) (dacl *windows.ACL, owner, group *windows.SID, err error) {
// desiredSecInfo defines the parts of a security descriptor to retrieve.
const desiredSecInfo windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION |
windows.GROUP_SECURITY_INFORMATION |
windows.DACL_SECURITY_INFORMATION |
windows.PROTECTED_DACL_SECURITY_INFORMATION |
windows.UNPROTECTED_DACL_SECURITY_INFORMATION
sd, err := windows.GetNamedSecurityInfo(absName, windows.SE_FILE_OBJECT, desiredSecInfo)
if err != nil {
return nil, nil, nil, fmt.Errorf("getting security descriptor: %w", err)
}
dacl, _, err = sd.DACL()
if err != nil {
return nil, nil, nil, fmt.Errorf("getting discretionary access control list: %w", err)
}
owner, _, err = sd.Owner()
if err != nil {
return nil, nil, nil, fmt.Errorf("getting owner sid: %w", err)
}
group, _, err = sd.Group()
if err != nil {
return nil, nil, nil, fmt.Errorf("getting group sid: %w", err)
}
return dacl, owner, group, nil
}
// chmod is a Windows implementation of [Chmod]. // chmod is a Windows implementation of [Chmod].
func chmod(name string, perm fs.FileMode) (err error) { func chmod(name string, perm fs.FileMode) (err error) {
const objectType windows.SE_OBJECT_TYPE = windows.SE_FILE_OBJECT
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
return fmt.Errorf("getting file info: %w", err) return fmt.Errorf("getting file info: %w", err)
@ -106,7 +123,10 @@ func chmod(name string, perm fs.FileMode) (err error) {
entries := make([]windows.EXPLICIT_ACCESS, 0, 3) entries := make([]windows.EXPLICIT_ACCESS, 0, 3)
creatorMask, groupMask, worldMask := permToMasks(perm, fi.IsDir()) creatorMask, groupMask, worldMask := permToMasks(perm, fi.IsDir())
sidMasks := container.KeyValues[windows.WELL_KNOWN_SID_TYPE, windows.ACCESS_MASK]{{ sidMasks := []struct {
Key windows.WELL_KNOWN_SID_TYPE
Value windows.ACCESS_MASK
}{{
Key: windows.WinCreatorOwnerSid, Key: windows.WinCreatorOwnerSid,
Value: creatorMask, Value: creatorMask,
}, { }, {
@ -148,11 +168,11 @@ func chmod(name string, perm fs.FileMode) (err error) {
return fmt.Errorf("creating access control list: %w", err) return fmt.Errorf("creating access control list: %w", err)
} }
secInfo := windows.SECURITY_INFORMATION( // secInfo defines the parts of a security descriptor to set.
windows.DACL_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION, const secInfo windows.SECURITY_INFORMATION = windows.DACL_SECURITY_INFORMATION |
) windows.PROTECTED_DACL_SECURITY_INFORMATION
err = windows.SetNamedSecurityInfo(name, objectType, secInfo, nil, nil, acl, nil) err = windows.SetNamedSecurityInfo(name, windows.SE_FILE_OBJECT, secInfo, nil, nil, acl, nil)
if err != nil { if err != nil {
return fmt.Errorf("setting security descriptor: %w", err) return fmt.Errorf("setting security descriptor: %w", err)
} }
@ -250,91 +270,131 @@ func newWellKnownTrustee(stype windows.WELL_KNOWN_SID_TYPE) (t *windows.TRUSTEE,
}, nil }, nil
} }
// Constants reflecting the UNIX permission bits. // UNIX file mode permission bits.
const ( const (
ownerWrite = 0b010_000_000 permRead = 0b100
groupWrite = 0b000_010_000 permWrite = 0b010
worldWrite = 0b000_000_010 permExecute = 0b001
ownerRead = 0b100_000_000
groupRead = 0b000_100_000
worldRead = 0b000_000_100
ownerAll = 0b111_000_000
groupAll = 0b000_111_000
worldAll = 0b000_000_111
) )
// Constants reflecting the number of bits to shift the UNIX permission bits to // Windows access masks for appropriate UNIX file mode permission bits and
// convert them to the generic access rights used by Windows, see // file types.
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/access-mask.
const ( const (
genericOwner = 23 fileReadRights = windows.READ_CONTROL |
genericGroup = 26 windows.FILE_READ_DATA |
genericWorld = 29 windows.FILE_READ_ATTRIBUTES |
) windows.FILE_READ_EA |
windows.SYNCHRONIZE |
windows.ACCESS_SYSTEM_SECURITY
// Constants reflecting the number of bits to shift the UNIX write permission fileWriteRights = windows.WRITE_DAC |
// bits to convert them to the access rights used by Windows. windows.WRITE_OWNER |
const ( windows.FILE_WRITE_DATA |
deleteOwner = 9 windows.FILE_WRITE_ATTRIBUTES |
deleteGroup = 12 windows.FILE_WRITE_EA |
deleteWorld = 15 windows.DELETE |
windows.FILE_APPEND_DATA |
windows.SYNCHRONIZE |
windows.ACCESS_SYSTEM_SECURITY
listDirOwner = 7 fileExecuteRights = windows.FILE_EXECUTE
listDirGroup = 10
listDirWorld = 13
traverseOwner = 2 dirReadRights = windows.READ_CONTROL |
traverseGroup = 5 windows.FILE_LIST_DIRECTORY |
traverseWorld = 8 windows.FILE_READ_EA |
windows.FILE_READ_ATTRIBUTES<<1 |
windows.SYNCHRONIZE |
windows.ACCESS_SYSTEM_SECURITY
writeEAOwner = 2 dirWriteRights = windows.WRITE_DAC |
writeEAGroup = 1 windows.WRITE_OWNER |
writeEAWorld = 4 windows.DELETE |
windows.FILE_WRITE_DATA |
windows.FILE_APPEND_DATA |
windows.FILE_WRITE_EA |
windows.FILE_WRITE_ATTRIBUTES<<1 |
windows.SYNCHRONIZE |
windows.ACCESS_SYSTEM_SECURITY
deleteChildOwner = 1 dirExecuteRights = windows.FILE_TRAVERSE
deleteChildGroup = 4
deleteChildWorld = 7
) )
// permToMasks converts a UNIX file mode permissions to the corresponding // permToMasks converts a UNIX file mode permissions to the corresponding
// Windows access masks. The [isDir] argument is used to set specific access // Windows access masks. The [isDir] argument is used to set specific access
// bits for directories. // bits for directories.
func permToMasks(fm os.FileMode, isDir bool) (owner, group, world windows.ACCESS_MASK) { func permToMasks(fm os.FileMode, isDir bool) (owner, group, world windows.ACCESS_MASK) {
mask := windows.ACCESS_MASK(fm.Perm()) mask := fm.Perm()
owner = ((mask & ownerAll) << genericOwner) | ((mask & ownerWrite) << deleteOwner) owner = permToMask(byte((mask>>6)&0b111), isDir)
group = ((mask & groupAll) << genericGroup) | ((mask & groupWrite) << deleteGroup) group = permToMask(byte((mask>>3)&0b111), isDir)
world = ((mask & worldAll) << genericWorld) | ((mask & worldWrite) << deleteWorld) world = permToMask(byte(mask&0b111), isDir)
if isDir {
owner |= (mask & ownerRead) << listDirOwner
group |= (mask & groupRead) << listDirGroup
world |= (mask & worldRead) << listDirWorld
owner |= (mask & ownerRead) << traverseOwner
group |= (mask & groupRead) << traverseGroup
world |= (mask & worldRead) << traverseWorld
owner |= (mask & ownerWrite) << deleteChildOwner
group |= (mask & groupWrite) << deleteChildGroup
world |= (mask & worldWrite) << deleteChildWorld
owner |= (mask & ownerWrite) >> writeEAOwner
group |= (mask & groupWrite) << writeEAGroup
world |= (mask & worldWrite) << writeEAWorld
}
return owner, group, world return owner, group, world
} }
// permToMask converts a UNIX file mode permission bits within p byte to the
// corresponding Windows access mask. The [isDir] argument is used to set
// specific access bits for directories.
func permToMask(p byte, isDir bool) (mask windows.ACCESS_MASK) {
if p&permRead != 0 {
if isDir {
mask |= dirReadRights
} else {
mask |= fileReadRights
}
}
if p&permWrite != 0 {
if isDir {
mask |= dirWriteRights
} else {
mask |= fileWriteRights
}
}
if p&permExecute != 0 {
if isDir {
mask |= dirExecuteRights
} else {
mask |= fileExecuteRights
}
}
return mask
}
// masksToPerm converts Windows access masks to the corresponding UNIX file // masksToPerm converts Windows access masks to the corresponding UNIX file
// mode permission bits. // mode permission bits.
func masksToPerm(u, g, o windows.ACCESS_MASK) (perm os.FileMode) { func masksToPerm(u, g, o windows.ACCESS_MASK, isDir bool) (perm fs.FileMode) {
perm |= os.FileMode(((u >> genericOwner) & ownerAll) | ((u >> deleteOwner) & ownerWrite)) perm |= fs.FileMode(maskToPerm(u, isDir)) << 6
perm |= os.FileMode(((g >> genericGroup) & groupAll) | ((g >> deleteGroup) & groupWrite)) perm |= fs.FileMode(maskToPerm(g, isDir)) << 3
perm |= os.FileMode(((o >> genericWorld) & worldAll) | ((o >> deleteWorld) & worldWrite)) perm |= fs.FileMode(maskToPerm(o, isDir))
return perm
}
// maskToPerm converts a Windows access mask to the corresponding UNIX file
// mode permission bits.
func maskToPerm(mask windows.ACCESS_MASK, isDir bool) (perm byte) {
if isDir {
if mask&dirReadRights != 0 {
perm |= permRead
}
if mask&dirWriteRights != 0 {
perm |= permWrite
}
if mask&dirExecuteRights != 0 {
perm |= permExecute
}
} else {
if mask&fileReadRights != 0 {
perm |= permRead
}
if mask&fileWriteRights != 0 {
perm |= permWrite
}
if mask&fileExecuteRights != 0 {
perm |= permExecute
}
}
return perm return perm
} }

View file

@ -10,14 +10,6 @@ import (
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
// Common test constants for the Windows access masks.
const (
winAccessWrite = windows.GENERIC_WRITE | windows.DELETE
winAccessFull = windows.GENERIC_READ | windows.GENERIC_EXECUTE | winAccessWrite
winAccessDirRead = windows.GENERIC_READ | windows.FILE_LIST_DIRECTORY | windows.FILE_TRAVERSE
)
func TestPermToMasks(t *testing.T) { func TestPermToMasks(t *testing.T) {
t.Parallel() t.Parallel()
@ -31,14 +23,14 @@ func TestPermToMasks(t *testing.T) {
}{{ }{{
name: "all", name: "all",
perm: 0b111_111_111, perm: 0b111_111_111,
wantUser: winAccessFull, wantUser: fileReadRights | fileWriteRights | fileExecuteRights,
wantGroup: winAccessFull, wantGroup: fileReadRights | fileWriteRights | fileExecuteRights,
wantOther: winAccessFull, wantOther: fileReadRights | fileWriteRights | fileExecuteRights,
isDir: false, isDir: false,
}, { }, {
name: "user_write", name: "user_write",
perm: 0o010_000_000, perm: 0o010_000_000,
wantUser: winAccessWrite, wantUser: fileWriteRights,
wantGroup: 0, wantGroup: 0,
wantOther: 0, wantOther: 0,
isDir: false, isDir: false,
@ -46,20 +38,20 @@ func TestPermToMasks(t *testing.T) {
name: "group_read", name: "group_read",
perm: 0o000_010_000, perm: 0o000_010_000,
wantUser: 0, wantUser: 0,
wantGroup: windows.GENERIC_READ, wantGroup: fileReadRights,
wantOther: 0, wantOther: 0,
isDir: false, isDir: false,
}, { }, {
name: "all_dir", name: "all_dir",
perm: 0b111_111_111, perm: 0b111_111_111,
wantUser: winAccessFull | winAccessDirRead, wantUser: dirReadRights | dirWriteRights | dirExecuteRights,
wantGroup: winAccessFull | winAccessDirRead, wantGroup: dirReadRights | dirWriteRights | dirExecuteRights,
wantOther: winAccessFull | winAccessDirRead, wantOther: dirReadRights | dirWriteRights | dirExecuteRights,
isDir: true, isDir: true,
}, { }, {
name: "user_write_dir", name: "user_write_dir",
perm: 0o010_000_000, perm: 0o010_000_000,
wantUser: winAccessWrite, wantUser: dirWriteRights,
wantGroup: 0, wantGroup: 0,
wantOther: 0, wantOther: 0,
isDir: true, isDir: true,
@ -86,37 +78,58 @@ func TestMasksToPerm(t *testing.T) {
group windows.ACCESS_MASK group windows.ACCESS_MASK
other windows.ACCESS_MASK other windows.ACCESS_MASK
wantPerm fs.FileMode wantPerm fs.FileMode
isDir bool
}{{ }{{
name: "all", name: "all",
user: winAccessFull, user: fileReadRights | fileWriteRights | fileExecuteRights,
group: winAccessFull, group: fileReadRights | fileWriteRights | fileExecuteRights,
other: winAccessFull, other: fileReadRights | fileWriteRights | fileExecuteRights,
wantPerm: 0b111_111_111, wantPerm: 0b111_111_111,
isDir: false,
}, { }, {
name: "user_write", name: "user_write",
user: winAccessWrite, user: fileWriteRights,
group: 0, group: 0,
other: 0, other: 0,
wantPerm: 0o010_000_000, wantPerm: 0o010_000_000,
isDir: false,
}, { }, {
name: "group_read", name: "group_read",
user: 0, user: 0,
group: windows.GENERIC_READ, group: fileReadRights,
other: 0, other: 0,
wantPerm: 0o000_010_000, wantPerm: 0o000_010_000,
isDir: false,
}, { }, {
name: "no_access", name: "no_access",
user: 0, user: 0,
group: 0, group: 0,
other: 0, other: 0,
wantPerm: 0, wantPerm: 0,
isDir: false,
}, {
name: "all_dir",
user: dirReadRights | dirWriteRights | dirExecuteRights,
group: dirReadRights | dirWriteRights | dirExecuteRights,
other: dirReadRights | dirWriteRights | dirExecuteRights,
wantPerm: 0b111_111_111,
isDir: true,
}, {
name: "user_write_dir",
user: dirWriteRights,
group: 0,
other: 0,
wantPerm: 0o010_000_000,
isDir: true,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
assert.Equal(t, tc.wantPerm, masksToPerm(tc.user, tc.group, tc.other)) // Don't call [fs.FileMode.Perm] since the result is expected to
// contain only the permission bits.
assert.Equal(t, tc.wantPerm, masksToPerm(tc.user, tc.group, tc.other, tc.isDir))
}) })
} }
} }

View file

@ -687,7 +687,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
} }
if permcheck.NeedsMigration(confPath) { if permcheck.NeedsMigration(confPath) {
permcheck.Migrate(Context.workDir, dataDir, statsDir, querylogDir, confPath) permcheck.Migrate(Context.workDir, dataDir, statsDir, querylogDir, confPath, execPath)
} }
permcheck.Check(Context.workDir, dataDir, statsDir, querylogDir, confPath) permcheck.Check(Context.workDir, dataDir, statsDir, querylogDir, confPath)

View file

@ -32,11 +32,14 @@ func NeedsMigration(confFilePath string) (ok bool) {
// Migrate attempts to change the permissions of AdGuard Home's files. It logs // Migrate attempts to change the permissions of AdGuard Home's files. It logs
// the results at an appropriate level. // the results at an appropriate level.
func Migrate(workDir, dataDir, statsDir, querylogDir, confFilePath string) { func Migrate(workDir, dataDir, statsDir, querylogDir, confFilePath, execPath string) {
chmodDir(workDir) chmodDir(workDir)
chmodFile(confFilePath) chmodFile(confFilePath)
// Allow the binary to be executed.
chmodPath(execPath, typeFile, aghos.DefaultPermFile|0b001_000_000)
// TODO(a.garipov): Put all paths in one place and remove this duplication. // TODO(a.garipov): Put all paths in one place and remove this duplication.
chmodDir(dataDir) chmodDir(dataDir)
chmodDir(filepath.Join(dataDir, "filters")) chmodDir(filepath.Join(dataDir, "filters"))

View file

@ -60,7 +60,7 @@ func checkFile(filePath string) {
// checkPath checks the permissions of a single filesystem entity. The results // checkPath checks the permissions of a single filesystem entity. The results
// are logged at the appropriate level. // are logged at the appropriate level.
func checkPath(entPath, fileType string, want fs.FileMode) { func checkPath(entPath, fileType string, want fs.FileMode) {
s, err := os.Stat(entPath) s, err := aghos.Stat(entPath)
if err != nil { if err != nil {
logFunc := log.Error logFunc := log.Error
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {

View file

@ -360,9 +360,9 @@ func tarGzFileUnpackOne(outDir string, tr *tar.Reader, hdr *tar.Header) (name st
if name == "AdGuardHome" { if name == "AdGuardHome" {
// Top-level AdGuardHome/. Skip it. // Top-level AdGuardHome/. Skip it.
// //
// TODO(a.garipov): This whole package needs to be // TODO(a.garipov): This whole package needs to be rewritten and
// rewritten and covered in more integration tests. It // covered in more integration tests. It has weird assumptions and
// has weird assumptions and file mode issues. // file mode issues.
return "", nil return "", nil
} }
@ -383,7 +383,7 @@ func tarGzFileUnpackOne(outDir string, tr *tar.Reader, hdr *tar.Header) (name st
} }
var wc io.WriteCloser var wc io.WriteCloser
wc, err = os.OpenFile( wc, err = aghos.OpenFile(
outputName, outputName,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
os.FileMode(hdr.Mode&0o755), os.FileMode(hdr.Mode&0o755),
@ -464,8 +464,7 @@ func zipFileUnpackOne(outDir string, zf *zip.File) (name string, err error) {
if name == "AdGuardHome" { if name == "AdGuardHome" {
// Top-level AdGuardHome/. Skip it. // Top-level AdGuardHome/. Skip it.
// //
// TODO(a.garipov): See the similar todo in // TODO(a.garipov): See the similar todo in tarGzFileUnpack.
// tarGzFileUnpack.
return "", nil return "", nil
} }
@ -480,7 +479,7 @@ func zipFileUnpackOne(outDir string, zf *zip.File) (name string, err error) {
} }
var wc io.WriteCloser var wc io.WriteCloser
wc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) wc, err = aghos.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
if err != nil { if err != nil {
return "", fmt.Errorf("os.OpenFile(): %w", err) return "", fmt.Errorf("os.OpenFile(): %w", err)
} }