Compare commits

...

1 Commits

Author SHA1 Message Date
f97d36d6ed Add priviledge dropping and mount utils 2025-10-12 14:00:06 -07:00
3 changed files with 231 additions and 3 deletions

4
go.mod
View File

@ -1,3 +1,7 @@
module gitea.narnian.us/lordwelch/Podman
go 1.25.2
require kernel.org/pub/linux/libs/security/libcap/cap v1.2.76
require kernel.org/pub/linux/libs/security/libcap/psx v1.2.76 // indirect

88
main.go
View File

@ -2,7 +2,6 @@ package Podman
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/signal"
@ -35,7 +34,7 @@ func Run(args ...string) error {
var numericRe = regexp.MustCompile(`^[0-9]+$`)
func Signal(name string, sig os.Signal) error {
fis, err := ioutil.ReadDir("/proc")
fis, err := os.ReadDir("/proc")
if err != nil {
return err
}
@ -46,7 +45,7 @@ func Signal(name string, sig os.Signal) error {
if !numericRe.MatchString(fi.Name()) {
continue
}
b, err := ioutil.ReadFile(filepath.Join("/proc", fi.Name(), "cmdline"))
b, err := os.ReadFile(filepath.Join("/proc", fi.Name(), "cmdline"))
if err != nil {
if os.IsNotExist(err) {
continue // process vanished
@ -62,3 +61,86 @@ func Signal(name string, sig os.Signal) error {
}
return fmt.Errorf("Process not found: %s", name)
}
func IsMounted(mountpoint string) (bool, error) {
b, err := os.ReadFile("/proc/self/mountinfo")
if err != nil {
if os.IsNotExist(err) {
return false, nil // platform does not have /proc/self/mountinfo, fall back to not verifying
}
return false, err
}
for _, line := range strings.Split(strings.TrimSpace(string(b)), "\n") {
parts := strings.Fields(line)
if len(parts) < 5 {
continue
}
if filepath.Clean(parts[4]) == filepath.Clean(mountpoint) {
return true, nil
}
}
return false, nil
}
func MakeWritable(dir string) error {
mounted, err := IsMounted(dir)
if err != nil {
return err
}
if mounted {
// Nothing to do, directory is already mounted.
return nil
}
// Read all regular files in this directory.
regularFiles := make(map[string]string)
fis, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, fi := range fis {
b, err := os.ReadFile(filepath.Join(dir, fi.Name()))
if err != nil {
return err
}
regularFiles[fi.Name()] = string(b)
}
if err := MountTmpfs(dir); err != nil {
return err
}
// Write all regular files from memory back to new tmpfs.
for name, contents := range regularFiles {
if err := os.WriteFile(filepath.Join(dir, name), []byte(contents), 0644); err != nil {
return err
}
}
return nil
}
func MountTmpfs(dest string) error {
if mounted, err := IsMounted(dest); err == nil && mounted {
return nil
}
if err := syscall.Mount("tmpfs", dest, "tmpfs", syscall.MS_NOSUID|syscall.MS_RELATIME, "size=5G"); err != nil {
return fmt.Errorf("mounting tmpfs to %s: %v", dest, err)
}
return nil
}
func Bind(source, dest string) error {
if mounted, err := IsMounted(dest); err == nil && mounted {
return nil
}
os.MkdirAll(source, 0o777)
os.MkdirAll(dest, 0o777)
if err := syscall.Mount(source, dest, "", syscall.MS_BIND, ""); err != nil {
return fmt.Errorf("error mounting %s to %s: %v", source, dest, err)
}
return nil
}

142
privDrop.go Normal file
View File

@ -0,0 +1,142 @@
package Podman
import (
"log"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"kernel.org/pub/linux/libs/security/libcap/cap"
)
func setFromCaps(caps ...string) (*cap.Set, error) {
var err error
newSet := cap.NewSet()
valueList := make([]cap.Value, 0, len(caps))
for _, capName := range caps {
value, err := cap.FromName(strings.ToLower(capName))
if err != nil {
return nil, err
}
valueList = append(valueList, value)
}
err = newSet.SetFlag(cap.Effective, true, valueList...)
if err != nil {
return nil, err
}
err = newSet.Fill(cap.Permitted, cap.Effective)
if err != nil {
return nil, err
}
return newSet, err
}
func ChownRecursively(root string, owner int, group int) {
err := filepath.Walk(root,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if int(info.Sys().(*syscall.Stat_t).Uid) != owner || int(info.Sys().(*syscall.Stat_t).Gid) != group {
err = os.Chown(path, owner, group)
if err == nil {
log.Printf("File ownership of %s changed.\n", path)
}
}
return nil
})
if err != nil {
log.Printf("Chown failed: %v", err)
}
}
type Passwd struct {
User string
Password string
UID int
GID int
Gecos string
Home string
Shell string
Gids []string
}
// MustDropPrivileges executes the program in a child process, dropping root
// privileges, but retaining the given capabilities.
func MustDropPrivileges(caps ...string) {
// Read and display the capabilities of the running process
passwd := LoadPasswd(os.Getenv("HOME"))
err := os.Chown(passwd.Home, passwd.UID, passwd.GID)
if err != nil {
log.Print("Failed to chown home directory", os.Getenv("HOME"))
}
c := cap.GetProc()
if os.Getenv("PRIVILEGES_DROPPED") == "1" {
return
}
capSet, err := setFromCaps(caps...)
if err != nil {
log.Fatalf("Unable to parse capabilities: %s", err)
}
diff, _ := capSet.Cf(c)
if passwd.GID == os.Getgid() && passwd.UID == os.Getuid() && diff == 0 {
return
}
os.Setenv("PRIVILEGES_DROPPED", "1")
if passwd.GID != 0 {
err = cap.SetGroups(passwd.GID)
if err != nil {
log.Fatalf("unable to change gid to %d: %s", passwd.GID, err)
}
}
if passwd.GID != 0 {
err = cap.SetUID(passwd.UID)
if err != nil {
log.Fatalf("unable to change uid to %d: %s", passwd.UID, err)
}
}
err = capSet.SetProc()
if err != nil {
log.Fatalf("Unable to drop privileges: %s", err)
}
}
func LoadPasswd(home string) Passwd {
var (
passwd Passwd
)
passwd.User = filepath.Base(os.Getenv("HOME"))
b, err := os.ReadFile("/etc/passwd")
if err != nil {
return passwd
}
for _, line := range strings.SplitN(string(b), "\n", -1) {
fields := strings.SplitN(line, ":", 7)
if len(fields) != 7 {
continue
}
if passwd.User != string(fields[0]) {
continue
}
passwd.User = fields[0]
passwd.Password = fields[1]
passwd.UID, err = strconv.Atoi(fields[2])
if err != nil {
continue
}
passwd.GID, err = strconv.Atoi(fields[3])
if err != nil {
continue
}
passwd.Gecos = fields[4]
passwd.Home = fields[5]
passwd.Shell = fields[6]
}
return passwd
}