Initial Commit

This commit is contained in:
Elara 2023-10-23 14:18:20 -07:00
commit ac6e063639
7 changed files with 242 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/gofakeroot

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Elara Musayelyan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

17
README.md Normal file
View File

@ -0,0 +1,17 @@
# fakeroot
A pure-Go implementation of fakeroot using Linux user namespaces.
### What is fakeroot?
Fakeroot is a utility that runs commands in an environment where they appear to have root privileges even though they don't. The [original `fakeroot` command](https://salsa.debian.org/clint/fakeroot/) does this by using `LD_PRELOAD` to inject custom wrappers around libc functions that behave as if they're running as the root user. Basically, it intercepts calls to functions like `stat()`, `chmod()`, `chown()`, etc. and replaces them with ones that return values that make it seem like the user is root.
### How is this library different?
Instead of injecting custom libc functions, this library uses Linux's user namespaces. Basically, rather than pretending that the user is root, this library uses the Linux kernel's built-in isolation features to make it seem as if the user is actually root. That means even programs that don't use libc (such as Go programs), or programs with a statically-linked libc, will believe they're running as root. However, this approach will only work on Linux kernels new enough (3.8+) and on distros that don't disable this functionality. Most modern Linux systems support it though, so it should work in most cases.
### Why?
Fakeroot is very useful for building packages, as various utilities depend on file permissions and users. For example, the `tar` command that creates tar archives. It creates files inside the tar archive with the same permissions as the original files. That means if the files were owned by a particular user, they will still be owned by that user when the tar archive is extracted. This is problematic for package building because it means you can end up with system files in a package, owned by non-root users. Fakeroot is used to trick utilities like `tar` into making files owned as root.
Many utilities require root privileges for some operations but return errors even if the specific thing you're doing doesn't require them. Fakeroot can also be used to execute these programs without actually giving them root privileges, which provides extra security.

View File

@ -0,0 +1,60 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"lure.sh/fakeroot"
"lure.sh/fakeroot/loginshell"
)
func main() {
var showHelp bool
flag.BoolVar(&showHelp, "help", false, "Show help screen")
flag.BoolVar(&showHelp, "h", false, "Show help screen")
flag.Parse()
if showHelp {
printHelp()
return
}
var (
cmd string
args []string
err error
)
if flag.NArg() > 1 {
cmd = flag.Arg(0)
args = flag.Args()[1:]
} else {
cmd, err = loginshell.Get(-1)
if err != nil {
log.Fatalln(err)
}
}
c, err := fakeroot.Command(cmd, args...)
if err != nil {
log.Fatalln(err)
}
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
err = c.Run()
if err != nil {
log.Fatalln(err)
}
}
func printHelp() {
fmt.Print("Fakeroot implementation written in Go.\n\n")
fmt.Print("Usage: fakeroot [cmd] [args...]\n\n")
fmt.Print("Arguments:\n")
fmt.Print(" [cmd] Command to execute in fakeroot environment. If not specified, the user's login shell will be executed.\n")
fmt.Print(" [args...] Arguments to pass to the executed command.\n")
}

71
fakeroot.go Normal file
View File

@ -0,0 +1,71 @@
package fakeroot
import (
"errors"
"os"
"os/exec"
"slices"
"syscall"
)
var (
// ErrRootUIDAlreadyMapped is returned when there's already a mapping for the root user in a command
ErrRootUIDAlreadyMapped = errors.New("fakeroot: root user has already been mapped in this command")
// ErrRootGIDAlreadyMapped is returned when there's already a mapping for the root group in a command
ErrRootGIDAlreadyMapped = errors.New("fakeroot: root group has already been mapped in this command")
)
// Command returns a command that runs in a fakeroot environment
func Command(name string, arg ...string) (*exec.Cmd, error) {
cmd := exec.Command(name, arg...)
return cmd, Apply(cmd)
}
// Apply applies the options required to run in a fakeroot environment to
// a command. It returns an error if the root group or user already has a mapping
// registered in the command.
func Apply(cmd *exec.Cmd) error {
uid := os.Getuid()
// If the user is already root, there's no need for fakeroot
if uid == 0 {
return nil
}
// Ensure SysProcAttr isn't nil
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
// Create a new user namespace
cmd.SysProcAttr.Cloneflags |= syscall.CLONE_NEWUSER
// If the command already contains a mapping for the root user, return an error
if slices.ContainsFunc(cmd.SysProcAttr.UidMappings, rootMap) {
return ErrRootUIDAlreadyMapped
}
// If the command already contains a mapping for the root group, return an error
if slices.ContainsFunc(cmd.SysProcAttr.GidMappings, rootMap) {
return ErrRootGIDAlreadyMapped
}
cmd.SysProcAttr.UidMappings = append(cmd.SysProcAttr.UidMappings, syscall.SysProcIDMap{
ContainerID: 0,
HostID: uid,
Size: 1,
})
cmd.SysProcAttr.GidMappings = append(cmd.SysProcAttr.GidMappings, syscall.SysProcIDMap{
ContainerID: 0,
HostID: uid,
Size: 1,
})
return nil
}
func rootMap(m syscall.SysProcIDMap) bool {
return m.ContainerID == 0
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module lure.sh/fakeroot
go 1.21

69
loginshell/loginshell.go Normal file
View File

@ -0,0 +1,69 @@
package loginshell
import (
"bufio"
"errors"
"io/fs"
"os"
"strconv"
"strings"
)
var (
// ErrInvalidPasswd is returned when the passwd file is invalid and can't be parsed
ErrInvalidPasswd = errors.New("loginshell: invalid passwd file")
// ErrNoSuchUser is returned when a given user isn't in the passwd file
ErrNoSuchUser = errors.New("loginshell: provided uid not found in passwd file")
// ErrUnsupported is returned on unsupported platforms, such as Windows
ErrUnsupported = errors.New("loginshell: unsupported platform")
)
// Get returns the login shell belonging to the provided uid by parsing the passwd file.
// If uid is less than zero, the current uid will be used instead.
func Get(uid int) (string, error) {
if uid < 0 {
uid = os.Getuid()
}
// os.Getuid returns -1 on unsupported platforms
if uid == -1 {
return "", ErrUnsupported
}
fl, err := os.Open("/etc/passwd")
if errors.Is(err, fs.ErrNotExist) {
return "", ErrUnsupported
} else if err != nil {
return "", err
}
defer fl.Close()
s := bufio.NewScanner(fl)
for s.Scan() {
luid, shell, err := parsePasswdLine(s.Text())
if err != nil {
return "", err
}
if luid == uid {
return shell, nil
}
}
if err := s.Err(); err != nil {
return "", err
}
return "", ErrNoSuchUser
}
func parsePasswdLine(line string) (int, string, error) {
sline := strings.Split(line, ":")
if len(sline) < 7 {
return 0, "", ErrInvalidPasswd
}
uid, err := strconv.Atoi(sline[2])
if err != nil {
return 0, "", err
}
return uid, sline[6], nil
}