
Go 1.15.7 contained a security fix for CVE-2021-3115, which allowed arbitrary code to be executed at build time when using cgo on Windows. This issue also affects Unix users who have “.” listed explicitly in their PATH and are running “go get” outside of a module or with module mode disabled. This issue is not limited to the go command itself, and can also affect binaries that use `os.Command`, `os.LookPath`, etc. From the related blogpost (ttps://blog.golang.org/path-security): > Are your own programs affected? > > If you use exec.LookPath or exec.Command in your own programs, you only need to > be concerned if you (or your users) run your program in a directory with untrusted > contents. If so, then a subprocess could be started using an executable from dot > instead of from a system directory. (Again, using an executable from dot happens > always on Windows and only with uncommon PATH settings on Unix.) > > If you are concerned, then we’ve published the more restricted variant of os/exec > as golang.org/x/sys/execabs. You can use it in your program by simply replacing This patch replaces all uses of `os/exec` with `golang.org/x/sys/execabs`. While some uses of `os/exec` should not be problematic (e.g. part of tests), it is probably good to be consistent, in case code gets moved around. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
445 lines
12 KiB
Go
445 lines
12 KiB
Go
//go:build linux
|
|
// +build linux
|
|
|
|
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Copyright 2012-2017 Docker, Inc.
|
|
|
|
package dmsetup
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
blkdiscard "github.com/containerd/containerd/snapshots/devmapper/blkdiscard"
|
|
"github.com/pkg/errors"
|
|
exec "golang.org/x/sys/execabs"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const (
|
|
// DevMapperDir represents devmapper devices location
|
|
DevMapperDir = "/dev/mapper/"
|
|
// SectorSize represents the number of bytes in one sector on devmapper devices
|
|
SectorSize = 512
|
|
)
|
|
|
|
// ErrInUse represents an error mutating a device because it is in use elsewhere
|
|
var ErrInUse = errors.New("device is in use")
|
|
|
|
// DeviceInfo represents device info returned by "dmsetup info".
|
|
// dmsetup(8) provides more information on each of these fields.
|
|
type DeviceInfo struct {
|
|
Name string
|
|
BlockDeviceName string
|
|
TableLive bool
|
|
TableInactive bool
|
|
Suspended bool
|
|
ReadOnly bool
|
|
Major uint32
|
|
Minor uint32
|
|
OpenCount uint32 // Open reference count
|
|
TargetCount uint32 // Number of targets in the live table
|
|
EventNumber uint32 // Last event sequence number (used by wait)
|
|
}
|
|
|
|
var errTable map[string]unix.Errno
|
|
|
|
func init() {
|
|
// Precompute map of <text>=<errno> for optimal lookup
|
|
errTable = make(map[string]unix.Errno)
|
|
for errno := unix.EPERM; errno <= unix.EHWPOISON; errno++ {
|
|
errTable[errno.Error()] = errno
|
|
}
|
|
}
|
|
|
|
// CreatePool creates a device with the given name, data and metadata file and block size (see "dmsetup create")
|
|
func CreatePool(poolName, dataFile, metaFile string, blockSizeSectors uint32) error {
|
|
thinPool, err := makeThinPoolMapping(dataFile, metaFile, blockSizeSectors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = dmsetup("create", poolName, "--table", thinPool)
|
|
return err
|
|
}
|
|
|
|
// ReloadPool reloads existing thin-pool (see "dmsetup reload")
|
|
func ReloadPool(deviceName, dataFile, metaFile string, blockSizeSectors uint32) error {
|
|
thinPool, err := makeThinPoolMapping(dataFile, metaFile, blockSizeSectors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = dmsetup("reload", deviceName, "--table", thinPool)
|
|
return err
|
|
}
|
|
|
|
const (
|
|
lowWaterMark = 32768 // Picked arbitrary, might need tuning
|
|
skipZeroing = "skip_block_zeroing" // Skipping zeroing to reduce latency for device creation
|
|
)
|
|
|
|
// makeThinPoolMapping makes thin-pool table entry
|
|
func makeThinPoolMapping(dataFile, metaFile string, blockSizeSectors uint32) (string, error) {
|
|
dataDeviceSizeBytes, err := BlockDeviceSize(dataFile)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "failed to get block device size: %s", dataFile)
|
|
}
|
|
|
|
// Thin-pool mapping target has the following format:
|
|
// start - starting block in virtual device
|
|
// length - length of this segment
|
|
// metadata_dev - the metadata device
|
|
// data_dev - the data device
|
|
// data_block_size - the data block size in sectors
|
|
// low_water_mark - the low water mark, expressed in blocks of size data_block_size
|
|
// feature_args - the number of feature arguments
|
|
// args
|
|
lengthSectors := dataDeviceSizeBytes / SectorSize
|
|
target := fmt.Sprintf("0 %d thin-pool %s %s %d %d 1 %s",
|
|
lengthSectors,
|
|
metaFile,
|
|
dataFile,
|
|
blockSizeSectors,
|
|
lowWaterMark,
|
|
skipZeroing)
|
|
|
|
return target, nil
|
|
}
|
|
|
|
// CreateDevice sends "create_thin <deviceID>" message to the given thin-pool
|
|
func CreateDevice(poolName string, deviceID uint32) error {
|
|
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("create_thin %d", deviceID))
|
|
return err
|
|
}
|
|
|
|
// ActivateDevice activates the given thin-device using the 'thin' target
|
|
func ActivateDevice(poolName string, deviceName string, deviceID uint32, size uint64, external string) error {
|
|
mapping := makeThinMapping(poolName, deviceID, size, external)
|
|
_, err := dmsetup("create", deviceName, "--table", mapping)
|
|
return err
|
|
}
|
|
|
|
// makeThinMapping makes thin target table entry
|
|
func makeThinMapping(poolName string, deviceID uint32, sizeBytes uint64, externalOriginDevice string) string {
|
|
lengthSectors := sizeBytes / SectorSize
|
|
|
|
// Thin target has the following format:
|
|
// start - starting block in virtual device
|
|
// length - length of this segment
|
|
// pool_dev - the thin-pool device, can be /dev/mapper/pool_name or 253:0
|
|
// dev_id - the internal device id of the device to be activated
|
|
// external_origin_dev - an optional block device outside the pool to be treated as a read-only snapshot origin.
|
|
target := fmt.Sprintf("0 %d thin %s %d %s", lengthSectors, GetFullDevicePath(poolName), deviceID, externalOriginDevice)
|
|
return strings.TrimSpace(target)
|
|
}
|
|
|
|
// SuspendDevice suspends the given device (see "dmsetup suspend")
|
|
func SuspendDevice(deviceName string) error {
|
|
_, err := dmsetup("suspend", deviceName)
|
|
return err
|
|
}
|
|
|
|
// ResumeDevice resumes the given device (see "dmsetup resume")
|
|
func ResumeDevice(deviceName string) error {
|
|
_, err := dmsetup("resume", deviceName)
|
|
return err
|
|
}
|
|
|
|
// Table returns the current table for the device
|
|
func Table(deviceName string) (string, error) {
|
|
return dmsetup("table", deviceName)
|
|
}
|
|
|
|
// CreateSnapshot sends "create_snap" message to the given thin-pool.
|
|
// Caller needs to suspend and resume device if it is active.
|
|
func CreateSnapshot(poolName string, deviceID uint32, baseDeviceID uint32) error {
|
|
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("create_snap %d %d", deviceID, baseDeviceID))
|
|
return err
|
|
}
|
|
|
|
// DeleteDevice sends "delete <deviceID>" message to the given thin-pool
|
|
func DeleteDevice(poolName string, deviceID uint32) error {
|
|
_, err := dmsetup("message", poolName, "0", fmt.Sprintf("delete %d", deviceID))
|
|
return err
|
|
}
|
|
|
|
// RemoveDeviceOpt represents command line arguments for "dmsetup remove" command
|
|
type RemoveDeviceOpt string
|
|
|
|
const (
|
|
// RemoveWithForce flag replaces the table with one that fails all I/O if
|
|
// open device can't be removed
|
|
RemoveWithForce RemoveDeviceOpt = "--force"
|
|
// RemoveWithRetries option will cause the operation to be retried
|
|
// for a few seconds before failing
|
|
RemoveWithRetries RemoveDeviceOpt = "--retry"
|
|
// RemoveDeferred flag will enable deferred removal of open devices,
|
|
// the device will be removed when the last user closes it
|
|
RemoveDeferred RemoveDeviceOpt = "--deferred"
|
|
)
|
|
|
|
// RemoveDevice removes a device (see "dmsetup remove")
|
|
func RemoveDevice(deviceName string, opts ...RemoveDeviceOpt) error {
|
|
args := []string{
|
|
"remove",
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
args = append(args, string(opt))
|
|
}
|
|
|
|
args = append(args, GetFullDevicePath(deviceName))
|
|
|
|
_, err := dmsetup(args...)
|
|
if err == unix.ENXIO {
|
|
// Ignore "No such device or address" error because we dmsetup
|
|
// remove with "deferred" option, there is chance for the device
|
|
// having been removed.
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Info outputs device information (see "dmsetup info").
|
|
// If device name is empty, all device infos will be returned.
|
|
func Info(deviceName string) ([]*DeviceInfo, error) {
|
|
output, err := dmsetup(
|
|
"info",
|
|
"--columns",
|
|
"--noheadings",
|
|
"-o",
|
|
"name,blkdevname,attr,major,minor,open,segments,events",
|
|
"--separator",
|
|
" ",
|
|
deviceName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
lines = strings.Split(output, "\n")
|
|
devices = make([]*DeviceInfo, len(lines))
|
|
)
|
|
|
|
for i, line := range lines {
|
|
var (
|
|
attr = ""
|
|
info = &DeviceInfo{}
|
|
)
|
|
|
|
_, err := fmt.Sscan(line,
|
|
&info.Name,
|
|
&info.BlockDeviceName,
|
|
&attr,
|
|
&info.Major,
|
|
&info.Minor,
|
|
&info.OpenCount,
|
|
&info.TargetCount,
|
|
&info.EventNumber)
|
|
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse line %q", line)
|
|
}
|
|
|
|
// Parse attributes (see "man 8 dmsetup" for details)
|
|
info.Suspended = strings.Contains(attr, "s")
|
|
info.ReadOnly = strings.Contains(attr, "r")
|
|
info.TableLive = strings.Contains(attr, "L")
|
|
info.TableInactive = strings.Contains(attr, "I")
|
|
|
|
devices[i] = info
|
|
}
|
|
|
|
return devices, nil
|
|
}
|
|
|
|
// Version returns "dmsetup version" output
|
|
func Version() (string, error) {
|
|
return dmsetup("version")
|
|
}
|
|
|
|
// DeviceStatus represents devmapper device status information
|
|
type DeviceStatus struct {
|
|
RawOutput string
|
|
Offset int64
|
|
Length int64
|
|
Target string
|
|
Params []string
|
|
}
|
|
|
|
// Status provides status information for devmapper device
|
|
func Status(deviceName string) (*DeviceStatus, error) {
|
|
var (
|
|
err error
|
|
status DeviceStatus
|
|
)
|
|
|
|
output, err := dmsetup("status", deviceName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
status.RawOutput = output
|
|
|
|
// Status output format:
|
|
// Offset (int64)
|
|
// Length (int64)
|
|
// Target type (string)
|
|
// Params (Array of strings)
|
|
const MinParseCount = 4
|
|
parts := strings.Split(output, " ")
|
|
if len(parts) < MinParseCount {
|
|
return nil, errors.Errorf("failed to parse output: %q", output)
|
|
}
|
|
|
|
status.Offset, err = strconv.ParseInt(parts[0], 10, 64)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse offset: %q", parts[0])
|
|
}
|
|
|
|
status.Length, err = strconv.ParseInt(parts[1], 10, 64)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse length: %q", parts[1])
|
|
}
|
|
|
|
status.Target = parts[2]
|
|
status.Params = parts[3:]
|
|
|
|
return &status, nil
|
|
}
|
|
|
|
// GetFullDevicePath returns full path for the given device name (like "/dev/mapper/name")
|
|
func GetFullDevicePath(deviceName string) string {
|
|
if strings.HasPrefix(deviceName, DevMapperDir) {
|
|
return deviceName
|
|
}
|
|
|
|
return DevMapperDir + deviceName
|
|
}
|
|
|
|
// BlockDeviceSize returns size of block device in bytes
|
|
func BlockDeviceSize(path string) (int64, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer f.Close()
|
|
|
|
size, err := f.Seek(0, io.SeekEnd)
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "failed to seek on %q", path)
|
|
}
|
|
return size, nil
|
|
}
|
|
|
|
// DiscardBlocks discards all blocks for the given thin device
|
|
// ported from https://github.com/moby/moby/blob/7b9275c0da707b030e62c96b679a976f31f929d3/pkg/devicemapper/devmapper.go#L416
|
|
func DiscardBlocks(deviceName string) error {
|
|
inUse, err := isInUse(deviceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if inUse {
|
|
return ErrInUse
|
|
}
|
|
path := GetFullDevicePath(deviceName)
|
|
_, err = blkdiscard.BlkDiscard(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dmsetup(args ...string) (string, error) {
|
|
data, err := exec.Command("dmsetup", args...).CombinedOutput()
|
|
output := string(data)
|
|
if err != nil {
|
|
// Try find Linux error code otherwise return generic error with dmsetup output
|
|
if errno, ok := tryGetUnixError(output); ok {
|
|
return "", errno
|
|
}
|
|
|
|
return "", errors.Wrapf(err, "dmsetup %s\nerror: %s\n", strings.Join(args, " "), output)
|
|
}
|
|
|
|
output = strings.TrimSuffix(output, "\n")
|
|
output = strings.TrimSpace(output)
|
|
|
|
return output, nil
|
|
}
|
|
|
|
// tryGetUnixError tries to find Linux error code from dmsetup output
|
|
func tryGetUnixError(output string) (unix.Errno, bool) {
|
|
// It's useful to have Linux error codes like EBUSY, EPERM, ..., instead of just text.
|
|
// Unfortunately there is no better way than extracting/comparing error text.
|
|
text := parseDmsetupError(output)
|
|
if text == "" {
|
|
return 0, false
|
|
}
|
|
|
|
err, ok := errTable[text]
|
|
return err, ok
|
|
}
|
|
|
|
// dmsetup returns error messages in format:
|
|
// device-mapper: message ioctl on <name> failed: File exists\n
|
|
// Command failed\n
|
|
// parseDmsetupError extracts text between "failed: " and "\n"
|
|
func parseDmsetupError(output string) string {
|
|
lines := strings.SplitN(output, "\n", 2)
|
|
if len(lines) < 2 {
|
|
return ""
|
|
}
|
|
|
|
line := lines[0]
|
|
// Handle output like "Device /dev/mapper/snapshotter-suite-pool-snap-1 not found"
|
|
if strings.HasSuffix(line, "not found") {
|
|
return unix.ENXIO.Error()
|
|
}
|
|
|
|
const failedSubstr = "failed: "
|
|
idx := strings.LastIndex(line, failedSubstr)
|
|
if idx == -1 {
|
|
return ""
|
|
}
|
|
|
|
str := line[idx:]
|
|
|
|
// Strip "failed: " prefix
|
|
str = strings.TrimPrefix(str, failedSubstr)
|
|
|
|
str = strings.ToLower(str)
|
|
return str
|
|
}
|
|
|
|
func isInUse(deviceName string) (bool, error) {
|
|
info, err := Info(deviceName)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
if len(info) != 1 {
|
|
return true, errors.New("could not get device info")
|
|
}
|
|
return info[0].OpenCount != 0, nil
|
|
}
|