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
)
// 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 {
// Needed so that calls to to podman which in turn call new{u,g}idmap will succeed
os.Unsetenv("GOKRAZY_FIRST_START")
err := os.Mkdir("/var/run/user", 0o755)
if err != nil && !errors.Is(err, os.ErrExist) {
@@ -174,7 +178,7 @@ func IsMounted(mountpoint string) (bool, error) {
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)
if len(parts) < 5 {
continue

View File

@@ -1,6 +1,7 @@
package main
import (
"log"
"os"
"gitea.narnian.us/lordwelch/Podman"
@@ -8,7 +9,14 @@ import (
func main() {
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)
}
Podman.NewIDMap("gid_map")

View File

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

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
import (
"log"
"os"
"gitea.narnian.us/lordwelch/Podman"
@@ -8,7 +9,14 @@ import (
func main() {
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)
}
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
import (
"fmt"
"log"
"os"
"path/filepath"
@@ -70,8 +71,9 @@ type Passwd struct {
func MustDropPrivileges(passwd Passwd, caps ...string) {
err := os.Chown(passwd.Home, passwd.UID, passwd.GID)
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)
c := cap.GetProc()
if os.Getenv("PRIVILEGES_DROPPED") == "1" {
@@ -141,11 +143,18 @@ func LoadPasswd() 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
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 {
return passwds
return passwds, fmt.Errorf("Unable to read /etc/passwd: %w", err)
}
for line := range strings.SplitSeq(string(b), "\n") {
fields := strings.SplitN(line, ":", 7)
@@ -168,5 +177,5 @@ func LoadAllPasswd() []Passwd {
passwd.Shell = fields[6]
passwds = append(passwds, passwd)
}
return passwds
return passwds, nil
}