Initial Commit
This commit is contained in:
commit
ac6e063639
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/gofakeroot
|
21
LICENSE
Normal file
21
LICENSE
Normal 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
17
README.md
Normal 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.
|
60
cmd/gofakeroot/fakeroot.go
Normal file
60
cmd/gofakeroot/fakeroot.go
Normal 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
71
fakeroot.go
Normal 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
|
||||
}
|
69
loginshell/loginshell.go
Normal file
69
loginshell/loginshell.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user