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

FR-6365 - Prevent daemons from running when installing packages #183

Merged
merged 15 commits into from
Feb 23, 2024
Merged
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
3 changes: 3 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ ubuntu-image (3.2) UNRELEASED; urgency=medium
* Support deb822 format in sources list
* Unmount more reliably after executing commands in the chroot
* Support deb822 format for PPAs
* Detect new mount points before umounting everything when leaving a chroot
* Prevent services/daemons from starting using policy-rc.d, start-stop-daemon
and initctl

-- Masahiro nakagawa <[email protected]> Thu, 8 Feb 2024 13:56:00 +0900

Expand Down
93 changes: 75 additions & 18 deletions internal/helper/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
)

// define some functions that can be mocked by test cases
var osRename = os.Rename
var osRemove = os.Remove
var (
osRename = os.Rename
osRemove = os.Remove
osWriteFile = os.WriteFile
osutilFileExists = osutil.FileExists
)

func BoolPtr(b bool) *bool {
return &b
Expand Down Expand Up @@ -308,6 +312,19 @@
return nil
}

// RunCmds runs a list of commands and returns the error
// It stops at the first error
func RunCmds(cmds []*exec.Cmd, debug bool) error {
upils marked this conversation as resolved.
Show resolved Hide resolved
for _, cmd := range cmds {
err := RunCmd(cmd, debug)
if err != nil {
return err
}

Check warning on line 322 in internal/helper/helper.go

View check run for this annotation

Codecov / codecov/patch

internal/helper/helper.go#L317-L322

Added lines #L317 - L322 were not covered by tests
}

return nil

Check warning on line 325 in internal/helper/helper.go

View check run for this annotation

Codecov / codecov/patch

internal/helper/helper.go#L325

Added line #L325 was not covered by tests
}

// SafeQuantitySubtraction subtracts quantities while checking for integer underflow
func SafeQuantitySubtraction(orig, subtract quantity.Size) quantity.Size {
if subtract > orig {
Expand Down Expand Up @@ -467,23 +484,63 @@
// RestoreResolvConf restores the resolv.conf in the chroot from the
// version that was backed up by BackupAndCopyResolvConf
func RestoreResolvConf(chroot string) error {
if osutil.FileExists(filepath.Join(chroot, "etc", "resolv.conf.tmp")) {
if osutil.IsSymlink(filepath.Join(chroot, "etc", "resolv.conf")) {
// As per what live-build does, handle the case where some package
// in the install_packages phase converts resolv.conf into a
// symlink. In such case we don't restore our backup but instead
// remove it, leaving the symlink around.
backup := filepath.Join(chroot, "etc", "resolv.conf.tmp")
if err := osRemove(backup); err != nil {
return fmt.Errorf("Error removing file \"%s\": %s", backup, err.Error())
}
} else {
src := filepath.Join(chroot, "etc", "resolv.conf.tmp")
dest := filepath.Join(chroot, "etc", "resolv.conf")
if err := osRename(src, dest); err != nil {
return fmt.Errorf("Error moving file \"%s\" to \"%s\": %s", src, dest, err.Error())
}
if !osutil.FileExists(filepath.Join(chroot, "etc", "resolv.conf.tmp")) {
return nil
}

Check warning on line 489 in internal/helper/helper.go

View check run for this annotation

Codecov / codecov/patch

internal/helper/helper.go#L488-L489

Added lines #L488 - L489 were not covered by tests
if osutil.IsSymlink(filepath.Join(chroot, "etc", "resolv.conf")) {
// As per what live-build does, handle the case where some package
// in the install_packages phase converts resolv.conf into a
// symlink. In such case we don't restore our backup but instead
// remove it, leaving the symlink around.
backup := filepath.Join(chroot, "etc", "resolv.conf.tmp")
if err := osRemove(backup); err != nil {
return fmt.Errorf("Error removing file \"%s\": %s", backup, err.Error())
}
} else {
src := filepath.Join(chroot, "etc", "resolv.conf.tmp")
dest := filepath.Join(chroot, "etc", "resolv.conf")
if err := osRename(src, dest); err != nil {
return fmt.Errorf("Error moving file \"%s\" to \"%s\": %s", src, dest, err.Error())
}
}
return nil
}

const backupExt = ".REAL"

// BackupReplace backup the target file and replace it with the given content
// Returns the restore function.
func BackupReplace(target string, content string) (func(error) error, error) {
backup := target + backupExt
if osutilFileExists(backup) {
// already backed up so do nothing
return nil, nil
}

if err := osRename(target, backup); err != nil {
return nil, fmt.Errorf("Error moving file \"%s\" to \"%s\": %s", target, backup, err.Error())
}

if err := osWriteFile(target, []byte(content), 0755); err != nil {
return nil, fmt.Errorf("Error writing to %s : %s", target, err.Error())
}

return genRestoreFile(target), nil
}

// genRestoreFile returns the function to be called to restore the backuped file
func genRestoreFile(target string) func(err error) error {
return func(err error) error {
src := target + backupExt
if !osutilFileExists(src) {
return err
}

if tmpErr := osRename(src, target); tmpErr != nil {
tmpErr = fmt.Errorf("Error moving file \"%s\" to \"%s\": %s", src, target, tmpErr.Error())
return fmt.Errorf("%s\n%s", err, tmpErr)
}

return err

Check warning on line 544 in internal/helper/helper.go

View check run for this annotation

Codecov / codecov/patch

internal/helper/helper.go#L544

Added line #L544 was not covered by tests
}
}
111 changes: 111 additions & 0 deletions internal/helper/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ func mockRemove(string) error {
func mockRename(string, string) error {
return fmt.Errorf("Test Error")
}
func mockWriteFile(name string, data []byte, perm os.FileMode) error {
return fmt.Errorf("WriteFile Error")
}

// TestRestoreResolvConf tests if resolv.conf is restored correctly
func TestRestoreResolvConf(t *testing.T) {
Expand Down Expand Up @@ -335,3 +338,111 @@ func TestCheckEmptyFields(t *testing.T) {
})
}
}

func prepareMainFileToBackup(workDir string) (string, error) {
err := os.MkdirAll(filepath.Join(workDir, "sbin"), 0755)
if err != nil {
return "", err
}
mainTargetPath := filepath.Join(workDir, "sbin", "target")
mainConf, err := os.Create(mainTargetPath)
if err != nil {
return "", err
}
mainContent := []byte("Main")
_, err = mainConf.Write(mainContent)
if err != nil {
return "", err
}
mainConf.Close()

return mainTargetPath, nil
}

func prepareBackupFile(workDir string, content string, mainTargetPath string) (string, error) {
backupPath := mainTargetPath + backupExt
mainConf, err := os.Create(backupPath)
if err != nil {
return "", err
}
_, err = mainConf.Write([]byte(content))
if err != nil {
return "", err
}
mainConf.Close()

return backupPath, nil
}

func TestBackupReplace(t *testing.T) {
asserter := Asserter{T: t}
// Prepare temporary directory
workDir := filepath.Join("/tmp", "ubuntu-image-"+uuid.NewString())
err := os.Mkdir(workDir, 0755)
asserter.AssertErrNil(err, true)
t.Cleanup(func() { os.RemoveAll(workDir) })

// Create test environment
mainTargetPath, err := prepareMainFileToBackup(workDir)
asserter.AssertErrNil(err, true)
backupContent := "Backup"

// Test backup file exists
backupPath, err := prepareBackupFile(workDir, backupContent, mainTargetPath)
asserter.AssertErrNil(err, true)
restoreFunc, err := BackupReplace(mainTargetPath, backupContent)
asserter.AssertErrNil(err, true)
asserter.AssertEqual(nil, restoreFunc)
err = os.Remove(backupPath)
asserter.AssertErrNil(err, true)

// Mock the os.Rename failure
osRename = mockRename
t.Cleanup(func() {
osRename = os.Rename
})
restoreFunc, err = BackupReplace(mainTargetPath, backupContent)
asserter.AssertErrContains(err, "Error moving file")
asserter.AssertEqual(nil, restoreFunc)
osRename = os.Rename

// Mock the os.WriteFile failure
osWriteFile = mockWriteFile
t.Cleanup(func() {
osWriteFile = os.WriteFile
})
restoreFunc, err = BackupReplace(mainTargetPath, backupContent)
asserter.AssertErrContains(err, "Error writing to")
asserter.AssertEqual(nil, restoreFunc)
osWriteFile = os.WriteFile
err = os.Remove(backupPath)
asserter.AssertErrNil(err, true)

// Test genRestoreFile
mainTargetPath, err = prepareMainFileToBackup(workDir)
asserter.AssertErrNil(err, true)

restoreFunc, err = BackupReplace(mainTargetPath, backupContent)
asserter.AssertErrNil(err, true)

// Test no backup file anymore
err = os.Remove(backupPath)
asserter.AssertErrNil(err, true)

err = restoreFunc(nil)
asserter.AssertErrNil(err, true)

// Mock the os.Rename failure
_, err = prepareBackupFile(workDir, backupContent, mainTargetPath)
asserter.AssertErrNil(err, true)

osRename = mockRename
t.Cleanup(func() {
osRename = os.Rename
})
err = restoreFunc(nil)
asserter.AssertErrContains(err, "Error moving file")

osRename = os.Rename

}
Loading
Loading