Compare commits

..

2 Commits

Author SHA1 Message Date
Timmy Welch
fad0eb755a Add a podmanSocket command with hardcoded uid 199 2026-01-04 16:51:33 -08:00
Timmy Welch
587e906d6d Remove as many syscalls from new{g,u}idmap as possible
Add a test to validate subid generation
2026-01-04 16:23:01 -08:00
7 changed files with 147 additions and 37 deletions

View File

@@ -24,7 +24,11 @@ const (
G = 1024 * 1024 * 1024 G = 1024 * 1024 * 1024
) )
// Setup ensures that everything needed for running podman under the given uid is setup
// new{u,g}idmap is handled separately
// Note: This should be called while still root (before MustDropPrivileges) and must be called after any use of the GOKRAZY_FIRST_START environment variable
func Setup(uid int) error { func Setup(uid int) error {
// Needed so that calls to to podman which in turn call new{u,g}idmap will succeed
os.Unsetenv("GOKRAZY_FIRST_START") os.Unsetenv("GOKRAZY_FIRST_START")
err := os.Mkdir("/var/run/user", 0o755) err := os.Mkdir("/var/run/user", 0o755)
if err != nil && !errors.Is(err, os.ErrExist) { if err != nil && !errors.Is(err, os.ErrExist) {
@@ -174,7 +178,7 @@ func IsMounted(mountpoint string) (bool, error) {
return false, err return false, err
} }
for _, line := range strings.Split(strings.TrimSpace(string(b)), "\n") { for line := range strings.SplitSeq(strings.TrimSpace(string(b)), "\n") {
parts := strings.Fields(line) parts := strings.Fields(line)
if len(parts) < 5 { if len(parts) < 5 {
continue continue

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"log"
"os" "os"
"gitea.narnian.us/lordwelch/Podman" "gitea.narnian.us/lordwelch/Podman"
@@ -8,7 +9,14 @@ import (
func main() { func main() {
if os.Getenv("GOKRAZY_FIRST_START") == "1" { if os.Getenv("GOKRAZY_FIRST_START") == "1" {
Podman.WriteSubids("/etc/subgid") subidContent, err := Podman.GetSubids("/etc/subgid")
if err != nil {
log.Printf("Unable to generate /etc/subgid successfully, podman will probably not work: %s", err)
}
err = os.WriteFile("/etc/subgid", subidContent, 0o644)
if err != nil {
log.Printf("Unable to write to /etc/subgid, podman will probably not work: %s", err)
}
os.Exit(125) os.Exit(125)
} }
Podman.NewIDMap("gid_map") Podman.NewIDMap("gid_map")

View File

@@ -7,6 +7,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -56,34 +57,39 @@ func NewIDMap(idMapName string) {
log.Fatal("You aren't allowed to do that :-P") log.Fatal("You aren't allowed to do that :-P")
} }
var ( var (
user Passwd // This will get overwritten by /etc/passwd if we read subids from files
subid moby_user.SubID user = Passwd{
passwds []Passwd = LoadAllPasswd() User: os.Getenv("USER"),
UID: int(fstat.Uid),
GID: int(fstat.Gid),
}
subid = generateSubid(int64(fstat.Uid))
) )
subidstart := subidStart subid.Name = user.User
for _, u := range passwds {
if u.UID == int(fstat.Uid) { if subidUseFile {
user = u passwds, err := LoadAllPasswd()
subid = moby_user.SubID{ if err != nil {
Name: user.User, // Continue because we don't actually want to fail if there is no /etc/passwd
SubID: subidstart, // This command assumes a static mapping of subids based on the current uid. Which is already verified above
Count: subidCount, log.Println(err)
}
for _, u := range passwds {
if u.UID == int(fstat.Uid) {
user = u
} }
} }
subidstart += subidCount if user.User == "" {
} log.Fatalf("Unable to find user")
if user.User == "" { }
log.Fatalf("Unable to find user")
}
if subidUseFile {
if idMapName == "uid_map" { if idMapName == "uid_map" {
subid, err = getSubIDs("/etc/subuid", user.User, strconv.Itoa(user.UID)) subid, err = getSubIDs("/etc/subuid", user.User, strconv.Itoa(user.UID))
} else { } else {
subid, err = getSubIDs("/etc/subgid", user.User, strconv.Itoa(user.GID)) subid, err = getSubIDs("/etc/subgid", user.User, strconv.Itoa(user.GID))
} }
} if err != nil {
if err != nil { log.Fatalf("Unable to get subid map for the user: %v", err)
log.Fatalf("Unable to get subid map for the user: %v", err) }
} }
idmaps, err := getIDMaps(int64(user.UID), subid, args) idmaps, err := getIDMaps(int64(user.UID), subid, args)
if err != nil { if err != nil {
@@ -118,19 +124,28 @@ func NewIDMap(idMapName string) {
} }
} }
func WriteSubids(file string) error { func GetSubids(passwd string) ([]byte, error) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
passwds := LoadAllPasswd() passwds, err := LoadAllPasswd(passwd)
subidstart := subidStart if err != nil { // Only errors are io errors reading /etc/passwd
return nil, err
}
sort.Slice(passwds, func(i, j int) bool { return passwds[i].UID < passwds[j].UID })
for _, passwd := range passwds { for _, passwd := range passwds {
fmt.Fprintf(buf, "%s:%d:%d\n", passwd.User, subidstart, subidCount) subid := generateSubid(int64(passwd.UID))
subidstart += subidCount _, err := fmt.Fprintf(buf, "%s:%d:%d\n", passwd.User, subid.SubID, subid.Count)
if err != nil {
return nil, err
}
} }
err := os.WriteFile(file, buf.Bytes(), 0o644) return buf.Bytes(), nil
if err != nil { }
return err
func generateSubid(id int64) moby_user.SubID {
return moby_user.SubID{
SubID: subidStart + (subidCount * id),
Count: subidCount,
} }
return nil
} }
func getIDMaps(id int64, subid moby_user.SubID, args []string) ([]moby_user.IDMap, error) { func getIDMaps(id int64, subid moby_user.SubID, args []string) ([]moby_user.IDMap, error) {

38
newidmap_test.go Normal file
View File

@@ -0,0 +1,38 @@
package Podman_test
import (
"os"
"testing"
"gitea.narnian.us/lordwelch/Podman"
)
// passwdContent is specifically not in order
const passwdContent = `root:x:0:0:root:/perm/home:/perm/bin/bash
vw:x:2:2:vw:/perm/home/vw:/bin/nologin
jellyfin:x:1:1:jellyfin:/perm/home/jellyfin:/bin/nologin
nzbget:x:3:3:nzbget:/perm/home/nzbget:/bin/nologin
`
// expected is always in uid order
const expected = `root:200:65536
jellyfin:65736:65536
vw:131272:65536
nzbget:196808:65536
`
func TestGetSubids(t *testing.T) {
tmpDir := t.TempDir()
passwd := tmpDir + "/passwd"
err := os.WriteFile(passwd, []byte(passwdContent), 0o644)
if err != nil {
t.Fatalf("Unable to write %s: %s", passwd, err)
}
ids, err := Podman.GetSubids(passwd)
if err != nil {
t.Fatal("Unable to generate subids", err)
}
if got, want := string(ids), expected; got != want {
t.Errorf("GetSubids: unexpected result: got %q, want %q", got, want)
}
}

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"log"
"os" "os"
"gitea.narnian.us/lordwelch/Podman" "gitea.narnian.us/lordwelch/Podman"
@@ -8,7 +9,14 @@ import (
func main() { func main() {
if os.Getenv("GOKRAZY_FIRST_START") == "1" { if os.Getenv("GOKRAZY_FIRST_START") == "1" {
Podman.WriteSubids("/etc/subuid") subidContent, err := Podman.GetSubids("/etc/subuid")
if err != nil {
log.Printf("Unable to generate /etc/subuid successfully, podman will probably not work: %s", err)
}
err = os.WriteFile("/etc/subuid", subidContent, 0o644)
if err != nil {
log.Printf("Unable to write to /etc/subuid, podman will probably not work: %s", err)
}
os.Exit(125) os.Exit(125)
} }
Podman.NewIDMap("uid_map") Podman.NewIDMap("uid_map")

28
podmanSocket/main.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import (
"log"
"os"
"syscall"
"gitea.narnian.us/lordwelch/Podman"
)
func main() {
Podman.Setup(199) // subuids start at 200
Podman.MustDropPrivileges(Podman.Passwd{
User: "podmanSocket",
UID: 199,
GID: 199,
Home: "/perm/home/podmanSocket",
Shell: "/bin/sh",
})
args := []string{
"/user/podman", "system", "--log-level=debug", "service", "--time=0", "unix:///run/podman.sock",
}
err := syscall.Exec("/user/podman", args, os.Environ())
if err != nil {
log.Fatalf("failed to start podman: %v", err)
}
}

View File

@@ -1,6 +1,7 @@
package Podman package Podman
import ( import (
"fmt"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -70,8 +71,9 @@ type Passwd struct {
func MustDropPrivileges(passwd Passwd, caps ...string) { func MustDropPrivileges(passwd Passwd, caps ...string) {
err := os.Chown(passwd.Home, passwd.UID, passwd.GID) err := os.Chown(passwd.Home, passwd.UID, passwd.GID)
if err != nil { if err != nil {
log.Print("Failed to chown home directory", os.Getenv("HOME")) log.Printf("Failed to chown home directory %s", passwd.Home)
} }
os.Setenv("HOME", passwd.Home)
os.Setenv("USER", passwd.User) os.Setenv("USER", passwd.User)
c := cap.GetProc() c := cap.GetProc()
if os.Getenv("PRIVILEGES_DROPPED") == "1" { if os.Getenv("PRIVILEGES_DROPPED") == "1" {
@@ -141,11 +143,18 @@ func LoadPasswd() Passwd {
} }
return passwd return passwd
} }
func LoadAllPasswd() []Passwd {
// LoadAllPasswd reads /etc/passwd by default.
// If a string is passed the first argument is used as the path instead of /etc/passwd
func LoadAllPasswd(passwd ...string) ([]Passwd, error) {
var passwds []Passwd var passwds []Passwd
b, err := os.ReadFile("/etc/passwd") var passwdFile = "/etc/passwd"
if len(passwd) > 0 {
passwdFile = passwd[0]
}
b, err := os.ReadFile(passwdFile)
if err != nil { if err != nil {
return passwds return passwds, fmt.Errorf("Unable to read /etc/passwd: %w", err)
} }
for line := range strings.SplitSeq(string(b), "\n") { for line := range strings.SplitSeq(string(b), "\n") {
fields := strings.SplitN(line, ":", 7) fields := strings.SplitN(line, ":", 7)
@@ -168,5 +177,5 @@ func LoadAllPasswd() []Passwd {
passwd.Shell = fields[6] passwd.Shell = fields[6]
passwds = append(passwds, passwd) passwds = append(passwds, passwd)
} }
return passwds return passwds, nil
} }