Ctrl K
ComunidadGuías…

pkg/models/zonemount.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package models

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
)

// mountLineCap bounds one line of a mount table MountOf reads: an
// overlay's options (its lower layers) run to a few KiB.
const mountLineCap = 1 << 20

// Mount is one mount of a mount table (a /proc/<pid>/mountinfo): where it
// is mounted (field 5, a path in the view of the process the table is
// of) and its root (field 4: the path, in the filesystem it mounts, of the
// file or directory mounted there), each as the kernel writes it, escapes
// read back. Replaced is set where the kernel wrote the root with
// "//deleted" after it, cut from Root here: the file mounted there was
// unlinked since (a host's tzdata upgrade renames new zone files over the
// old), and the mount still holds the file it held.
type Mount struct {
	Point, Root string
	Replaced    bool
}

// Source is p's path in the filesystem m mounts, p a path at m's point or
// below it: m's root, and then the rest of p below m's point.
func (m Mount) Source(p string) string {
	rest := strings.TrimPrefix(p, m.Point)
	if m.Point == "/" {
		rest = p
	}
	if rest == "" {
		return m.Root
	}
	return path.Join(m.Root, rest)
}

// MountOf is the mount of the mount table in file (a /proc/<pid>/mountinfo,
// or /proc/self/mountinfo) that p, a clean absolute path in the view the
// table writes its mount points in (that process's own root), is read
// through, and true; false where the table lists no mount at p or above
// it. It walks the mounts as a lookup of p crosses them, by each mount's
// id and its parent's (fields 1 and 2), never by the order the table lists
// them in (kernel 6.8 lists them in the order they were made, and a mount
// moved in later can list before the one it sits on): from the mounts
// whose parent the table does not list (the process's root), to the child
// whose point is the first p's path crosses (the one at the shortest
// point at or above p: a mount on a directory hides what its parent
// mounted below it), and on, until no child of the mount reached is at or
// above p. An error where keploy cannot read the table, or where two
// mounts sit at the first point on p's path at one level (two of one
// parent, or two whose parents the table does not list), so keploy cannot
// tell which p is read through. Mounts whose parents loop (no table the kernel
// writes) are never reached.
func MountOf(file, p string) (Mount, bool, error) {
	f, err := os.Open(file)
	if err != nil {
		return Mount{}, false, err
	}
	defer func() { _ = f.Close() }()
	type entry struct {
		id, parent string
		m          Mount
	}
	var ms []entry
	ids := map[string]bool{}
	sc := bufio.NewScanner(f)
	sc.Buffer(make([]byte, 0, 64<<10), mountLineCap)
	for sc.Scan() {
		fields := strings.Fields(sc.Text())
		if len(fields) < 5 {
			continue
		}
		root, replaced := strings.CutSuffix(unescapeMount(fields[3]), "//deleted")
		ms = append(ms, entry{id: fields[0], parent: fields[1], m: Mount{Point: unescapeMount(fields[4]), Root: root, Replaced: replaced}})
		ids[fields[0]] = true
	}
	if err := sc.Err(); err != nil {
		return Mount{}, false, fmt.Errorf("reading %s: %w", file, err)
	}
	children := map[string][]int{}
	var level []int
	for i, e := range ms {
		if e.parent == e.id || !ids[e.parent] {
			level = append(level, i)
			continue
		}
		children[e.parent] = append(children[e.parent], i)
	}
	at := -1
	// Each step goes to a child of the mount reached, from one whose parent
	// the table does not list: no mount whose parents loop is reached (its
	// parent is in the loop), so the walk ends.
	for {
		next, tie := -1, -1
		for _, i := range level {
			mp := ms[i].m.Point
			if mp != p && mp != "/" && !strings.HasPrefix(p, mp+"/") {
				continue
			}
			switch {
			case next < 0 || len(mp) < len(ms[next].m.Point):
				next, tie = i, -1
			case len(mp) == len(ms[next].m.Point):
				tie = i
			}
		}
		if tie >= 0 {
			return Mount{}, false, fmt.Errorf("%s lists two mounts (ids %s and %s) at %s at one level of the mount tree, so keploy cannot tell which %s is read through", file, ms[next].id, ms[tie].id, ms[next].m.Point, p)
		}
		if next < 0 {
			if at < 0 {
				return Mount{}, false, nil
			}
			return ms[at].m, true, nil
		}
		at, level = next, children[ms[next].id]
	}
}

// unescapeMount is a mount table's path as the kernel wrote it, its
// space, tab, newline and backslash escaped as \040, \011, \012 and \134,
// read back.
func unescapeMount(s string) string {
	if !strings.Contains(s, `\`) {
		return s
	}
	var b strings.Builder
	for i := 0; i < len(s); i++ {
		if s[i] == '\\' && i+3 < len(s) && isOctal(s[i+1]) && isOctal(s[i+2]) && isOctal(s[i+3]) {
			b.WriteByte((s[i+1]-'0')<<6 | (s[i+2]-'0')<<3 | (s[i+3] - '0'))
			i += 3
			continue
		}
		b.WriteByte(s[i])
	}
	return b.String()
}

func isOctal(c byte) bool { return c >= '0' && c <= '7' }

// ZoneinfoName is the IANA name a zone file's path names, after its
// zoneinfo directory ("/usr/share/zoneinfo/Asia/Kolkata": "Asia/Kolkata",
// and so of its posix/ tree, the same zones); "" where it names none.
func ZoneinfoName(p string) string {
	i := strings.LastIndex(p, "zoneinfo/")
	if i < 0 {
		return ""
	}
	n := p[i+len("zoneinfo/"):]
	n = strings.TrimPrefix(n, "posix/")
	if n == "" || strings.Contains(n, "..") {
		return ""
	}
	return n
}

// utcNames are IANA's names of UTC: each reads UTC+00:00 with no change
// of offset in every release of its data (the backward file links them
// to Etc/UTC or Etc/GMT), so a file of one mounted over another's holds
// that zone's rules (Fedora's /usr/share/zoneinfo/UTC over an image's
// Etc/UTC).
var utcNames = map[string]bool{
	"UTC": true, "Etc/UTC": true, "UCT": true, "Etc/UCT": true, "Universal": true, "Etc/Universal": true,
	"Zulu": true, "Etc/Zulu": true, "GMT": true, "Etc/GMT": true, "GMT0": true, "Etc/GMT0": true,
	"GMT+0": true, "Etc/GMT+0": true, "GMT-0": true, "Etc/GMT-0": true, "Greenwich": true, "Etc/Greenwich": true,
}

// ZoneMount is a zone file read from another zone's file through a mount
// (ZoneFileMount).
type ZoneMount struct {
	// Source is the path the file is read from in the filesystem the
	// mount comes from (Mount.Source), Zone the zone that path names by
	// its zoneinfo path ("" where it names none: a copy).
	Source, Zone string
	// Replaced: the mounted file was since replaced on the host.
	Replaced bool
}

// ZoneFileMount is what the mount table in table (MountOf) says of the zone
// file at p, a clean path in the table's view with its links resolved,
// which the process reads as the zone zn: nil where p is read as zn's
// file, as far as the mounts tell: through no mount of p itself, nor of a
// directory inside the part of p's path that names its zone (below its
// zoneinfo directory: a directory mounted at or above it keeps every
// file's name in it), or through one whose file (Mount.Source) is zn's, or
// p's own zone's (zn an alias the image links to it: Asia/Calcutta to
// Asia/Kolkata), or of another of IANA's names of UTC where zn is one
// (utcNames). Where it is another zone's file, or one whose path names no
// zone (a host's /etc/localtime that is a copy), the mount (ZoneMount): the
// C library, Go and Python read that file's rules, which are not zn's. An
// error where keploy cannot read the table.
func ZoneFileMount(table, p, zn string) (*ZoneMount, error) {
	m, ok, err := MountOf(table, p)
	if err != nil || !ok {
		return nil, err
	}
	own := ZoneinfoName(p)
	if m.Point != p {
		i := strings.LastIndex(p, "zoneinfo/")
		if i < 0 || len(m.Point) <= i+len("zoneinfo") {
			return nil, nil
		}
	}
	src := m.Source(p)
	sn := ZoneinfoName(src)
	if sn != "" && (sn == zn || sn == own || utcNames[sn] && utcNames[zn]) {
		return nil, nil
	}
	return &ZoneMount{Source: src, Zone: sn, Replaced: m.Replaced}, nil
}

// ownZoneMounts is the mount table keploy's own zone files are read
// through (ZoneLocation); a variable, so a row reads a table of its own.
var ownZoneMounts = "/proc/self/mountinfo"

// goZoneDirs are the directories time.LoadLocation reads a zone's file
// from on Linux, in its order, after $ZONEINFO's.
var goZoneDirs = []string{"/usr/share/zoneinfo/", "/usr/share/lib/zoneinfo/", "/usr/lib/locale/TZ/", "/etc/zoneinfo/"}

// errOwnZoneMount marks ZoneLocation's refusal of a name whose file in
// keploy's own zone files is mounted from another zone's (IsOwnZoneMount).
var errOwnZoneMount = errors.New("keploy's own zone file is mounted from another zone's")

// IsOwnZoneMount reports whether err is ZoneLocation's refusal of a zone
// whose file in keploy's own zone files is another zone's, mounted over it
// where keploy runs: a record made again with the same keploy reads it the
// same way.
func IsOwnZoneMount(err error) bool { return errors.Is(err, errOwnZoneMount) }

// SetOwnZoneFilesForTest makes ZoneLocation's mount check (and only it)
// look for keploy's own file of a zone under dirs, each ending in "/", in
// place of Go's zone directories (goZoneDirs; a $ZONEINFO directory is
// still looked in first), and read the mount table at table in place of
// /proc/self/mountinfo, forgetting the zones ZoneLocation has read; the
// returned func puts both back and forgets them again. time.LoadLocation
// still reads the zone itself from Go's own directories or time/tzdata, so
// the files under dirs need hold no zone. For rows of other packages, which
// cannot set these variables: never called outside a test, and never from a
// parallel test, as it writes them unguarded (the t.Setenv of $ZONEINFO a
// row needs anyway refuses a parallel test).
func SetOwnZoneFilesForTest(table string, dirs []string) (restore func()) {
	oldDirs, oldTable := goZoneDirs, ownZoneMounts
	goZoneDirs, ownZoneMounts = dirs, table
	zoneLocations.Clear()
	return func() {
		goZoneDirs, ownZoneMounts = oldDirs, oldTable
		zoneLocations.Clear()
	}
}

// ownZoneFileMounted is nil where the file time.LoadLocation reads name
// from (goZoneFile), if any, is name's own as far as keploy's own mounts
// tell (ZoneFileMount); an error naming both where it is mounted from
// another zone's file (a host's /etc/localtime bind-mounted over the link
// to the zone where keploy runs natively, in the application's
// container), or where keploy cannot tell. Off Linux no mount table is
// read: macOS's zone files are no container's.
func ownZoneFileMounted(name string) error {
	if runtime.GOOS != "linux" {
		return nil
	}
	f := goZoneFile(name)
	if f == "" {
		return nil
	}
	p, err := filepath.EvalSymlinks(f)
	if err != nil {
		return fmt.Errorf("keploy cannot resolve its own zone file %s of %q, so it cannot tell whether it is a file mounted from another zone's: %w", f, name, err)
	}
	m, err := ZoneFileMount(ownZoneMounts, p, name)
	if err != nil {
		return fmt.Errorf("keploy cannot read its own mounts, so it cannot tell whether its zone file %s of %q is a file mounted from another zone's: %w", p, name, err)
	}
	if m == nil {
		return nil
	}
	what := fmt.Sprintf("the zone file of %q", m.Zone)
	if m.Zone == "" {
		what = "a file whose path names no zone (a copy)"
	}
	if m.Replaced {
		what += ", since replaced on the host"
	}
	return fmt.Errorf("%w: keploy reads %q from its own zone file %s, which is a file mounted from %s, %s, so keploy cannot tell whether it holds %q's rules, which a JVM and Node read %q by in their own zone data. A host's /etc/localtime bind-mounted over the link to that zone where keploy runs (-v /etc/localtime:/etc/localtime:ro lands on the file the link names) gives this: drop the mount, or set TZ to the zone the application runs in",
		errOwnZoneMount, name, p, m.Source, what, name, name)
}

// goZoneFile is the file time.LoadLocation reads name from: under
// $ZONEINFO where that is a directory holding it, else under the first of
// Go's zone directories that holds it (goZoneDirs); "" where it reads none
// of them (keploy's built-in zone data: time/tzdata). A $ZONEINFO that is a
// zip file is passed over: Go reads name from it where it holds it, so the
// file checked is then one it does not read, never one it reads unchecked.
func goZoneFile(name string) string {
	dirs := goZoneDirs
	if z := os.Getenv("ZONEINFO"); z != "" {
		if fi, err := os.Stat(z); err == nil && fi.IsDir() {
			dirs = append([]string{z + "/"}, dirs...)
		}
	}
	for _, d := range dirs {
		if fi, err := os.Stat(d + name); err == nil && fi.Mode().IsRegular() {
			return d + name
		}
	}
	return ""
}