blob: 58076763711610d84278154eb6cffd18f6e80fcf [file] [log] [blame]
// 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"
"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
}
var fd uintptr
func getFd() (uintptr, error) {
if fd == 0 {
f, err := os.Open("/dev/mapper/control")
if os.IsNotExist(err) {
_ = os.MkdirAll("/dev/mapper", 0755)
if err := unix.Mknod("/dev/mapper/control", unix.S_IFCHR|0600, int(unix.Mkdev(10, 236))); err != nil {
return 0, err
}
f, err = os.Open("/dev/mapper/control")
if err != nil {
return 0, err
}
} else if err != nil {
return 0, err
}
fd = f.Fd()
return f.Fd(), nil
}
return fd, nil
}
func GetVersion() (Version, error) {
req := newReq()
fd, err := getFd()
if err != nil {
return Version{}, err
}
if _, _, err := unix.Syscall(unix.SYS_IOCTL, 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
}
fd, err := getFd()
if err != nil {
return 0, err
}
if _, _, err := unix.Syscall(unix.SYS_IOCTL, 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
}
fd, err := getFd()
if err != nil {
return err
}
if _, _, err := unix.Syscall(unix.SYS_IOCTL, 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.
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 {
// Gives the size of the spec and the null-terminated params aligned to 8 bytes
padding := len(target.Parameters) % 8
targetSize := uint32(int(unsafe.Sizeof(DMTargetSpec{})) + (len(target.Parameters) + 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(target.Parameters)
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
}
fd, err := getFd()
if err != nil {
return err
}
if _, _, err := unix.Syscall(unix.SYS_IOCTL, 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
}
fd, err := getFd()
if err != nil {
return err
}
if _, _, err := unix.Syscall(unix.SYS_IOCTL, 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
}