Repository: opencontainers/runc
Pull request #2956 (2021-05-20 06:36:56)

Fix/improve runc -v
Two things:

1. `runc -v`: set default for, always show `main.version`
    
    Apparently not everyone compiles runc via the provided Makefile. For
    example, one can just run `go build`, in which case `version` variable
    is left empty, which leads to:
    
            $ ./runc -v
            runc version spec: 1.0.2-dev
            go: go1.16.3
    
    Surely, the main problem here is runc was built in a wrong way, but the
    second problem is such output is very confusing -- it may seem that we
    have runc 1.0.2.
    
    To solve, make sure to _always_ add version (even if empty), plus set a
    default value in case someone compiles runc in a wrong way.
    
            $ ./runc -v
            runc version unknown
            spec: 1.0.2-dev
            go: go1.16.3

    NOTE this does not change anything in case runc is compiled normally
    (via the Makefile).
       
2. `Makefile`: use `git describe` for `COMMIT`
    
    Use `git describe --dirty --long` instead of `git rev-parse`. As a
    result, the commit ID will contain the closest tag, the number of commits
    since the tag, and the (abbreviated) git commit sha (see the example below).
    
    NOTE that this tag is still unique and can be used instead of bare sha
    for all git commands.
    
    Example output of `runc -v | grep commit`.
    
    Before:

            commit: 4d875738716862d08a84c00211b1983c3044f711

    After:

            commit: v1.0.0-rc95-9-g6f55d074
    
    This means that
     - the closest tag is v1.0.0-rc95
     - there were 9 commits after the tag
     - the abbreviated sha is 6f55d074



Patch:
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index f54cc6e9..e41a7af7 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -20,7 +20,9 @@ jobs:
       - uses: golangci/golangci-lint-action@v2
         with:
           # must be specified without patch version
-          version: v1.40
+          version: v1.36
+          # Only show new issues for a pull request.
+          only-new-issues: true
 
   shfmt:
     runs-on: ubuntu-20.04
diff --git a/Makefile b/Makefile
index aff6d531..4c73e6ab 100644
--- a/Makefile
+++ b/Makefile
@@ -10,8 +10,7 @@ GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g")
 RUNC_IMAGE := runc_dev$(if $(GIT_BRANCH_CLEAN),:$(GIT_BRANCH_CLEAN))
 PROJECT := github.com/opencontainers/runc
 BUILDTAGS ?= seccomp
-COMMIT_NO := $(shell git rev-parse HEAD 2> /dev/null || true)
-COMMIT ?= $(if $(shell git status --porcelain --untracked-files=no),"$(COMMIT_NO)-dirty","$(COMMIT_NO)")
+COMMIT ?= $(shell git describe --dirty --long --always)
 VERSION := $(shell cat ./VERSION)
 
 # TODO: rm -mod=vendor once go 1.13 is unsupported
diff --git a/libcontainer/cgroups/devices/devices_emulator.go b/libcontainer/cgroups/devices/devices_emulator.go
index 6f29ef03..3572a5ea 100644
--- a/libcontainer/cgroups/devices/devices_emulator.go
+++ b/libcontainer/cgroups/devices/devices_emulator.go
@@ -371,12 +371,3 @@ func (source *Emulator) Transition(target *Emulator) ([]*devices.Rule, error) {
 	}
 	return transitionRules, nil
 }
-
-// Rules returns the minimum set of rules necessary to convert a *deny-all*
-// cgroup to the emulated filter state (note that this is not the same as a
-// default cgroupv1 cgroup -- which is allow-all). This is effectively just a
-// wrapper around Transition() with the source emulator being an empty cgroup.
-func (e *Emulator) Rules() ([]*devices.Rule, error) {
-	defaultCgroup := &Emulator{defaultAllow: false}
-	return defaultCgroup.Transition(e)
-}
diff --git a/libcontainer/cgroups/ebpf/devicefilter/devicefilter.go b/libcontainer/cgroups/ebpf/devicefilter/devicefilter.go
index 96cbca39..fcd3746e 100644
--- a/libcontainer/cgroups/ebpf/devicefilter/devicefilter.go
+++ b/libcontainer/cgroups/ebpf/devicefilter/devicefilter.go
@@ -11,7 +11,6 @@ import (
 	"strconv"
 
 	"github.com/cilium/ebpf/asm"
-	devicesemulator "github.com/opencontainers/runc/libcontainer/cgroups/devices"
 	"github.com/opencontainers/runc/libcontainer/devices"
 	"github.com/pkg/errors"
 	"golang.org/x/sys/unix"
@@ -23,44 +22,11 @@ const (
 )
 
 // DeviceFilter returns eBPF device filter program and its license string
-func DeviceFilter(rules []*devices.Rule) (asm.Instructions, string, error) {
-	// Generate the minimum ruleset for the device rules we are given. While we
-	// don't care about minimum transitions in cgroupv2, using the emulator
-	// gives us a guarantee that the behaviour of devices filtering is the same
-	// as cgroupv1, including security hardenings to avoid misconfiguration
-	// (such as punching holes in wildcard rules).
-	emu := new(devicesemulator.Emulator)
-	for _, rule := range rules {
-		if err := emu.Apply(*rule); err != nil {
-			return nil, "", err
-		}
-	}
-	cleanRules, err := emu.Rules()
-	if err != nil {
-		return nil, "", err
-	}
-
-	p := &program{
-		defaultAllow: emu.IsBlacklist(),
-	}
+func DeviceFilter(devices []*devices.Rule) (asm.Instructions, string, error) {
+	p := &program{}
 	p.init()
-
-	for idx, rule := range cleanRules {
-		if rule.Type == devices.WildcardDevice {
-			// We can safely skip over wildcard entries because there should
-			// only be one (at most) at the very start to instruct cgroupv1 to
-			// go into allow-list mode. However we do double-check this here.
-			if idx != 0 || rule.Allow != emu.IsBlacklist() {
-				return nil, "", errors.Errorf("[internal error] emulated cgroupv2 devices ruleset had bad wildcard at idx %v (%s)", idx, rule.CgroupString())
-			}
-			continue
-		}
-		if rule.Allow == p.defaultAllow {
-			// There should be no rules which have an action equal to the
-			// default action, the emulator removes those.
-			return nil, "", errors.Errorf("[internal error] emulated cgroupv2 devices ruleset had no-op rule at idx %v (%s)", idx, rule.CgroupString())
-		}
-		if err := p.appendRule(rule); err != nil {
+	for i := len(devices) - 1; i >= 0; i-- {
+		if err := p.appendDevice(devices[i]); err != nil {
 			return nil, "", err
 		}
 	}
@@ -69,9 +35,9 @@ func DeviceFilter(rules []*devices.Rule) (asm.Instructions, string, error) {
 }
 
 type program struct {
-	insts        asm.Instructions
-	defaultAllow bool
-	blockID      int
+	insts       asm.Instructions
+	hasWildCard bool
+	blockID     int
 }
 
 func (p *program) init() {
@@ -101,35 +67,39 @@ func (p *program) init() {
 		asm.LoadMem(asm.R5, asm.R1, 8, asm.Word))
 }
 
-// appendRule rule converts an OCI rule to the relevant eBPF block and adds it
-// to the in-progress filter program. In order to operate properly, it must be
-// called with a "clean" rule list (generated by devices.Emulator.Rules() --
-// with any "a" rules removed).
-func (p *program) appendRule(rule *devices.Rule) error {
+// appendDevice needs to be called from the last element of OCI linux.resources.devices to the head element.
+func (p *program) appendDevice(dev *devices.Rule) error {
 	if p.blockID < 0 {
 		return errors.New("the program is finalized")
 	}
+	if p.hasWildCard {
+		// All entries after wildcard entry are ignored
+		return nil
+	}
 
-	var bpfType int32
-	switch rule.Type {
-	case devices.CharDevice:
+	bpfType := int32(-1)
+	hasType := true
+	switch dev.Type {
+	case 'c':
 		bpfType = int32(unix.BPF_DEVCG_DEV_CHAR)
-	case devices.BlockDevice:
+	case 'b':
 		bpfType = int32(unix.BPF_DEVCG_DEV_BLOCK)
+	case 'a':
+		hasType = false
 	default:
-		// We do not permit 'a', nor any other types we don't know about.
-		return errors.Errorf("invalid type %q", string(rule.Type))
+		// if not specified in OCI json, typ is set to DeviceTypeAll
+		return errors.Errorf("invalid Type %q", string(dev.Type))
 	}
-	if rule.Major > math.MaxUint32 {
-		return errors.Errorf("invalid major %d", rule.Major)
+	if dev.Major > math.MaxUint32 {
+		return errors.Errorf("invalid major %d", dev.Major)
 	}
-	if rule.Minor > math.MaxUint32 {
-		return errors.Errorf("invalid minor %d", rule.Major)
+	if dev.Minor > math.MaxUint32 {
+		return errors.Errorf("invalid minor %d", dev.Major)
 	}
-	hasMajor := rule.Major >= 0 // if not specified in OCI json, major is set to -1
-	hasMinor := rule.Minor >= 0
+	hasMajor := dev.Major >= 0 // if not specified in OCI json, major is set to -1
+	hasMinor := dev.Minor >= 0
 	bpfAccess := int32(0)
-	for _, r := range rule.Permissions {
+	for _, r := range dev.Permissions {
 		switch r {
 		case 'r':
 			bpfAccess |= unix.BPF_DEVCG_ACC_READ
@@ -149,10 +119,12 @@ func (p *program) appendRule(rule *devices.Rule) error {
 		nextBlockSym     = "block-" + strconv.Itoa(p.blockID+1)
 		prevBlockLastIdx = len(p.insts) - 1
 	)
-	p.insts = append(p.insts,
-		// if (R2 != bpfType) goto next
-		asm.JNE.Imm(asm.R2, bpfType, nextBlockSym),
-	)
+	if hasType {
+		p.insts = append(p.insts,
+			// if (R2 != bpfType) goto next
+			asm.JNE.Imm(asm.R2, bpfType, nextBlockSym),
+		)
+	}
 	if hasAccess {
 		p.insts = append(p.insts,
 			// if (R3 & bpfAccess != R3 /* use R1 as a temp var */) goto next
@@ -164,16 +136,19 @@ func (p *program) appendRule(rule *devices.Rule) error {
 	if hasMajor {
 		p.insts = append(p.insts,
 			// if (R4 != major) goto next
-			asm.JNE.Imm(asm.R4, int32(rule.Major), nextBlockSym),
+			asm.JNE.Imm(asm.R4, int32(dev.Major), nextBlockSym),
 		)
 	}
 	if hasMinor {
 		p.insts = append(p.insts,
 			// if (R5 != minor) goto next
-			asm.JNE.Imm(asm.R5, int32(rule.Minor), nextBlockSym),
+			asm.JNE.Imm(asm.R5, int32(dev.Minor), nextBlockSym),
 		)
 	}
-	p.insts = append(p.insts, acceptBlock(rule.Allow)...)
+	if !hasType && !hasAccess && !hasMajor && !hasMinor {
+		p.hasWildCard = true
+	}
+	p.insts = append(p.insts, acceptBlock(dev.Allow)...)
 	// set blockSym to the first instruction we added in this iteration
 	p.insts[prevBlockLastIdx+1] = p.insts[prevBlockLastIdx+1].Sym(blockSym)
 	p.blockID++
@@ -181,14 +156,14 @@ func (p *program) appendRule(rule *devices.Rule) error {
 }
 
 func (p *program) finalize() (asm.Instructions, error) {
-	var v int32
-	if p.defaultAllow {
-		v = 1
+	if p.hasWildCard {
+		// acceptBlock with asm.Return() is already inserted
+		return p.insts, nil
 	}
 	blockSym := "block-" + strconv.Itoa(p.blockID)
 	p.insts = append(p.insts,
-		// R0 <- v
-		asm.Mov.Imm32(asm.R0, v).Sym(blockSym),
+		// R0 <- 0
+		asm.Mov.Imm32(asm.R0, 0).Sym(blockSym),
 		asm.Return(),
 	)
 	p.blockID = -1
@@ -196,7 +171,7 @@ func (p *program) finalize() (asm.Instructions, error) {
 }
 
 func acceptBlock(accept bool) asm.Instructions {
-	var v int32
+	v := int32(0)
 	if accept {
 		v = 1
 	}
diff --git a/libcontainer/cgroups/ebpf/ebpf.go b/libcontainer/cgroups/ebpf/ebpf.go
index 6066d08c..3bee21cb 100644
--- a/libcontainer/cgroups/ebpf/ebpf.go
+++ b/libcontainer/cgroups/ebpf/ebpf.go
@@ -1,83 +1,22 @@
 package ebpf
 
 import (
-	"fmt"
-	"runtime"
-	"unsafe"
-
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/link"
 	"github.com/pkg/errors"
-	"github.com/sirupsen/logrus"
 	"golang.org/x/sys/unix"
 )
 
-func nilCloser() error {
-	return nil
-}
-
-func findAttachedCgroupDeviceFilters(dirFd int) ([]*ebpf.Program, error) {
-	type bpfAttrQuery struct {
-		TargetFd    uint32
-		AttachType  uint32
-		QueryType   uint32
-		AttachFlags uint32
-		ProgIds     uint64 // __aligned_u64
-		ProgCnt     uint32
-	}
-
-	// Currently you can only have 64 eBPF programs attached to a cgroup.
-	size := 64
-	retries := 0
-	for retries < 10 {
-		progIds := make([]uint32, size)
-		query := bpfAttrQuery{
-			TargetFd:   uint32(dirFd),
-			AttachType: uint32(unix.BPF_CGROUP_DEVICE),
-			ProgIds:    uint64(uintptr(unsafe.Pointer(&progIds[0]))),
-			ProgCnt:    uint32(len(progIds)),
-		}
-
-		// Fetch the list of program ids.
-		_, _, errno := unix.Syscall(unix.SYS_BPF,
-			uintptr(unix.BPF_PROG_QUERY),
-			uintptr(unsafe.Pointer(&query)),
-			unsafe.Sizeof(query))
-		size = int(query.ProgCnt)
-		runtime.KeepAlive(query)
-		if errno != 0 {
-			// On ENOSPC we get the correct number of programs.
-			if errno == unix.ENOSPC {
-				retries++
-				continue
-			}
-			return nil, fmt.Errorf("bpf_prog_query(BPF_CGROUP_DEVICE) failed: %w", errno)
-		}
-
-		// Convert the ids to program handles.
-		progIds = progIds[:size]
-		programs := make([]*ebpf.Program, len(progIds))
-		for idx, progId := range progIds {
-			program, err := ebpf.NewProgramFromID(ebpf.ProgramID(progId))
-			if err != nil {
-				return nil, fmt.Errorf("cannot fetch program from id: %w", err)
-			}
-			programs[idx] = program
-		}
-		runtime.KeepAlive(progIds)
-		return programs, nil
-	}
-
-	return nil, errors.New("could not get complete list of CGROUP_DEVICE programs")
-}
-
 // LoadAttachCgroupDeviceFilter installs eBPF device filter program to /sys/fs/cgroup/<foo> directory.
 //
 // Requires the system to be running in cgroup2 unified-mode with kernel >= 4.15 .
 //
 // https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92
-func LoadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd int) (func() error, error) {
+func LoadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFD int) (func() error, error) {
+	nilCloser := func() error {
+		return nil
+	}
 	// Increase `ulimit -l` limit to avoid BPF_PROG_LOAD error (#2167).
 	// This limit is not inherited into the container.
 	memlockLimit := &unix.Rlimit{
@@ -85,11 +24,6 @@ func LoadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd
 		Max: unix.RLIM_INFINITY,
 	}
 	_ = unix.Setrlimit(unix.RLIMIT_MEMLOCK, memlockLimit)
-	// Get the list of existing programs.
-	oldProgs, err := findAttachedCgroupDeviceFilters(dirFd)
-	if err != nil {
-		return nilCloser, err
-	}
 	spec := &ebpf.ProgramSpec{
 		Type:         ebpf.CGroupDevice,
 		Instructions: insts,
@@ -99,49 +33,25 @@ func LoadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd
 	if err != nil {
 		return nilCloser, err
 	}
-	// If there is only one old program, we can just replace it directly.
-	var replaceProg *ebpf.Program
-	if len(oldProgs) == 1 {
-		replaceProg = oldProgs[0]
-	}
 	err = link.RawAttachProgram(link.RawAttachProgramOptions{
-		Target:  dirFd,
+		Target:  dirFD,
 		Program: prog,
-		Replace: replaceProg,
 		Attach:  ebpf.AttachCGroupDevice,
 		Flags:   unix.BPF_F_ALLOW_MULTI,
 	})
 	if err != nil {
-		return nilCloser, fmt.Errorf("failed to call BPF_PROG_ATTACH (BPF_CGROUP_DEVICE, BPF_F_ALLOW_MULTI): %w", err)
+		return nilCloser, errors.Wrap(err, "failed to call BPF_PROG_ATTACH (BPF_CGROUP_DEVICE, BPF_F_ALLOW_MULTI)")
 	}
 	closer := func() error {
 		err = link.RawDetachProgram(link.RawDetachProgramOptions{
-			Target:  dirFd,
+			Target:  dirFD,
 			Program: prog,
 			Attach:  ebpf.AttachCGroupDevice,
 		})
 		if err != nil {
-			return fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE): %w", err)
+			return errors.Wrap(err, "failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE)")
 		}
-		// TODO: Should we attach the old filters back in this case? Otherwise
-		//       we fail-open on a security feature, which is a bit scary.
 		return nil
 	}
-	// If there was more than one old program, give a warning (since this
-	// really shouldn't happen with runc-managed cgroups) and then detach all
-	// the old programs.
-	if len(oldProgs) > 1 {
-		logrus.Warnf("found more than one filter (%d) attached to a cgroup -- removing extra filters!", len(oldProgs))
-		for _, oldProg := range oldProgs {
-			err = link.RawDetachProgram(link.RawDetachProgramOptions{
-				Target:  dirFd,
-				Program: oldProg,
-				Attach:  ebpf.AttachCGroupDevice,
-			})
-			if err != nil {
-				return closer, fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE) on old filter program: %w", err)
-			}
-		}
-	}
 	return closer, nil
 }
diff --git a/libcontainer/cgroups/fs/memory.go b/libcontainer/cgroups/fs/memory.go
index 18641bf8..dc27cb9e 100644
--- a/libcontainer/cgroups/fs/memory.go
+++ b/libcontainer/cgroups/fs/memory.go
@@ -200,6 +200,14 @@ func (s *MemoryGroup) GetStats(path string, stats *cgroups.Stats) error {
 	return nil
 }
 
+func memoryAssigned(cgroup *configs.Cgroup) bool {
+	return cgroup.Resources.Memory != 0 ||
+		cgroup.Resources.MemoryReservation != 0 ||
+		cgroup.Resources.MemorySwap > 0 ||
+		cgroup.Resources.OomKillDisable ||
+		(cgroup.Resources.MemorySwappiness != nil && int64(*cgroup.Resources.MemorySwappiness) != -1)
+}
+
 func getMemoryData(path, name string) (cgroups.MemoryData, error) {
 	memoryData := cgroups.MemoryData{}
 
diff --git a/libcontainer/cgroups/fs2/devices.go b/libcontainer/cgroups/fs2/devices.go
index 6b4b73a1..fa9194b5 100644
--- a/libcontainer/cgroups/fs2/devices.go
+++ b/libcontainer/cgroups/fs2/devices.go
@@ -58,6 +58,9 @@ func setDevices(dirPath string, r *configs.Resources) error {
 	if r.SkipDevices {
 		return nil
 	}
+	// XXX: This is currently a white-list (but all callers pass a blacklist of
+	//      devices). This is bad for a whole variety of reasons, but will need
+	//      to be fixed with co-ordinated effort with downstreams.
 	insts, license, err := devicefilter.DeviceFilter(r.Devices)
 	if err != nil {
 		return err
@@ -67,6 +70,17 @@ func setDevices(dirPath string, r *configs.Resources) error {
 		return errors.Errorf("cannot get dir FD for %s", dirPath)
 	}
 	defer unix.Close(dirFD)
+	// XXX: This code is currently incorrect when it comes to updating an
+	//      existing cgroup with new rules (new rulesets are just appended to
+	//      the program list because this uses BPF_F_ALLOW_MULTI). If we didn't
+	//      use BPF_F_ALLOW_MULTI we could actually atomically swap the
+	//      programs.
+	//
+	//      The real issue is that BPF_F_ALLOW_MULTI makes it hard to have a
+	//      race-free blacklist because it acts as a whitelist by default, and
+	//      having a deny-everything program cannot be overridden by other
+	//      programs. You could temporarily insert a deny-everything program
+	//      but that would result in spurrious failures during updates.
 	if _, err := ebpf.LoadAttachCgroupDeviceFilter(insts, license, dirFD); err != nil {
 		if !canSkipEBPFError(r) {
 			return err
diff --git a/libcontainer/cgroups/systemd/common.go b/libcontainer/cgroups/systemd/common.go
index e1733945..91c314e0 100644
--- a/libcontainer/cgroups/systemd/common.go
+++ b/libcontainer/cgroups/systemd/common.go
@@ -203,7 +203,8 @@ func generateDeviceProperties(rules []*devices.Rule) ([]systemdDbus.Property, er
 	// Now generate the set of rules we actually need to apply. Unlike the
 	// normal devices cgroup, in "strict" mode systemd defaults to a deny-all
 	// whitelist which is the default for devices.Emulator.
-	finalRules, err := configEmu.Rules()
+	baseEmu := &cgroupdevices.Emulator{}
+	finalRules, err := baseEmu.Transition(configEmu)
 	if err != nil {
 		return nil, errors.Wrap(err, "get simplified rules for systemd")
 	}
@@ -475,7 +476,7 @@ func addCpuset(cm *dbusConnManager, props *[]systemdDbus.Property, cpus, mems st
 	}
 
 	if cpus != "" {
-		bits, err := RangeToBits(cpus)
+		bits, err := rangeToBits(cpus)
 		if err != nil {
 			return fmt.Errorf("resources.CPU.Cpus=%q conversion error: %w",
 				cpus, err)
@@ -484,7 +485,7 @@ func addCpuset(cm *dbusConnManager, props *[]systemdDbus.Property, cpus, mems st
 			newProp("AllowedCPUs", bits))
 	}
 	if mems != "" {
-		bits, err := RangeToBits(mems)
+		bits, err := rangeToBits(mems)
 		if err != nil {
 			return fmt.Errorf("resources.CPU.Mems=%q conversion error: %w",
 				mems, err)
diff --git a/libcontainer/cgroups/systemd/cpuset.go b/libcontainer/cgroups/systemd/cpuset.go
index 264f4c89..07098218 100644
--- a/libcontainer/cgroups/systemd/cpuset.go
+++ b/libcontainer/cgroups/systemd/cpuset.go
@@ -9,11 +9,11 @@ import (
 	"github.com/willf/bitset"
 )
 
-// RangeToBits converts a text representation of a CPU mask (as written to
+// rangeToBits converts a text representation of a CPU mask (as written to
 // or read from cgroups' cpuset.* files, e.g. "1,3-5") to a slice of bytes
 // with the corresponding bits set (as consumed by systemd over dbus as
 // AllowedCPUs/AllowedMemoryNodes unit property value).
-func RangeToBits(str string) ([]byte, error) {
+func rangeToBits(str string) ([]byte, error) {
 	bits := &bitset.BitSet{}
 
 	for _, r := range strings.Split(str, ",") {
diff --git a/libcontainer/cgroups/systemd/v2.go b/libcontainer/cgroups/systemd/v2.go
index a7a2264c..8abb0feb 100644
--- a/libcontainer/cgroups/systemd/v2.go
+++ b/libcontainer/cgroups/systemd/v2.go
@@ -96,7 +96,7 @@ func unifiedResToSystemdProps(cm *dbusConnManager, res map[string]string) (props
 				newProp("CPUWeight", num))
 
 		case "cpuset.cpus", "cpuset.mems":
-			bits, err := RangeToBits(v)
+			bits, err := rangeToBits(v)
 			if err != nil {
 				return nil, fmt.Errorf("unified resource %q=%q conversion error: %w", k, v, err)
 			}
diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go
index c6b259e9..14a09603 100644
--- a/libcontainer/configs/config.go
+++ b/libcontainer/configs/config.go
@@ -387,7 +387,7 @@ func (c Command) Run(s *specs.State) error {
 	case err := <-errC:
 		return err
 	case <-timerCh:
-		_ = cmd.Process.Kill()
+		cmd.Process.Kill()
 		<-errC
 		return fmt.Errorf("hook ran past specified timeout of %.1fs", c.Timeout.Seconds())
 	}
diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go
index 5b65f889..849bf4a6 100644
--- a/libcontainer/container_linux.go
+++ b/libcontainer/container_linux.go
@@ -1352,7 +1352,7 @@ func (c *linuxContainer) Restore(process *Process, criuOpts *CriuOpts) error {
 	if err != nil {
 		return err
 	}
-	defer unix.Unmount(root, unix.MNT_DETACH) //nolint: errcheck
+	defer unix.Unmount(root, unix.MNT_DETACH)
 	t := criurpc.CriuReqType_RESTORE
 	req := &criurpc.CriuReq{
 		Type: &t,
@@ -1665,7 +1665,7 @@ func (c *linuxContainer) criuSwrk(process *Process, req *criurpc.CriuReq, opts *
 		break
 	}
 
-	_ = criuClientCon.CloseWrite()
+	criuClientCon.CloseWrite()
 	// cmd.Wait() waits cmd.goroutines which are used for proxying file descriptors.
 	// Here we want to wait only the CRIU process.
 	criuProcessState, err = criuProcess.Wait()
diff --git a/libcontainer/factory_linux.go b/libcontainer/factory_linux.go
index b3beba81..972b0f2b 100644
--- a/libcontainer/factory_linux.go
+++ b/libcontainer/factory_linux.go
@@ -196,11 +196,7 @@ func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
 		Validator: validate.New(),
 		CriuPath:  "criu",
 	}
-
-	if err := Cgroupfs(l); err != nil {
-		return nil, err
-	}
-
+	Cgroupfs(l)
 	for _, opt := range options {
 		if opt == nil {
 			continue
diff --git a/libcontainer/process_linux.go b/libcontainer/process_linux.go
index 80f25e33..053971b9 100644
--- a/libcontainer/process_linux.go
+++ b/libcontainer/process_linux.go
@@ -188,7 +188,7 @@ func (p *setnsProcess) start() (retErr error) {
 	}
 	// Must be done after Shutdown so the child will exit and we can wait for it.
 	if ierr != nil {
-		_, _ = p.wait()
+		p.wait()
 		return ierr
 	}
 	return nil
@@ -201,16 +201,16 @@ func (p *setnsProcess) start() (retErr error) {
 func (p *setnsProcess) execSetns() error {
 	status, err := p.cmd.Process.Wait()
 	if err != nil {
-		_ = p.cmd.Wait()
+		p.cmd.Wait()
 		return newSystemErrorWithCause(err, "waiting on setns process to finish")
 	}
 	if !status.Success() {
-		_ = p.cmd.Wait()
+		p.cmd.Wait()
 		return newSystemError(&exec.ExitError{ProcessState: status})
 	}
 	var pid *pid
 	if err := json.NewDecoder(p.messageSockPair.parent).Decode(&pid); err != nil {
-		_ = p.cmd.Wait()
+		p.cmd.Wait()
 		return newSystemErrorWithCause(err, "reading pid from init pipe")
 	}
 
@@ -292,7 +292,7 @@ func (p *initProcess) externalDescriptors() []string {
 func (p *initProcess) getChildPid() (int, error) {
 	var pid pid
 	if err := json.NewDecoder(p.messageSockPair.parent).Decode(&pid); err != nil {
-		_ = p.cmd.Wait()
+		p.cmd.Wait()
 		return -1, err
 	}
 
@@ -309,11 +309,11 @@ func (p *initProcess) getChildPid() (int, error) {
 func (p *initProcess) waitForChildExit(childPid int) error {
 	status, err := p.cmd.Process.Wait()
 	if err != nil {
-		_ = p.cmd.Wait()
+		p.cmd.Wait()
 		return err
 	}
 	if !status.Success() {
-		_ = p.cmd.Wait()
+		p.cmd.Wait()
 		return &exec.ExitError{ProcessState: status}
 	}
 
@@ -327,12 +327,12 @@ func (p *initProcess) waitForChildExit(childPid int) error {
 }
 
 func (p *initProcess) start() (retErr error) {
-	defer p.messageSockPair.parent.Close() //nolint: errcheck
+	defer p.messageSockPair.parent.Close()
 	err := p.cmd.Start()
 	p.process.ops = p
 	// close the write-side of the pipes (controlled by child)
-	_ = p.messageSockPair.child.Close()
-	_ = p.logFilePair.child.Close()
+	p.messageSockPair.child.Close()
+	p.logFilePair.child.Close()
 	if err != nil {
 		p.process.ops = nil
 		return newSystemErrorWithCause(err, "starting init process command")
@@ -371,9 +371,9 @@ func (p *initProcess) start() (retErr error) {
 				logrus.WithError(err).Warn("unable to terminate initProcess")
 			}
 
-			_ = p.manager.Destroy()
+			p.manager.Destroy()
 			if p.intelRdtManager != nil {
-				_ = p.intelRdtManager.Destroy()
+				p.intelRdtManager.Destroy()
 			}
 		}
 	}()
@@ -553,7 +553,7 @@ func (p *initProcess) start() (retErr error) {
 
 	// Must be done after Shutdown so the child will exit and we can wait for it.
 	if ierr != nil {
-		_, _ = p.wait()
+		p.wait()
 		return ierr
 	}
 	return nil
@@ -563,7 +563,7 @@ func (p *initProcess) wait() (*os.ProcessState, error) {
 	err := p.cmd.Wait()
 	// we should kill all processes in cgroup when init is died if we use host PID namespace
 	if p.sharePidns {
-		_ = signalAllProcesses(p.manager, unix.SIGKILL)
+		signalAllProcesses(p.manager, unix.SIGKILL)
 	}
 	return p.cmd.ProcessState, err
 }
@@ -668,7 +668,7 @@ func (p *Process) InitializeIO(rootuid, rootgid int) (i *IO, err error) {
 	defer func() {
 		if err != nil {
 			for _, fd := range fds {
-				_ = unix.Close(int(fd))
+				unix.Close(int(fd))
 			}
 		}
 	}()
diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go
index ff91b3db..d9c5146d 100644
--- a/libcontainer/rootfs_linux.go
+++ b/libcontainer/rootfs_linux.go
@@ -196,9 +196,9 @@ func prepareTmp(topTmpDir string) (string, error) {
 	return tmpdir, nil
 }
 
-func cleanupTmp(tmpdir string) {
-	_ = unix.Unmount(tmpdir, 0)
-	_ = os.RemoveAll(tmpdir)
+func cleanupTmp(tmpdir string) error {
+	unix.Unmount(tmpdir, 0)
+	return os.RemoveAll(tmpdir)
 }
 
 func mountCmd(cmd configs.Command) error {
@@ -615,7 +615,7 @@ func reOpenDevNull() error {
 	if err != nil {
 		return fmt.Errorf("Failed to open /dev/null - %s", err)
 	}
-	defer file.Close() //nolint: errcheck
+	defer file.Close()
 	if err := unix.Fstat(int(file.Fd()), &devNullStat); err != nil {
 		return err
 	}
@@ -661,7 +661,7 @@ func bindMountDeviceNode(rootfs, dest string, node *devices.Device) error {
 		return err
 	}
 	if f != nil {
-		_ = f.Close()
+		f.Close()
 	}
 	return utils.WithProcfd(rootfs, dest, func(procfd string) error {
 		return unix.Mount(node.Path, procfd, "bind", unix.MS_BIND, "")
@@ -826,13 +826,13 @@ func pivotRoot(rootfs string) error {
 	if err != nil {
 		return err
 	}
-	defer unix.Close(oldroot) //nolint: errcheck
+	defer unix.Close(oldroot)
 
 	newroot, err := unix.Open(rootfs, unix.O_DIRECTORY|unix.O_RDONLY, 0)
 	if err != nil {
 		return err
 	}
-	defer unix.Close(newroot) //nolint: errcheck
+	defer unix.Close(newroot)
 
 	// Change to the new root so that the pivot_root actually acts on it.
 	if err := unix.Fchdir(newroot); err != nil {
@@ -956,7 +956,7 @@ func createIfNotExists(path string, isDir bool) error {
 			if err != nil {
 				return err
 			}
-			_ = f.Close()
+			f.Close()
 		}
 	}
 	return nil
diff --git a/libcontainer/setns_init_linux.go b/libcontainer/setns_init_linux.go
index 2d7e5814..97987f1d 100644
--- a/libcontainer/setns_init_linux.go
+++ b/libcontainer/setns_init_linux.go
@@ -37,7 +37,7 @@ func (l *linuxSetnsInit) Init() error {
 		if err := selinux.SetKeyLabel(l.config.ProcessLabel); err != nil {
 			return err
 		}
-		defer selinux.SetKeyLabel("") //nolint: errcheck
+		defer selinux.SetKeyLabel("")
 		// Do not inherit the parent's session keyring.
 		if _, err := keys.JoinSessionKeyring(l.getSessionRingName()); err != nil {
 			// Same justification as in standart_init_linux.go as to why we
@@ -65,7 +65,7 @@ func (l *linuxSetnsInit) Init() error {
 	if err := selinux.SetExecLabel(l.config.ProcessLabel); err != nil {
 		return err
 	}
-	defer selinux.SetExecLabel("") //nolint: errcheck
+	defer selinux.SetExecLabel("")
 	// Without NoNewPrivileges seccomp is a privileged operation, so we need to
 	// do this before dropping capabilities; otherwise do it as late as possible
 	// just before execve so as few syscalls take place after it as possible.
diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go
index 45c6d66c..d77022ad 100644
--- a/libcontainer/standard_init_linux.go
+++ b/libcontainer/standard_init_linux.go
@@ -52,7 +52,7 @@ func (l *linuxStandardInit) Init() error {
 		if err := selinux.SetKeyLabel(l.config.ProcessLabel); err != nil {
 			return err
 		}
-		defer selinux.SetKeyLabel("") //nolint: errcheck
+		defer selinux.SetKeyLabel("")
 		ringname, keepperms, newperms := l.getSessionRingParams()
 
 		// Do not inherit the parent's session keyring.
@@ -151,7 +151,7 @@ func (l *linuxStandardInit) Init() error {
 	if err := selinux.SetExecLabel(l.config.ProcessLabel); err != nil {
 		return errors.Wrap(err, "set process label")
 	}
-	defer selinux.SetExecLabel("") //nolint: errcheck
+	defer selinux.SetExecLabel("")
 	// Without NoNewPrivileges seccomp is a privileged operation, so we need to
 	// do this before dropping capabilities; otherwise do it as late as possible
 	// just before execve so as few syscalls take place after it as possible.
@@ -183,7 +183,7 @@ func (l *linuxStandardInit) Init() error {
 	}
 	// Close the pipe to signal that we have completed our init.
 	logrus.Debugf("init: closing the pipe to signal completion")
-	_ = l.pipe.Close()
+	l.pipe.Close()
 
 	// Close the log pipe fd so the parent's ForwardLogs can exit.
 	if err := unix.Close(l.logFd); err != nil {
@@ -207,7 +207,7 @@ func (l *linuxStandardInit) Init() error {
 	// N.B. the core issue itself (passing dirfds to the host filesystem) has
 	// since been resolved.
 	// https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318
-	_ = unix.Close(l.fifoFd)
+	unix.Close(l.fifoFd)
 	// Set seccomp as close to execve as possible, so as few syscalls take
 	// place afterward (reducing the amount of syscalls that users need to
 	// enable in their seccomp profiles).
diff --git a/libcontainer/state_linux.go b/libcontainer/state_linux.go
index 43c040c8..02ff06ea 100644
--- a/libcontainer/state_linux.go
+++ b/libcontainer/state_linux.go
@@ -157,7 +157,7 @@ func (i *createdState) transition(s containerState) error {
 }
 
 func (i *createdState) destroy() error {
-	_ = i.c.initProcess.signal(unix.SIGKILL)
+	i.c.initProcess.signal(unix.SIGKILL)
 	return destroy(i.c)
 }
 
diff --git a/libcontainer/system/proc.go b/libcontainer/system/proc.go
index d0407cfe..b73cf70b 100644
--- a/libcontainer/system/proc.go
+++ b/libcontainer/system/proc.go
@@ -96,7 +96,7 @@ func parseStat(data string) (stat Stat_t, err error) {
 	// one (PID) and two (Name) in the paren-split.
 	parts = strings.Split(data[i+2:], " ")
 	var state int
-	fmt.Sscanf(parts[3-3], "%c", &state) //nolint:staticcheck // "3-3" is more readable in this context.
+	fmt.Sscanf(parts[3-3], "%c", &state)
 	stat.State = State(state)
 	fmt.Sscanf(parts[22-3], "%d", &stat.StartTime)
 	return stat, nil
diff --git a/main.go b/main.go
index 328ea30c..ba5e3c82 100644
--- a/main.go
+++ b/main.go
@@ -15,9 +15,9 @@ import (
 	"github.com/urfave/cli"
 )
 
-// version will be populated by the Makefile, read from
-// VERSION file of the source code.
-var version = ""
+// version must be set from the contents of VERSION file by go build's
+// -X main.version= option in the Makefile.
+var version = "unknown"
 
 // gitCommit will be the hash that the binary was built from
 // and will be populated by the Makefile
@@ -55,10 +55,8 @@ func main() {
 	app.Name = "runc"
 	app.Usage = usage
 
-	var v []string
-	if version != "" {
-		v = append(v, version)
-	}
+	v := []string{version}
+
 	if gitCommit != "" {
 		v = append(v, "commit: "+gitCommit)
 	}
diff --git a/signals.go b/signals.go
index 73d8de0f..5682989c 100644
--- a/signals.go
+++ b/signals.go
@@ -66,11 +66,11 @@ func (h *signalHandler) forward(process *libcontainer.Process, tty *tty, detach
 
 	if h.notifySocket != nil {
 		if detach {
-			_ = h.notifySocket.run(pid1)
+			h.notifySocket.run(pid1)
 			return 0, nil
 		}
-		_ = h.notifySocket.run(os.Getpid())
-		go func() { _ = h.notifySocket.run(0) }()
+		h.notifySocket.run(os.Getpid())
+		go h.notifySocket.run(0)
 	}
 
 	// Perform the initial tty resize. Always ignore errors resizing because
@@ -96,7 +96,7 @@ func (h *signalHandler) forward(process *libcontainer.Process, tty *tty, detach
 					// call Wait() on the process even though we already have the exit
 					// status because we must ensure that any of the go specific process
 					// fun such as flushing pipes are complete before we return.
-					_, _ = process.Wait()
+					process.Wait()
 					return e.status, nil
 				}
 			}
diff --git a/tty.go b/tty.go
index 8ca193aa..844d7f84 100644
--- a/tty.go
+++ b/tty.go
@@ -27,8 +27,8 @@ type tty struct {
 
 func (t *tty) copyIO(w io.Writer, r io.ReadCloser) {
 	defer t.wg.Done()
-	_, _ = io.Copy(w, r)
-	_ = r.Close()
+	io.Copy(w, r)
+	r.Close()
 }
 
 // setup pipes for the process so that advanced features like c/r are able to easily checkpoint
@@ -56,8 +56,8 @@ func setupProcessPipes(p *libcontainer.Process, rootuid, rootgid int) (*tty, err
 		}
 	}
 	go func() {
-		_, _ = io.Copy(i.Stdin, os.Stdin)
-		_ = i.Stdin.Close()
+		io.Copy(i.Stdin, os.Stdin)
+		i.Stdin.Close()
 	}()
 	t.wg.Add(2)
 	go t.copyIO(os.Stdout, i.Stdout)
@@ -126,11 +126,11 @@ func (t *tty) recvtty(process *libcontainer.Process, socket *os.File) (Err error
 	}
 	defer func() {
 		if Err != nil {
-			_ = epollConsole.Close()
+			epollConsole.Close()
 		}
 	}()
-	go func() { _ = epoller.Wait() }()
-	go func() { _, _ = io.Copy(epollConsole, os.Stdin) }()
+	go epoller.Wait()
+	go io.Copy(epollConsole, os.Stdin)
 	t.wg.Add(1)
 	go t.copyIO(os.Stdout, epollConsole)
 
@@ -150,7 +150,7 @@ func handleInterrupt(c console.Console) {
 	sigchan := make(chan os.Signal, 1)
 	signal.Notify(sigchan, os.Interrupt)
 	<-sigchan
-	_ = c.Reset()
+	c.Reset()
 	os.Exit(0)
 }
 
@@ -165,7 +165,7 @@ func (t *tty) waitConsole() error {
 // so that we no longer have copy in our process.
 func (t *tty) ClosePostStart() error {
 	for _, c := range t.postStart {
-		_ = c.Close()
+		c.Close()
 	}
 	return nil
 }
@@ -175,19 +175,19 @@ func (t *tty) ClosePostStart() error {
 func (t *tty) Close() error {
 	// ensure that our side of the fds are always closed
 	for _, c := range t.postStart {
-		_ = c.Close()
+		c.Close()
 	}
 	// the process is gone at this point, shutting down the console if we have
 	// one and wait for all IO to be finished
 	if t.console != nil && t.epoller != nil {
-		_ = t.console.Shutdown(t.epoller.CloseConsole)
+		t.console.Shutdown(t.epoller.CloseConsole)
 	}
 	t.wg.Wait()
 	for _, c := range t.closers {
-		_ = c.Close()
+		c.Close()
 	}
 	if t.hostConsole != nil {
-		_ = t.hostConsole.Reset()
+		t.hostConsole.Reset()
 	}
 	return nil
 }
diff --git a/utils.go b/utils.go
index f3244a49..85413c29 100644
--- a/utils.go
+++ b/utils.go
@@ -39,7 +39,7 @@ func checkArgs(context *cli.Context, expected, checkType int) error {
 
 	if err != nil {
 		fmt.Printf("Incorrect Usage.\n\n")
-		_ = cli.ShowCommandHelp(context, cmdName)
+		cli.ShowCommandHelp(context, cmdName)
 		return err
 	}
 	return nil


Test patch:
diff --git a/libcontainer/cgroups/ebpf/devicefilter/devicefilter_test.go b/libcontainer/cgroups/ebpf/devicefilter/devicefilter_test.go
index a8fc562d..cfcaa203 100644
--- a/libcontainer/cgroups/ebpf/devicefilter/devicefilter_test.go
+++ b/libcontainer/cgroups/ebpf/devicefilter/devicefilter_test.go
@@ -55,81 +55,81 @@ block-0:
 func TestDeviceFilter_BuiltInAllowList(t *testing.T) {
 	expected := `
 // load parameters into registers
-        0: LdXMemW dst: r2 src: r1 off: 0 imm: 0
-        1: And32Imm dst: r2 imm: 65535
-        2: LdXMemW dst: r3 src: r1 off: 0 imm: 0
-        3: RSh32Imm dst: r3 imm: 16
-        4: LdXMemW dst: r4 src: r1 off: 4 imm: 0
-        5: LdXMemW dst: r5 src: r1 off: 8 imm: 0
+         0: LdXMemW dst: r2 src: r1 off: 0 imm: 0
+         1: And32Imm dst: r2 imm: 65535
+         2: LdXMemW dst: r3 src: r1 off: 0 imm: 0
+         3: RSh32Imm dst: r3 imm: 16
+         4: LdXMemW dst: r4 src: r1 off: 4 imm: 0
+         5: LdXMemW dst: r5 src: r1 off: 8 imm: 0
 block-0:
-// (b, wildcard, wildcard, m, true)
-        6: JNEImm dst: r2 off: -1 imm: 1 <block-1>
-        7: Mov32Reg dst: r1 src: r3
-        8: And32Imm dst: r1 imm: 1
-        9: JNEReg dst: r1 off: -1 src: r3 <block-1>
-        10: Mov32Imm dst: r0 imm: 1
-        11: Exit
+// tuntap (c, 10, 200, rwm, allow)
+         6: JNEImm dst: r2 off: -1 imm: 2 <block-1>
+         7: JNEImm dst: r4 off: -1 imm: 10 <block-1>
+         8: JNEImm dst: r5 off: -1 imm: 200 <block-1>
+         9: Mov32Imm dst: r0 imm: 1
+        10: Exit
 block-1:
-// (c, wildcard, wildcard, m, true)
-        12: JNEImm dst: r2 off: -1 imm: 2 <block-2>
-        13: Mov32Reg dst: r1 src: r3
-        14: And32Imm dst: r1 imm: 1
-        15: JNEReg dst: r1 off: -1 src: r3 <block-2>
-        16: Mov32Imm dst: r0 imm: 1
-        17: Exit
+        11: JNEImm dst: r2 off: -1 imm: 2 <block-2>
+        12: JNEImm dst: r4 off: -1 imm: 5 <block-2>
+        13: JNEImm dst: r5 off: -1 imm: 2 <block-2>
+        14: Mov32Imm dst: r0 imm: 1
+        15: Exit
 block-2:
-        18: JNEImm dst: r2 off: -1 imm: 2 <block-3>
-        19: JNEImm dst: r4 off: -1 imm: 1 <block-3>
-        20: JNEImm dst: r5 off: -1 imm: 3 <block-3>
-        21: Mov32Imm dst: r0 imm: 1
-        22: Exit
+// /dev/pts (c, 136, wildcard, rwm, true)
+        16: JNEImm dst: r2 off: -1 imm: 2 <block-3>
+        17: JNEImm dst: r4 off: -1 imm: 136 <block-3>
+        18: Mov32Imm dst: r0 imm: 1
+        19: Exit
 block-3:
-        23: JNEImm dst: r2 off: -1 imm: 2 <block-4>
-        24: JNEImm dst: r4 off: -1 imm: 1 <block-4>
-        25: JNEImm dst: r5 off: -1 imm: 5 <block-4>
-        26: Mov32Imm dst: r0 imm: 1
-        27: Exit
+        20: JNEImm dst: r2 off: -1 imm: 2 <block-4>
+        21: JNEImm dst: r4 off: -1 imm: 1 <block-4>
+        22: JNEImm dst: r5 off: -1 imm: 9 <block-4>
+        23: Mov32Imm dst: r0 imm: 1
+        24: Exit
 block-4:
-        28: JNEImm dst: r2 off: -1 imm: 2 <block-5>
-        29: JNEImm dst: r4 off: -1 imm: 1 <block-5>
-        30: JNEImm dst: r5 off: -1 imm: 7 <block-5>
-        31: Mov32Imm dst: r0 imm: 1
-        32: Exit
+        25: JNEImm dst: r2 off: -1 imm: 2 <block-5>
+        26: JNEImm dst: r4 off: -1 imm: 1 <block-5>
+        27: JNEImm dst: r5 off: -1 imm: 5 <block-5>
+        28: Mov32Imm dst: r0 imm: 1
+        29: Exit
 block-5:
-        33: JNEImm dst: r2 off: -1 imm: 2 <block-6>
-        34: JNEImm dst: r4 off: -1 imm: 1 <block-6>
-        35: JNEImm dst: r5 off: -1 imm: 8 <block-6>
-        36: Mov32Imm dst: r0 imm: 1
-        37: Exit
+        30: JNEImm dst: r2 off: -1 imm: 2 <block-6>
+        31: JNEImm dst: r4 off: -1 imm: 5 <block-6>
+        32: JNEImm dst: r5 off: -1 imm: 0 <block-6>
+        33: Mov32Imm dst: r0 imm: 1
+        34: Exit
 block-6:
-        38: JNEImm dst: r2 off: -1 imm: 2 <block-7>
-        39: JNEImm dst: r4 off: -1 imm: 1 <block-7>
-        40: JNEImm dst: r5 off: -1 imm: 9 <block-7>
-        41: Mov32Imm dst: r0 imm: 1
-        42: Exit
+        35: JNEImm dst: r2 off: -1 imm: 2 <block-7>
+        36: JNEImm dst: r4 off: -1 imm: 1 <block-7>
+        37: JNEImm dst: r5 off: -1 imm: 7 <block-7>
+        38: Mov32Imm dst: r0 imm: 1
+        39: Exit
 block-7:
-        43: JNEImm dst: r2 off: -1 imm: 2 <block-8>
-        44: JNEImm dst: r4 off: -1 imm: 5 <block-8>
-        45: JNEImm dst: r5 off: -1 imm: 0 <block-8>
-        46: Mov32Imm dst: r0 imm: 1
-        47: Exit
+        40: JNEImm dst: r2 off: -1 imm: 2 <block-8>
+        41: JNEImm dst: r4 off: -1 imm: 1 <block-8>
+        42: JNEImm dst: r5 off: -1 imm: 8 <block-8>
+        43: Mov32Imm dst: r0 imm: 1
+        44: Exit
 block-8:
-        48: JNEImm dst: r2 off: -1 imm: 2 <block-9>
-        49: JNEImm dst: r4 off: -1 imm: 5 <block-9>
-        50: JNEImm dst: r5 off: -1 imm: 2 <block-9>
-        51: Mov32Imm dst: r0 imm: 1
-        52: Exit
+        45: JNEImm dst: r2 off: -1 imm: 2 <block-9>
+        46: JNEImm dst: r4 off: -1 imm: 1 <block-9>
+        47: JNEImm dst: r5 off: -1 imm: 3 <block-9>
+        48: Mov32Imm dst: r0 imm: 1
+        49: Exit
 block-9:
-// tuntap (c, 10, 200, rwm, allow)
-        53: JNEImm dst: r2 off: -1 imm: 2 <block-10>
-        54: JNEImm dst: r4 off: -1 imm: 10 <block-10>
-        55: JNEImm dst: r5 off: -1 imm: 200 <block-10>
-        56: Mov32Imm dst: r0 imm: 1
-        57: Exit
+// (b, wildcard, wildcard, m, true)
+        50: JNEImm dst: r2 off: -1 imm: 1 <block-10>
+        51: Mov32Reg dst: r1 src: r3
+        52: And32Imm dst: r1 imm: 1
+        53: JNEReg dst: r1 off: -1 src: r3 <block-10>
+        54: Mov32Imm dst: r0 imm: 1
+        55: Exit
 block-10:
-// /dev/pts (c, 136, wildcard, rwm, true)
-        58: JNEImm dst: r2 off: -1 imm: 2 <block-11>
-        59: JNEImm dst: r4 off: -1 imm: 136 <block-11>
+// (c, wildcard, wildcard, m, true)
+        56: JNEImm dst: r2 off: -1 imm: 2 <block-11>
+        57: Mov32Reg dst: r1 src: r3
+        58: And32Imm dst: r1 imm: 1
+        59: JNEReg dst: r1 off: -1 src: r3 <block-11>
         60: Mov32Imm dst: r0 imm: 1
         61: Exit
 block-11:
diff --git a/libcontainer/cgroups/systemd/cpuset_test.go b/libcontainer/cgroups/systemd/cpuset_test.go
index 3030cba9..7dda1e3c 100644
--- a/libcontainer/cgroups/systemd/cpuset_test.go
+++ b/libcontainer/cgroups/systemd/cpuset_test.go
@@ -40,7 +40,7 @@ func TestRangeToBits(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		out, err := RangeToBits(tc.in)
+		out, err := rangeToBits(tc.in)
 		if err != nil {
 			if !tc.isErr {
 				t.Errorf("case %q: unexpected error: %v", tc.in, err)
diff --git a/libcontainer/configs/config_test.go b/libcontainer/configs/config_test.go
index a270f378..97286740 100644
--- a/libcontainer/configs/config_test.go
+++ b/libcontainer/configs/config_test.go
@@ -145,15 +145,12 @@ func TestFuncHookRun(t *testing.T) {
 
 	fHook := configs.NewFunctionHook(func(s *specs.State) error {
 		if !reflect.DeepEqual(state, s) {
-			return fmt.Errorf("expected state %+v to equal %+v", state, s)
+			t.Errorf("Expected state %+v to equal %+v", state, s)
 		}
 		return nil
 	})
 
-	err := fHook.Run(state)
-	if err != nil {
-		t.Fatal(err)
-	}
+	fHook.Run(state)
 }
 
 func TestCommandHookRun(t *testing.T) {
diff --git a/libcontainer/configs/validate/validator_test.go b/libcontainer/configs/validate/validator_test.go
index 7d1ef0f6..327776b5 100644
--- a/libcontainer/configs/validate/validator_test.go
+++ b/libcontainer/configs/validate/validator_test.go
@@ -154,7 +154,7 @@ func TestValidateSecurityWithoutNEWNS(t *testing.T) {
 
 func TestValidateUsernamespace(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
-		t.Skip("Test requires userns.")
+		t.Skip("userns is unsupported")
 	}
 	config := &configs.Config{
 		Rootfs: "/var",
diff --git a/libcontainer/factory_linux_test.go b/libcontainer/factory_linux_test.go
index 0b88c3d3..665a8a7c 100644
--- a/libcontainer/factory_linux_test.go
+++ b/libcontainer/factory_linux_test.go
@@ -122,10 +122,7 @@ func TestFactoryNewTmpfs(t *testing.T) {
 	if m.Source != "tmpfs" {
 		t.Fatalf("Source of root: %s, expected %s", m.Source, "tmpfs")
 	}
-	err = unix.Unmount(root, unix.MNT_DETACH)
-	if err != nil {
-		t.Error("failed to unmount root:", err)
-	}
+	unix.Unmount(root, unix.MNT_DETACH)
 }
 
 func TestFactoryLoadNotExists(t *testing.T) {
@@ -133,7 +130,7 @@ func TestFactoryLoadNotExists(t *testing.T) {
 	if rerr != nil {
 		t.Fatal(rerr)
 	}
-	defer os.RemoveAll(root) //nolint: errcheck
+	defer os.RemoveAll(root)
 	factory, err := New(root, Cgroupfs)
 	if err != nil {
 		t.Fatal(err)
@@ -156,7 +153,7 @@ func TestFactoryLoadContainer(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	defer os.RemoveAll(root) //nolint: errcheck
+	defer os.RemoveAll(root)
 	// setup default container config and state for mocking
 	var (
 		id            = "1"
@@ -222,7 +219,7 @@ func marshal(path string, v interface{}) error {
 	if err != nil {
 		return err
 	}
-	defer f.Close() //nolint: errcheck
+	defer f.Close()
 	return utils.WriteJSON(f, v)
 }
 
diff --git a/libcontainer/integration/checkpoint_test.go b/libcontainer/integration/checkpoint_test.go
index f2870ae0..9ec03b92 100644
--- a/libcontainer/integration/checkpoint_test.go
+++ b/libcontainer/integration/checkpoint_test.go
@@ -14,16 +14,16 @@ import (
 	"golang.org/x/sys/unix"
 )
 
-func showFile(t *testing.T, fname string) {
+func showFile(t *testing.T, fname string) error {
 	t.Helper()
 	t.Logf("=== %s ===\n", fname)
 
 	f, err := os.Open(fname)
 	if err != nil {
 		t.Log(err)
-		return
+		return err
 	}
-	defer f.Close() //nolint: errcheck
+	defer f.Close()
 
 	scanner := bufio.NewScanner(f)
 	for scanner.Scan() {
@@ -31,16 +31,17 @@ func showFile(t *testing.T, fname string) {
 	}
 
 	if err := scanner.Err(); err != nil {
-		t.Log(err)
-		return
+		return err
 	}
 
 	t.Logf("=== END ===\n")
+
+	return nil
 }
 
 func TestUsernsCheckpoint(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
-		t.Skip("Test requires userns.")
+		t.Skip("userns is unsupported")
 	}
 	cmd := exec.Command("criu", "check", "--feature", "userns")
 	if err := cmd.Run(); err != nil {
@@ -64,7 +65,7 @@ func testCheckpoint(t *testing.T, userns bool) {
 
 	root, err := newTestRoot()
 	ok(t, err)
-	defer remove(root)
+	defer os.RemoveAll(root)
 
 	rootfs, err := newRootfs()
 	ok(t, err)
@@ -79,7 +80,7 @@ func testCheckpoint(t *testing.T, userns bool) {
 
 	container, err := factory.Create("test", config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	stdinR, stdinW, err := os.Pipe()
 	ok(t, err)
@@ -96,8 +97,8 @@ func testCheckpoint(t *testing.T, userns bool) {
 	}
 
 	err = container.Run(&pconfig)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	pid, err := pconfig.Pid()
@@ -108,7 +109,7 @@ func testCheckpoint(t *testing.T, userns bool) {
 
 	parentDir, err := ioutil.TempDir("", "criu-parent")
 	ok(t, err)
-	defer remove(parentDir)
+	defer os.RemoveAll(parentDir)
 
 	preDumpOpts := &libcontainer.CriuOpts{
 		ImagesDirectory: parentDir,
@@ -131,7 +132,7 @@ func testCheckpoint(t *testing.T, userns bool) {
 
 	imagesDir, err := ioutil.TempDir("", "criu")
 	ok(t, err)
-	defer remove(imagesDir)
+	defer os.RemoveAll(imagesDir)
 
 	checkpointOpts := &libcontainer.CriuOpts{
 		ImagesDirectory: imagesDir,
@@ -153,7 +154,7 @@ func testCheckpoint(t *testing.T, userns bool) {
 		t.Fatal("Unexpected state checkpoint: ", state)
 	}
 
-	_ = stdinW.Close()
+	stdinW.Close()
 	_, err = process.Wait()
 	ok(t, err)
 
@@ -173,8 +174,8 @@ func testCheckpoint(t *testing.T, userns bool) {
 	}
 
 	err = container.Restore(restoreProcessConfig, checkpointOpts)
-	_ = restoreStdinR.Close()
-	defer restoreStdinW.Close() //nolint: errcheck
+	restoreStdinR.Close()
+	defer restoreStdinW.Close()
 	if err != nil {
 		showFile(t, restoreLog)
 		t.Fatal(err)
@@ -195,7 +196,7 @@ func testCheckpoint(t *testing.T, userns bool) {
 	_, err = restoreStdinW.WriteString("Hello!")
 	ok(t, err)
 
-	_ = restoreStdinW.Close()
+	restoreStdinW.Close()
 	waitProcess(restoreProcessConfig, t)
 
 	output := restoreStdout.String()
diff --git a/libcontainer/integration/exec_test.go b/libcontainer/integration/exec_test.go
index 08f272d3..505006a8 100644
--- a/libcontainer/integration/exec_test.go
+++ b/libcontainer/integration/exec_test.go
@@ -29,7 +29,7 @@ func TestExecPS(t *testing.T) {
 
 func TestUsernsExecPS(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
-		t.Skip("Test requires userns.")
+		t.Skip("userns is unsupported")
 	}
 	testExecPS(t, true)
 }
@@ -166,7 +166,7 @@ func TestRlimit(t *testing.T) {
 
 func TestUsernsRlimit(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
-		t.Skip("Test requires userns.")
+		t.Skip("userns is unsupported")
 	}
 
 	testRlimit(t, true)
@@ -213,7 +213,7 @@ func TestEnter(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	// Execute a first process in the container
 	stdinR, stdinW, err := os.Pipe()
@@ -230,8 +230,8 @@ func TestEnter(t *testing.T) {
 		Init:   true,
 	}
 	err = container.Run(&pconfig)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 	pid, err := pconfig.Pid()
 	ok(t, err)
@@ -248,8 +248,8 @@ func TestEnter(t *testing.T) {
 	pconfig2.Stdout = &stdout2
 
 	err = container.Run(&pconfig2)
-	_ = stdinR2.Close()
-	defer stdinW2.Close() //nolint: errcheck
+	stdinR2.Close()
+	defer stdinW2.Close()
 	ok(t, err)
 
 	pid2, err := pconfig2.Pid()
@@ -269,10 +269,10 @@ func TestEnter(t *testing.T) {
 	}
 
 	// Wait processes
-	_ = stdinW2.Close()
+	stdinW2.Close()
 	waitProcess(&pconfig2, t)
 
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(&pconfig, t)
 
 	// Check that both processes live in the same pidns
@@ -300,7 +300,7 @@ func TestProcessEnv(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	var stdout bytes.Buffer
 	pconfig := libcontainer.Process{
@@ -349,7 +349,7 @@ func TestProcessEmptyCaps(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	var stdout bytes.Buffer
 	pconfig := libcontainer.Process{
@@ -397,7 +397,7 @@ func TestProcessCaps(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	var stdout bytes.Buffer
 	pconfig := libcontainer.Process{
@@ -462,7 +462,7 @@ func TestAdditionalGroups(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	var stdout bytes.Buffer
 	pconfig := libcontainer.Process{
@@ -498,7 +498,7 @@ func TestFreeze(t *testing.T) {
 
 func TestSystemdFreeze(t *testing.T) {
 	if !systemd.IsRunningSystemd() {
-		t.Skip("Test requires systemd.")
+		t.Skip("Systemd is unsupported")
 	}
 	testFreeze(t, true)
 }
@@ -518,7 +518,7 @@ func testFreeze(t *testing.T, systemd bool) {
 	})
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	stdinR, stdinW, err := os.Pipe()
 	ok(t, err)
@@ -531,8 +531,8 @@ func testFreeze(t *testing.T, systemd bool) {
 		Init:  true,
 	}
 	err = container.Run(pconfig)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	err = container.Pause()
@@ -545,7 +545,7 @@ func testFreeze(t *testing.T, systemd bool) {
 		t.Fatal("Unexpected state: ", state)
 	}
 
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(pconfig, t)
 }
 
@@ -555,7 +555,7 @@ func TestCpuShares(t *testing.T) {
 
 func TestCpuSharesSystemd(t *testing.T) {
 	if !systemd.IsRunningSystemd() {
-		t.Skip("Test requires systemd.")
+		t.Skip("Systemd is unsupported")
 	}
 	testCpuShares(t, true)
 }
@@ -590,7 +590,7 @@ func TestPids(t *testing.T) {
 
 func TestPidsSystemd(t *testing.T) {
 	if !systemd.IsRunningSystemd() {
-		t.Skip("Test requires systemd.")
+		t.Skip("Systemd is unsupported")
 	}
 	testPids(t, true)
 }
@@ -664,7 +664,7 @@ func TestCgroupResourcesUnifiedErrorOnV1(t *testing.T) {
 
 func TestCgroupResourcesUnifiedErrorOnV1Systemd(t *testing.T) {
 	if !systemd.IsRunningSystemd() {
-		t.Skip("Test requires systemd.")
+		t.Skip("Systemd is unsupported")
 	}
 	testCgroupResourcesUnifiedErrorOnV1(t, true)
 }
@@ -699,7 +699,7 @@ func TestCgroupResourcesUnified(t *testing.T) {
 
 func TestCgroupResourcesUnifiedSystemd(t *testing.T) {
 	if !systemd.IsRunningSystemd() {
-		t.Skip("Test requires systemd.")
+		t.Skip("Systemd is unsupported")
 	}
 	testCgroupResourcesUnified(t, true)
 }
@@ -826,7 +826,7 @@ func TestContainerState(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	stdinR, stdinW, err := os.Pipe()
 	ok(t, err)
@@ -840,8 +840,8 @@ func TestContainerState(t *testing.T) {
 	}
 	err = container.Run(p)
 	ok(t, err)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 
 	st, err := container.State()
 	ok(t, err)
@@ -851,7 +851,7 @@ func TestContainerState(t *testing.T) {
 	if l1 != l {
 		t.Fatal("Container using non-host ipc namespace")
 	}
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(p, t)
 }
 
@@ -868,7 +868,7 @@ func TestPassExtraFiles(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	var stdout bytes.Buffer
 	pipeout1, pipein1, err := os.Pipe()
@@ -921,7 +921,7 @@ func TestMountCmds(t *testing.T) {
 
 	tmpDir, err := ioutil.TempDir("", "tmpdir")
 	ok(t, err)
-	defer remove(tmpDir)
+	defer os.RemoveAll(tmpDir)
 
 	config := newTemplateConfig(t, &tParam{rootfs: rootfs})
 	config.Mounts = append(config.Mounts, &configs.Mount{
@@ -941,7 +941,7 @@ func TestMountCmds(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	pconfig := libcontainer.Process{
 		Cwd:  "/",
@@ -981,7 +981,7 @@ func TestSysctl(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	var stdout bytes.Buffer
 	pconfig := libcontainer.Process{
@@ -1110,7 +1110,7 @@ func TestOomScoreAdj(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	var stdout bytes.Buffer
 	pconfig := libcontainer.Process{
@@ -1305,8 +1305,8 @@ func TestSTDIOPermissions(t *testing.T) {
 	}
 }
 
-func unmountOp(path string) {
-	_ = unix.Unmount(path, unix.MNT_DETACH)
+func unmountOp(path string) error {
+	return unix.Unmount(path, unix.MNT_DETACH)
 }
 
 // Launch container with rootfsPropagation in rslave mode. Also
@@ -1333,7 +1333,7 @@ func TestRootfsPropagationSlaveMount(t *testing.T) {
 	// Bind mount a volume
 	dir1host, err := ioutil.TempDir("", "mnt1host")
 	ok(t, err)
-	defer remove(dir1host)
+	defer os.RemoveAll(dir1host)
 
 	// Make this dir a "shared" mount point. This will make sure a
 	// slave relationship can be established in container.
@@ -1351,7 +1351,7 @@ func TestRootfsPropagationSlaveMount(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	stdinR, stdinW, err := os.Pipe()
 	ok(t, err)
@@ -1365,15 +1365,15 @@ func TestRootfsPropagationSlaveMount(t *testing.T) {
 	}
 
 	err = container.Run(pconfig)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	// Create mnt1host/mnt2host and bind mount itself on top of it. This
 	// should be visible in container.
 	dir2host, err := ioutil.TempDir(dir1host, "mnt2host")
 	ok(t, err)
-	defer remove(dir2host)
+	defer os.RemoveAll(dir2host)
 
 	err = unix.Mount(dir2host, dir2host, "bind", unix.MS_BIND, "")
 	defer unmountOp(dir2host)
@@ -1394,13 +1394,13 @@ func TestRootfsPropagationSlaveMount(t *testing.T) {
 	}
 
 	err = container.Run(pconfig2)
-	_ = stdinR2.Close()
-	defer stdinW2.Close() //nolint: errcheck
+	stdinR2.Close()
+	defer stdinW2.Close()
 	ok(t, err)
 
-	_ = stdinW2.Close()
+	stdinW2.Close()
 	waitProcess(pconfig2, t)
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(pconfig, t)
 
 	mountPropagated = false
@@ -1448,7 +1448,7 @@ func TestRootfsPropagationSharedMount(t *testing.T) {
 	// Bind mount a volume
 	dir1host, err := ioutil.TempDir("", "mnt1host")
 	ok(t, err)
-	defer remove(dir1host)
+	defer os.RemoveAll(dir1host)
 
 	// Make this dir a "shared" mount point. This will make sure a
 	// shared relationship can be established in container.
@@ -1466,7 +1466,7 @@ func TestRootfsPropagationSharedMount(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	stdinR, stdinW, err := os.Pipe()
 	ok(t, err)
@@ -1480,8 +1480,8 @@ func TestRootfsPropagationSharedMount(t *testing.T) {
 	}
 
 	err = container.Run(pconfig)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	// Create mnt1host/mnt2cont.  This will become visible inside container
@@ -1489,7 +1489,7 @@ func TestRootfsPropagationSharedMount(t *testing.T) {
 	// should be visible on host now.
 	dir2host, err := ioutil.TempDir(dir1host, "mnt2cont")
 	ok(t, err)
-	defer remove(dir2host)
+	defer os.RemoveAll(dir2host)
 
 	dir2cont = filepath.Join(dir1cont, filepath.Base(dir2host))
 
@@ -1515,14 +1515,14 @@ func TestRootfsPropagationSharedMount(t *testing.T) {
 	pconfig2.Capabilities.Inheritable = append(config.Capabilities.Inheritable, "CAP_SYS_ADMIN")
 
 	err = container.Run(pconfig2)
-	_ = stdinR2.Close()
-	defer stdinW2.Close() //nolint: errcheck
+	stdinR2.Close()
+	defer stdinW2.Close()
 	ok(t, err)
 
 	// Wait for process
-	_ = stdinW2.Close()
+	stdinW2.Close()
 	waitProcess(pconfig2, t)
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(pconfig, t)
 
 	defer unmountOp(dir2host)
@@ -1629,7 +1629,7 @@ func TestInitJoinPID(t *testing.T) {
 	// Execute a long-running container
 	container1, err := newContainer(t, newTemplateConfig(t, &tParam{rootfs: rootfs}))
 	ok(t, err)
-	defer destroyContainer(container1)
+	defer container1.Destroy()
 
 	stdinR1, stdinW1, err := os.Pipe()
 	ok(t, err)
@@ -1641,8 +1641,8 @@ func TestInitJoinPID(t *testing.T) {
 		Init:  true,
 	}
 	err = container1.Run(init1)
-	_ = stdinR1.Close()
-	defer stdinW1.Close() //nolint: errcheck
+	stdinR1.Close()
+	defer stdinW1.Close()
 	ok(t, err)
 
 	// get the state of the first container
@@ -1656,7 +1656,7 @@ func TestInitJoinPID(t *testing.T) {
 	config2.Cgroups.Path = "integration/test2"
 	container2, err := newContainer(t, config2)
 	ok(t, err)
-	defer destroyContainer(container2)
+	defer container2.Destroy()
 
 	stdinR2, stdinW2, err := os.Pipe()
 	ok(t, err)
@@ -1668,8 +1668,8 @@ func TestInitJoinPID(t *testing.T) {
 		Init:  true,
 	}
 	err = container2.Run(init2)
-	_ = stdinR2.Close()
-	defer stdinW2.Close() //nolint: errcheck
+	stdinR2.Close()
+	defer stdinW2.Close()
 	ok(t, err)
 	// get the state of the second container
 	state2, err := container2.State()
@@ -1703,9 +1703,9 @@ func TestInitJoinPID(t *testing.T) {
 
 	// Stop init processes one by one. Stop the second container should
 	// not stop the first.
-	_ = stdinW2.Close()
+	stdinW2.Close()
 	waitProcess(init2, t)
-	_ = stdinW1.Close()
+	stdinW1.Close()
 	waitProcess(init1, t)
 
 	out := strings.TrimSpace(buffers.Stdout.String())
@@ -1720,7 +1720,7 @@ func TestInitJoinPID(t *testing.T) {
 
 func TestInitJoinNetworkAndUser(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
-		t.Skip("Test requires userns.")
+		t.Skip("userns is unsupported")
 	}
 	if testing.Short() {
 		return
@@ -1736,7 +1736,7 @@ func TestInitJoinNetworkAndUser(t *testing.T) {
 	})
 	container1, err := newContainer(t, config1)
 	ok(t, err)
-	defer destroyContainer(container1)
+	defer container1.Destroy()
 
 	stdinR1, stdinW1, err := os.Pipe()
 	ok(t, err)
@@ -1748,8 +1748,8 @@ func TestInitJoinNetworkAndUser(t *testing.T) {
 		Init:  true,
 	}
 	err = container1.Run(init1)
-	_ = stdinR1.Close()
-	defer stdinW1.Close() //nolint: errcheck
+	stdinR1.Close()
+	defer stdinW1.Close()
 	ok(t, err)
 
 	// get the state of the first container
@@ -1772,7 +1772,7 @@ func TestInitJoinNetworkAndUser(t *testing.T) {
 	config2.Cgroups.Path = "integration/test2"
 	container2, err := newContainer(t, config2)
 	ok(t, err)
-	defer destroyContainer(container2)
+	defer container2.Destroy()
 
 	stdinR2, stdinW2, err := os.Pipe()
 	ok(t, err)
@@ -1784,8 +1784,8 @@ func TestInitJoinNetworkAndUser(t *testing.T) {
 		Init:  true,
 	}
 	err = container2.Run(init2)
-	_ = stdinR2.Close()
-	defer stdinW2.Close() //nolint: errcheck
+	stdinR2.Close()
+	defer stdinW2.Close()
 	ok(t, err)
 
 	// get the state of the second container
@@ -1809,9 +1809,9 @@ func TestInitJoinNetworkAndUser(t *testing.T) {
 	}
 	// Stop init processes one by one. Stop the second container should
 	// not stop the first.
-	_ = stdinW2.Close()
+	stdinW2.Close()
 	waitProcess(init2, t)
-	_ = stdinW1.Close()
+	stdinW1.Close()
 	waitProcess(init1, t)
 }
 
@@ -1835,7 +1835,7 @@ func TestTmpfsCopyUp(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	var stdout bytes.Buffer
 	pconfig := libcontainer.Process{
@@ -1861,7 +1861,7 @@ func TestTmpfsCopyUp(t *testing.T) {
 
 func TestCGROUPPrivate(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/cgroup"); os.IsNotExist(err) {
-		t.Skip("Test requires cgroupns.")
+		t.Skip("cgroupns is unsupported")
 	}
 	if testing.Short() {
 		return
@@ -1890,7 +1890,7 @@ func TestCGROUPPrivate(t *testing.T) {
 
 func TestCGROUPHost(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/cgroup"); os.IsNotExist(err) {
-		t.Skip("Test requires cgroupns.")
+		t.Skip("cgroupns is unsupported")
 	}
 	if testing.Short() {
 		return
@@ -1921,9 +1921,6 @@ func TestFdLeaks(t *testing.T) {
 }
 
 func TestFdLeaksSystemd(t *testing.T) {
-	if !systemd.IsRunningSystemd() {
-		t.Skip("Test requires systemd.")
-	}
 	testFdLeaks(t, true)
 }
 
diff --git a/libcontainer/integration/execin_test.go b/libcontainer/integration/execin_test.go
index 8af90364..894e54f3 100644
--- a/libcontainer/integration/execin_test.go
+++ b/libcontainer/integration/execin_test.go
@@ -28,7 +28,7 @@ func TestExecIn(t *testing.T) {
 	config := newTemplateConfig(t, &tParam{rootfs: rootfs})
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	// Execute a first process in the container
 	stdinR, stdinW, err := os.Pipe()
@@ -41,8 +41,8 @@ func TestExecIn(t *testing.T) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	buffers := newStdBuffers()
@@ -58,7 +58,7 @@ func TestExecIn(t *testing.T) {
 	err = container.Run(ps)
 	ok(t, err)
 	waitProcess(ps, t)
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(process, t)
 
 	out := buffers.Stdout.String()
@@ -72,7 +72,7 @@ func TestExecIn(t *testing.T) {
 
 func TestExecInUsernsRlimit(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
-		t.Skip("Test requires userns.")
+		t.Skip("userns is unsupported")
 	}
 
 	testExecInRlimit(t, true)
@@ -98,7 +98,7 @@ func testExecInRlimit(t *testing.T, userns bool) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	stdinR, stdinW, err := os.Pipe()
 	ok(t, err)
@@ -110,8 +110,8 @@ func testExecInRlimit(t *testing.T, userns bool) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	buffers := newStdBuffers()
@@ -132,7 +132,7 @@ func testExecInRlimit(t *testing.T, userns bool) {
 	ok(t, err)
 	waitProcess(ps, t)
 
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(process, t)
 
 	out := buffers.Stdout.String()
@@ -153,7 +153,7 @@ func TestExecInAdditionalGroups(t *testing.T) {
 	config := newTemplateConfig(t, &tParam{rootfs: rootfs})
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	// Execute a first process in the container
 	stdinR, stdinW, err := os.Pipe()
@@ -166,8 +166,8 @@ func TestExecInAdditionalGroups(t *testing.T) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	var stdout bytes.Buffer
@@ -185,7 +185,7 @@ func TestExecInAdditionalGroups(t *testing.T) {
 	// Wait for process
 	waitProcess(&pconfig, t)
 
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(process, t)
 
 	outputGroups := stdout.String()
@@ -210,7 +210,7 @@ func TestExecInError(t *testing.T) {
 	config := newTemplateConfig(t, &tParam{rootfs: rootfs})
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	// Execute a first process in the container
 	stdinR, stdinW, err := os.Pipe()
@@ -223,9 +223,9 @@ func TestExecInError(t *testing.T) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
+	stdinR.Close()
 	defer func() {
-		_ = stdinW.Close()
+		stdinW.Close()
 		if _, err := process.Wait(); err != nil {
 			t.Log(err)
 		}
@@ -264,7 +264,7 @@ func TestExecInTTY(t *testing.T) {
 	config := newTemplateConfig(t, &tParam{rootfs: rootfs})
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	// Execute a first process in the container
 	stdinR, stdinW, err := os.Pipe()
@@ -277,9 +277,9 @@ func TestExecInTTY(t *testing.T) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
+	stdinR.Close()
 	defer func() {
-		_ = stdinW.Close()
+		stdinW.Close()
 		if _, err := process.Wait(); err != nil {
 			t.Log(err)
 		}
@@ -335,8 +335,8 @@ func TestExecInTTY(t *testing.T) {
 		}
 
 		waitProcess(ps, t)
-		_ = parent.Close()
-		_ = child.Close()
+		parent.Close()
+		child.Close()
 
 		out := stdout.String()
 		if !strings.Contains(out, "cat") || !strings.Contains(out, "ps") {
@@ -358,7 +358,7 @@ func TestExecInEnvironment(t *testing.T) {
 	config := newTemplateConfig(t, &tParam{rootfs: rootfs})
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	// Execute a first process in the container
 	stdinR, stdinW, err := os.Pipe()
@@ -371,8 +371,8 @@ func TestExecInEnvironment(t *testing.T) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	buffers := newStdBuffers()
@@ -394,7 +394,7 @@ func TestExecInEnvironment(t *testing.T) {
 	ok(t, err)
 	waitProcess(process2, t)
 
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(process, t)
 
 	out := buffers.Stdout.String()
@@ -419,7 +419,7 @@ func TestExecinPassExtraFiles(t *testing.T) {
 	config := newTemplateConfig(t, &tParam{rootfs: rootfs})
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	// Execute a first process in the container
 	stdinR, stdinW, err := os.Pipe()
@@ -432,8 +432,8 @@ func TestExecinPassExtraFiles(t *testing.T) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	var stdout bytes.Buffer
@@ -453,7 +453,7 @@ func TestExecinPassExtraFiles(t *testing.T) {
 	ok(t, err)
 
 	waitProcess(inprocess, t)
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(process, t)
 
 	out := stdout.String()
@@ -488,7 +488,7 @@ func TestExecInOomScoreAdj(t *testing.T) {
 	config.OomScoreAdj = ptrInt(200)
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	stdinR, stdinW, err := os.Pipe()
 	ok(t, err)
@@ -500,8 +500,8 @@ func TestExecInOomScoreAdj(t *testing.T) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	buffers := newStdBuffers()
@@ -517,7 +517,7 @@ func TestExecInOomScoreAdj(t *testing.T) {
 	ok(t, err)
 	waitProcess(ps, t)
 
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(process, t)
 
 	out := buffers.Stdout.String()
@@ -528,7 +528,7 @@ func TestExecInOomScoreAdj(t *testing.T) {
 
 func TestExecInUserns(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
-		t.Skip("Test requires userns.")
+		t.Skip("userns is unsupported")
 	}
 	if testing.Short() {
 		return
@@ -542,7 +542,7 @@ func TestExecInUserns(t *testing.T) {
 	})
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer destroyContainer(container)
+	defer container.Destroy()
 
 	// Execute a first process in the container
 	stdinR, stdinW, err := os.Pipe()
@@ -556,8 +556,8 @@ func TestExecInUserns(t *testing.T) {
 		Init:  true,
 	}
 	err = container.Run(process)
-	_ = stdinR.Close()
-	defer stdinW.Close() //nolint: errcheck
+	stdinR.Close()
+	defer stdinW.Close()
 	ok(t, err)
 
 	initPID, err := process.Pid()
@@ -578,7 +578,7 @@ func TestExecInUserns(t *testing.T) {
 	err = container.Run(process2)
 	ok(t, err)
 	waitProcess(process2, t)
-	_ = stdinW.Close()
+	stdinW.Close()
 	waitProcess(process, t)
 
 	if out := strings.TrimSpace(buffers.Stdout.String()); out != initUserns {
diff --git a/libcontainer/integration/seccomp_test.go b/libcontainer/integration/seccomp_test.go
index 244f2752..288dc893 100644
--- a/libcontainer/integration/seccomp_test.go
+++ b/libcontainer/integration/seccomp_test.go
@@ -37,7 +37,7 @@ func TestSeccompDenyGetcwdWithErrno(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer container.Destroy() //nolint:errcheck
+	defer container.Destroy()
 
 	buffers := newStdBuffers()
 	pwd := &libcontainer.Process{
@@ -100,7 +100,7 @@ func TestSeccompDenyGetcwd(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer container.Destroy() //nolint:errcheck
+	defer container.Destroy()
 
 	buffers := newStdBuffers()
 	pwd := &libcontainer.Process{
@@ -170,7 +170,7 @@ func TestSeccompPermitWriteConditional(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer container.Destroy() //nolint:errcheck
+	defer container.Destroy()
 
 	buffers := newStdBuffers()
 	dmesg := &libcontainer.Process{
@@ -226,7 +226,7 @@ func TestSeccompDenyWriteConditional(t *testing.T) {
 
 	container, err := newContainer(t, config)
 	ok(t, err)
-	defer container.Destroy() //nolint:errcheck
+	defer container.Destroy()
 
 	buffers := newStdBuffers()
 	dmesg := &libcontainer.Process{
diff --git a/libcontainer/integration/utils_test.go b/libcontainer/integration/utils_test.go
index f3d176bf..75203d6f 100644
--- a/libcontainer/integration/utils_test.go
+++ b/libcontainer/integration/utils_test.go
@@ -137,7 +137,7 @@ func newRootfs() (string, error) {
 }
 
 func remove(dir string) {
-	_ = os.RemoveAll(dir)
+	os.RemoveAll(dir)
 }
 
 // copyBusybox copies the rootfs for a busybox container created for the test image
@@ -179,7 +179,7 @@ func runContainer(t *testing.T, config *configs.Config, console string, args ...
 	if err != nil {
 		return nil, -1, err
 	}
-	defer destroyContainer(container)
+	defer container.Destroy()
 	buffers = newStdBuffers()
 	process := &libcontainer.Process{
 		Cwd:    "/",
@@ -209,7 +209,3 @@ func runContainer(t *testing.T, config *configs.Config, console string, args ...
 	}
 	return
 }
-
-func destroyContainer(container libcontainer.Container) {
-	_ = container.Destroy()
-}
diff --git a/libcontainer/nsenter/nsenter_test.go b/libcontainer/nsenter/nsenter_test.go
index 9627a61f..83340913 100644
--- a/libcontainer/nsenter/nsenter_test.go
+++ b/libcontainer/nsenter/nsenter_test.go
@@ -84,7 +84,7 @@ func TestNsenterValidPaths(t *testing.T) {
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	_, _ = p.Wait()
+	p.Wait()
 }
 
 func TestNsenterInvalidPaths(t *testing.T) {
@@ -179,10 +179,8 @@ func TestNsenterChildLogging(t *testing.T) {
 	if err != nil {
 		t.Fatalf("failed to create log pipe %v", err)
 	}
-	defer func() {
-		_ = logwrite.Close()
-		_ = logread.Close()
-	}()
+	defer logread.Close()
+	defer logwrite.Close()
 
 	namespaces := []string{
 		// join pid ns of the current process
diff --git a/libcontainer/seccomp/patchbpf/enosys_linux_test.go b/libcontainer/seccomp/patchbpf/enosys_linux_test.go
index ce5f0254..ee26a6a6 100644
--- a/libcontainer/seccomp/patchbpf/enosys_linux_test.go
+++ b/libcontainer/seccomp/patchbpf/enosys_linux_test.go
@@ -106,6 +106,14 @@ var testArches = []string{
 	"s390x",
 }
 
+func archStringToNative(arch string) (nativeArch, error) {
+	scmpArch, err := libseccomp.GetArchFromString(arch)
+	if err != nil {
+		return 0, fmt.Errorf("unknown architecture %q: %v", arch, err)
+	}
+	return archToNative(scmpArch)
+}
+
 func testEnosysStub(t *testing.T, defaultAction configs.Action, arches []string) {
 	explicitSyscalls := []string{
 		"setns",
diff --git a/libcontainer/specconv/spec_linux_test.go b/libcontainer/specconv/spec_linux_test.go
index 1a0aae9a..e8deabae 100644
--- a/libcontainer/specconv/spec_linux_test.go
+++ b/libcontainer/specconv/spec_linux_test.go
@@ -470,7 +470,7 @@ func TestDupNamespaces(t *testing.T) {
 
 func TestNonZeroEUIDCompatibleSpecconvValidate(t *testing.T) {
 	if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
-		t.Skip("Test requires userns.")
+		t.Skip("userns is unsupported")
 	}
 
 	spec := Example()
diff --git a/tests/integration/update.bats b/tests/integration/update.bats
index 41344a9d..e204ea49 100644
--- a/tests/integration/update.bats
+++ b/tests/integration/update.bats
@@ -648,29 +648,3 @@ EOF
 	runc resume test_update
 	[ "$status" -eq 0 ]
 }
-
-@test "runc update replaces devices cgroup program" {
-	[[ "$ROOTLESS" -ne 0 ]] && requires rootless_cgroup
-
-	# Unfortunately we can't update device rules directly with runc ("runc
-	# update" doesn't support it, and adding support would require ironing out
-	# some long-standing design issues with device configuration). So instead
-	# we just run "runc update" many times, relying on the fact that runc will
-	# re-apply devices cgroup rules on each runc update.
-	#
-	# In the past runc would not delete old cgroupv2 eBPF programs, so this
-	# test ensures that once we go past the program limit (64 stacked programs
-	# at time of writing) you can still run "runc" update.
-
-	# Run the container in the background.
-	runc run -d --console-socket "$CONSOLE_SOCKET" test_update
-	[ "$status" -eq 0 ]
-
-	for new_limit in $(seq 300); do
-		runc update --pids-limit "$((2 * new_limit))" test_update
-		[ "$status" -eq 0 ]
-	done
-
-	# The container should still be running.
-	testcontainer test_update running
-}