|  | // Copyright 2020 The Monogon Project Authors. | 
|  | // | 
|  | // SPDX-License-Identifier: Apache-2.0 | 
|  | // | 
|  | // 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. | 
|  |  | 
|  | // package devicemapper is a thin wrapper for the devicemapper ioctl API. | 
|  | // See: https://github.com/torvalds/linux/blob/master/include/uapi/linux/dm-ioctl.h | 
|  | package devicemapper | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "encoding/binary" | 
|  | "fmt" | 
|  | "os" | 
|  | "runtime" | 
|  | "strings" | 
|  | "sync" | 
|  | "unsafe" | 
|  |  | 
|  | "github.com/pkg/errors" | 
|  | "github.com/yalue/native_endian" | 
|  | "golang.org/x/sys/unix" | 
|  | ) | 
|  |  | 
|  | type DMIoctl struct { | 
|  | Version     Version | 
|  | DataSize    uint32 | 
|  | DataStart   uint32 | 
|  | TargetCount uint32 | 
|  | OpenCount   int32 | 
|  | Flags       uint32 | 
|  | EventNumber uint32 | 
|  | _padding1   uint32 | 
|  | Dev         uint64 | 
|  | Name        [128]byte | 
|  | UUID        [129]byte | 
|  | _padding2   [7]byte | 
|  | Data        [16384]byte | 
|  | } | 
|  |  | 
|  | type DMTargetSpec struct { | 
|  | SectorStart uint64 | 
|  | Length      uint64 | 
|  | Status      int32 | 
|  | Next        uint32 | 
|  | TargetType  [16]byte | 
|  | } | 
|  |  | 
|  | type DMTargetDeps struct { | 
|  | Count   uint32 | 
|  | Padding uint32 | 
|  | Dev     []uint64 | 
|  | } | 
|  |  | 
|  | type DMNameList struct { | 
|  | Dev  uint64 | 
|  | Next uint32 | 
|  | Name []byte | 
|  | } | 
|  |  | 
|  | type DMTargetVersions struct { | 
|  | Next    uint32 | 
|  | Version [3]uint32 | 
|  | } | 
|  |  | 
|  | type DMTargetMessage struct { | 
|  | Sector  uint64 | 
|  | Message []byte | 
|  | } | 
|  |  | 
|  | type Version [3]uint32 | 
|  |  | 
|  | const ( | 
|  | /* Top level cmds */ | 
|  | DM_VERSION_CMD uintptr = (0xc138fd << 8) + iota | 
|  | DM_REMOVE_ALL_CMD | 
|  | DM_LIST_DEVICES_CMD | 
|  |  | 
|  | /* device level cmds */ | 
|  | DM_DEV_CREATE_CMD | 
|  | DM_DEV_REMOVE_CMD | 
|  | DM_DEV_RENAME_CMD | 
|  | DM_DEV_SUSPEND_CMD | 
|  | DM_DEV_STATUS_CMD | 
|  | DM_DEV_WAIT_CMD | 
|  |  | 
|  | /* Table level cmds */ | 
|  | DM_TABLE_LOAD_CMD | 
|  | DM_TABLE_CLEAR_CMD | 
|  | DM_TABLE_DEPS_CMD | 
|  | DM_TABLE_STATUS_CMD | 
|  |  | 
|  | /* Added later */ | 
|  | DM_LIST_VERSIONS_CMD | 
|  | DM_TARGET_MSG_CMD | 
|  | DM_DEV_SET_GEOMETRY_CMD | 
|  | DM_DEV_ARM_POLL_CMD | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | DM_READONLY_FLAG       = 1 << 0 /* In/Out */ | 
|  | DM_SUSPEND_FLAG        = 1 << 1 /* In/Out */ | 
|  | DM_PERSISTENT_DEV_FLAG = 1 << 3 /* In */ | 
|  | ) | 
|  |  | 
|  | const baseDataSize = uint32(unsafe.Sizeof(DMIoctl{})) - 16384 | 
|  |  | 
|  | func newReq() DMIoctl { | 
|  | return DMIoctl{ | 
|  | Version:   Version{4, 0, 0}, | 
|  | DataSize:  baseDataSize, | 
|  | DataStart: baseDataSize, | 
|  | } | 
|  | } | 
|  |  | 
|  | // stringToDelimitedBuf copies src to dst and returns an error if len(src) > | 
|  | // len(dst), or when the string contains a null byte. | 
|  | func stringToDelimitedBuf(dst []byte, src string) error { | 
|  | if len(src) > len(dst)-1 { | 
|  | return fmt.Errorf("string longer than target buffer (%v > %v)", len(src), len(dst)-1) | 
|  | } | 
|  | for i := 0; i < len(src); i++ { | 
|  | if src[i] == 0x00 { | 
|  | return errors.New("string contains null byte, this is unsupported by DM") | 
|  | } | 
|  | dst[i] = src[i] | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // marshalParams marshals a list of strings into a single string according to | 
|  | // the rules in the kernel-side decoder. Strings with null bytes or only | 
|  | // whitespace characters cannot be encoded and will return an errors. | 
|  | func marshalParams(params []string) (string, error) { | 
|  | var strb strings.Builder | 
|  | for _, param := range params { | 
|  | var hasNonWhitespace bool | 
|  | for i := 0; i < len(param); i++ { | 
|  | b := param[i] | 
|  | if b == 0x00 { | 
|  | return "", errors.New("parameter with null bytes cannot be encoded") | 
|  | } | 
|  | isWhitespace := ctypeLookup[b]&_S != 0 | 
|  | if !isWhitespace { | 
|  | hasNonWhitespace = true | 
|  | } | 
|  | if isWhitespace || b == '\\' { | 
|  | strb.WriteByte('\\') | 
|  | } | 
|  | strb.WriteByte(b) | 
|  | } | 
|  | if !hasNonWhitespace { | 
|  | return "", errors.New("parameter with only whitespace cannot be encoded") | 
|  | } | 
|  | strb.WriteByte(' ') | 
|  | } | 
|  | return strb.String(), nil | 
|  | } | 
|  |  | 
|  | var ctrlFile *os.File | 
|  | var ctrlFileError error | 
|  | var ctrlFileOnce sync.Once | 
|  |  | 
|  | func initCtrlFile() { | 
|  | ctrlFile, ctrlFileError = os.Open("/dev/mapper/control") | 
|  | if os.IsNotExist(ctrlFileError) { | 
|  | _ = os.MkdirAll("/dev/mapper", 0755) | 
|  | ctrlFileError = unix.Mknod("/dev/mapper/control", unix.S_IFCHR|0600, int(unix.Mkdev(10, 236))) | 
|  | if ctrlFileError != nil { | 
|  | ctrlFileError = fmt.Errorf("devicemapper control device doesn't exist and can't be mknod()ed: %w", ctrlFileError) | 
|  | return | 
|  | } | 
|  | ctrlFile, ctrlFileError = os.Open("/dev/mapper/control") | 
|  | } | 
|  | if ctrlFileError != nil { | 
|  | ctrlFileError = fmt.Errorf("failed to open devicemapper control device: %w", ctrlFileError) | 
|  | } | 
|  | } | 
|  |  | 
|  | func GetVersion() (Version, error) { | 
|  | req := newReq() | 
|  | ctrlFileOnce.Do(initCtrlFile) | 
|  | if ctrlFileError != nil { | 
|  | return Version{}, ctrlFileError | 
|  | } | 
|  | if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_VERSION_CMD, uintptr(unsafe.Pointer(&req))); err != 0 { | 
|  | return Version{}, err | 
|  | } | 
|  | return req.Version, nil | 
|  | } | 
|  |  | 
|  | func CreateDevice(name string) (uint64, error) { | 
|  | req := newReq() | 
|  | if err := stringToDelimitedBuf(req.Name[:], name); err != nil { | 
|  | return 0, err | 
|  | } | 
|  | ctrlFileOnce.Do(initCtrlFile) | 
|  | if ctrlFileError != nil { | 
|  | return 0, ctrlFileError | 
|  | } | 
|  | if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_DEV_CREATE_CMD, uintptr(unsafe.Pointer(&req))); err != 0 { | 
|  | return 0, err | 
|  | } | 
|  | return req.Dev, nil | 
|  | } | 
|  |  | 
|  | func RemoveDevice(name string) error { | 
|  | req := newReq() | 
|  | if err := stringToDelimitedBuf(req.Name[:], name); err != nil { | 
|  | return err | 
|  | } | 
|  | ctrlFileOnce.Do(initCtrlFile) | 
|  | if ctrlFileError != nil { | 
|  | return ctrlFileError | 
|  | } | 
|  | if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_DEV_REMOVE_CMD, uintptr(unsafe.Pointer(&req))); err != 0 { | 
|  | return err | 
|  | } | 
|  | runtime.KeepAlive(req) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // Target represents a byte region inside a devicemapper table for a given | 
|  | // device provided by a given target implementation. | 
|  | type Target struct { | 
|  | // StartSector is the first sector (defined as being 512 bytes long) this | 
|  | // target covers. | 
|  | StartSector uint64 | 
|  | // Length is the number of sectors (defined as being 512 bytes long) this | 
|  | // target covers, starting from StartSector. | 
|  | Length uint64 | 
|  | // Type is the type of target handling this byte region. | 
|  | // Types implemented by the Linux kernel can be found at | 
|  | // @linux//drivers/md/... by looking for dm_register_target() calls. | 
|  | Type string | 
|  | // Parameters are additional parameters specific to the target type. | 
|  | // Note that null bytes and parameters consisting only of whitespace | 
|  | // characters cannot be encoded and will return an error. | 
|  | Parameters []string | 
|  | } | 
|  |  | 
|  | func LoadTable(name string, readOnly bool, targets []Target) error { | 
|  | req := newReq() | 
|  | if err := stringToDelimitedBuf(req.Name[:], name); err != nil { | 
|  | return err | 
|  | } | 
|  | var data bytes.Buffer | 
|  | for _, target := range targets { | 
|  | encodedParams, err := marshalParams(target.Parameters) | 
|  | if err != nil { | 
|  | return fmt.Errorf("cannot encode parameters: %w", err) | 
|  | } | 
|  | // Gives the size of the spec and the null-terminated params aligned to 8 bytes | 
|  | padding := len(encodedParams) % 8 | 
|  | targetSize := uint32(int(unsafe.Sizeof(DMTargetSpec{})) + (len(encodedParams) + 1) + padding) | 
|  |  | 
|  | targetSpec := DMTargetSpec{ | 
|  | SectorStart: target.StartSector, | 
|  | Length:      target.Length, | 
|  | Next:        targetSize, | 
|  | } | 
|  | if err := stringToDelimitedBuf(targetSpec.TargetType[:], target.Type); err != nil { | 
|  | return err | 
|  | } | 
|  | if err := binary.Write(&data, native_endian.NativeEndian(), &targetSpec); err != nil { | 
|  | panic(err) | 
|  | } | 
|  | data.WriteString(encodedParams) | 
|  | data.WriteByte(0x00) | 
|  | for i := 0; i < padding; i++ { | 
|  | data.WriteByte(0x00) | 
|  | } | 
|  | } | 
|  | req.TargetCount = uint32(len(targets)) | 
|  | if data.Len() >= 16384 { | 
|  | return errors.New("table too large for allocated memory") | 
|  | } | 
|  | req.DataSize = baseDataSize + uint32(data.Len()) | 
|  | copy(req.Data[:], data.Bytes()) | 
|  | if readOnly { | 
|  | req.Flags = DM_READONLY_FLAG | 
|  | } | 
|  | ctrlFileOnce.Do(initCtrlFile) | 
|  | if ctrlFileError != nil { | 
|  | return ctrlFileError | 
|  | } | 
|  | if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_TABLE_LOAD_CMD, uintptr(unsafe.Pointer(&req))); err != 0 { | 
|  | return err | 
|  | } | 
|  | runtime.KeepAlive(req) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func suspendResume(name string, suspend bool) error { | 
|  | req := newReq() | 
|  | if err := stringToDelimitedBuf(req.Name[:], name); err != nil { | 
|  | return err | 
|  | } | 
|  | if suspend { | 
|  | req.Flags = DM_SUSPEND_FLAG | 
|  | } | 
|  | ctrlFileOnce.Do(initCtrlFile) | 
|  | if ctrlFileError != nil { | 
|  | return ctrlFileError | 
|  | } | 
|  | if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_DEV_SUSPEND_CMD, uintptr(unsafe.Pointer(&req))); err != 0 { | 
|  | return err | 
|  | } | 
|  | runtime.KeepAlive(req) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func Suspend(name string) error { | 
|  | return suspendResume(name, true) | 
|  | } | 
|  | func Resume(name string) error { | 
|  | return suspendResume(name, false) | 
|  | } | 
|  |  | 
|  | func CreateActiveDevice(name string, readOnly bool, targets []Target) (uint64, error) { | 
|  | dev, err := CreateDevice(name) | 
|  | if err != nil { | 
|  | return 0, fmt.Errorf("DM_DEV_CREATE failed: %w", err) | 
|  | } | 
|  | if err := LoadTable(name, readOnly, targets); err != nil { | 
|  | _ = RemoveDevice(name) | 
|  | return 0, fmt.Errorf("DM_TABLE_LOAD failed: %w", err) | 
|  | } | 
|  | if err := Resume(name); err != nil { | 
|  | _ = RemoveDevice(name) | 
|  | return 0, fmt.Errorf("DM_DEV_SUSPEND failed: %w", err) | 
|  | } | 
|  | return dev, nil | 
|  | } |