Skip to content

Commit f520c3a

Browse files
committed
[ws-daemon] Internalize DeviceFilter(..) and dependencies which are not longer exported from runc
Reference: opencontainers/runc@47e0997
1 parent cd17a4f commit f520c3a

File tree

4 files changed

+864
-3
lines changed

4 files changed

+864
-3
lines changed

components/ws-daemon/pkg/cgroup/plugin_fuse_v2.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ import (
88
"context"
99
"path/filepath"
1010

11-
"github.com/opencontainers/runc/libcontainer/cgroups/ebpf"
12-
"github.com/opencontainers/runc/libcontainer/cgroups/ebpf/devicefilter"
1311
"github.com/opencontainers/runc/libcontainer/devices"
1412
"github.com/opencontainers/runc/libcontainer/specconv"
1513
"golang.org/x/sys/unix"
1614
"golang.org/x/xerrors"
1715

1816
"github.com/gitpod-io/gitpod/common-go/log"
17+
"github.com/gitpod-io/gitpod/ws-daemon/pkg/devicefilter"
1918
)
2019

2120
var (
@@ -47,7 +46,7 @@ func (c *FuseDeviceEnablerV2) Apply(ctx context.Context, opts *PluginOptions) er
4746
return xerrors.Errorf("failed to generate device filter: %w", err)
4847
}
4948

50-
_, err = ebpf.LoadAttachCgroupDeviceFilter(insts, license, cgroupFD)
49+
_, err = devicefilter.LoadAttachCgroupDeviceFilter(insts, license, cgroupFD)
5150
if err != nil {
5251
return xerrors.Errorf("failed to attach cgroup device filter: %w", err)
5352
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright (c) 2025 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
// Copied from https://github.com/opencontainers/runc/blob/e0406b4ba62071d40f1eaa443945764e0ef56c41/libcontainer/cgroups/devices/devicefilter.go
6+
//
7+
// Implements creation of eBPF device filter program.
8+
//
9+
// Based on https://github.com/containers/crun/blob/0.10.2/src/libcrun/ebpf.c
10+
//
11+
// Although ebpf.c is originally licensed under LGPL-3.0-or-later, the author (Giuseppe Scrivano)
12+
// agreed to relicense the file in Apache License 2.0: https://github.com/opencontainers/runc/issues/2144#issuecomment-543116397
13+
package devicefilter
14+
15+
import (
16+
"errors"
17+
"fmt"
18+
"math"
19+
"strconv"
20+
21+
"github.com/cilium/ebpf/asm"
22+
"github.com/opencontainers/runc/libcontainer/devices"
23+
"golang.org/x/sys/unix"
24+
)
25+
26+
const (
27+
// license string format is same as kernel MODULE_LICENSE macro
28+
license = "Apache"
29+
)
30+
31+
// DeviceFilter returns eBPF device filter program and its license string.
32+
func DeviceFilter(rules []*devices.Rule) (asm.Instructions, string, error) {
33+
// Generate the minimum ruleset for the device rules we are given. While we
34+
// don't care about minimum transitions in cgroupv2, using the emulator
35+
// gives us a guarantee that the behaviour of devices filtering is the same
36+
// as cgroupv1, including security hardenings to avoid misconfiguration
37+
// (such as punching holes in wildcard rules).
38+
emu := new(emulator)
39+
for _, rule := range rules {
40+
if err := emu.Apply(*rule); err != nil {
41+
return nil, "", err
42+
}
43+
}
44+
cleanRules, err := emu.Rules()
45+
if err != nil {
46+
return nil, "", err
47+
}
48+
49+
p := &program{
50+
defaultAllow: emu.IsBlacklist(),
51+
}
52+
p.init()
53+
54+
for idx, rule := range cleanRules {
55+
if rule.Type == devices.WildcardDevice {
56+
// We can safely skip over wildcard entries because there should
57+
// only be one (at most) at the very start to instruct cgroupv1 to
58+
// go into allow-list mode. However we do double-check this here.
59+
if idx != 0 || rule.Allow != emu.IsBlacklist() {
60+
return nil, "", fmt.Errorf("[internal error] emulated cgroupv2 devices ruleset had bad wildcard at idx %v (%s)", idx, rule.CgroupString())
61+
}
62+
continue
63+
}
64+
if rule.Allow == p.defaultAllow {
65+
// There should be no rules which have an action equal to the
66+
// default action, the emulator removes those.
67+
return nil, "", fmt.Errorf("[internal error] emulated cgroupv2 devices ruleset had no-op rule at idx %v (%s)", idx, rule.CgroupString())
68+
}
69+
if err := p.appendRule(rule); err != nil {
70+
return nil, "", err
71+
}
72+
}
73+
return p.finalize(), license, nil
74+
}
75+
76+
type program struct {
77+
insts asm.Instructions
78+
defaultAllow bool
79+
blockID int
80+
}
81+
82+
func (p *program) init() {
83+
// struct bpf_cgroup_dev_ctx: https://elixir.bootlin.com/linux/v5.3.6/source/include/uapi/linux/bpf.h#L3423
84+
/*
85+
u32 access_type
86+
u32 major
87+
u32 minor
88+
*/
89+
// R2 <- type (lower 16 bit of u32 access_type at R1[0])
90+
p.insts = append(p.insts,
91+
asm.LoadMem(asm.R2, asm.R1, 0, asm.Word),
92+
asm.And.Imm32(asm.R2, 0xFFFF))
93+
94+
// R3 <- access (upper 16 bit of u32 access_type at R1[0])
95+
p.insts = append(p.insts,
96+
asm.LoadMem(asm.R3, asm.R1, 0, asm.Word),
97+
// RSh: bitwise shift right
98+
asm.RSh.Imm32(asm.R3, 16))
99+
100+
// R4 <- major (u32 major at R1[4])
101+
p.insts = append(p.insts,
102+
asm.LoadMem(asm.R4, asm.R1, 4, asm.Word))
103+
104+
// R5 <- minor (u32 minor at R1[8])
105+
p.insts = append(p.insts,
106+
asm.LoadMem(asm.R5, asm.R1, 8, asm.Word))
107+
}
108+
109+
// appendRule rule converts an OCI rule to the relevant eBPF block and adds it
110+
// to the in-progress filter program. In order to operate properly, it must be
111+
// called with a "clean" rule list (generated by devices.Emulator.Rules() --
112+
// with any "a" rules removed).
113+
func (p *program) appendRule(rule *devices.Rule) error {
114+
if p.blockID < 0 {
115+
return errors.New("the program is finalized")
116+
}
117+
118+
var bpfType int32
119+
switch rule.Type {
120+
case devices.CharDevice:
121+
bpfType = int32(unix.BPF_DEVCG_DEV_CHAR)
122+
case devices.BlockDevice:
123+
bpfType = int32(unix.BPF_DEVCG_DEV_BLOCK)
124+
default:
125+
// We do not permit 'a', nor any other types we don't know about.
126+
return fmt.Errorf("invalid type %q", string(rule.Type))
127+
}
128+
if rule.Major > math.MaxUint32 {
129+
return fmt.Errorf("invalid major %d", rule.Major)
130+
}
131+
if rule.Minor > math.MaxUint32 {
132+
return fmt.Errorf("invalid minor %d", rule.Major)
133+
}
134+
hasMajor := rule.Major >= 0 // if not specified in OCI json, major is set to -1
135+
hasMinor := rule.Minor >= 0
136+
bpfAccess := int32(0)
137+
for _, r := range rule.Permissions {
138+
switch r {
139+
case 'r':
140+
bpfAccess |= unix.BPF_DEVCG_ACC_READ
141+
case 'w':
142+
bpfAccess |= unix.BPF_DEVCG_ACC_WRITE
143+
case 'm':
144+
bpfAccess |= unix.BPF_DEVCG_ACC_MKNOD
145+
default:
146+
return fmt.Errorf("unknown device access %v", r)
147+
}
148+
}
149+
// If the access is rwm, skip the check.
150+
hasAccess := bpfAccess != (unix.BPF_DEVCG_ACC_READ | unix.BPF_DEVCG_ACC_WRITE | unix.BPF_DEVCG_ACC_MKNOD)
151+
152+
var (
153+
blockSym = "block-" + strconv.Itoa(p.blockID)
154+
nextBlockSym = "block-" + strconv.Itoa(p.blockID+1)
155+
prevBlockLastIdx = len(p.insts) - 1
156+
)
157+
p.insts = append(p.insts,
158+
// if (R2 != bpfType) goto next
159+
asm.JNE.Imm(asm.R2, bpfType, nextBlockSym),
160+
)
161+
if hasAccess {
162+
p.insts = append(p.insts,
163+
// if (R3 & bpfAccess != R3 /* use R1 as a temp var */) goto next
164+
asm.Mov.Reg32(asm.R1, asm.R3),
165+
asm.And.Imm32(asm.R1, bpfAccess),
166+
asm.JNE.Reg(asm.R1, asm.R3, nextBlockSym),
167+
)
168+
}
169+
if hasMajor {
170+
p.insts = append(p.insts,
171+
// if (R4 != major) goto next
172+
asm.JNE.Imm(asm.R4, int32(rule.Major), nextBlockSym),
173+
)
174+
}
175+
if hasMinor {
176+
p.insts = append(p.insts,
177+
// if (R5 != minor) goto next
178+
asm.JNE.Imm(asm.R5, int32(rule.Minor), nextBlockSym),
179+
)
180+
}
181+
p.insts = append(p.insts, acceptBlock(rule.Allow)...)
182+
// set blockSym to the first instruction we added in this iteration
183+
p.insts[prevBlockLastIdx+1] = p.insts[prevBlockLastIdx+1].WithSymbol(blockSym)
184+
p.blockID++
185+
return nil
186+
}
187+
188+
func (p *program) finalize() asm.Instructions {
189+
var v int32
190+
if p.defaultAllow {
191+
v = 1
192+
}
193+
blockSym := "block-" + strconv.Itoa(p.blockID)
194+
p.insts = append(p.insts,
195+
// R0 <- v
196+
asm.Mov.Imm32(asm.R0, v).WithSymbol(blockSym),
197+
asm.Return(),
198+
)
199+
p.blockID = -1
200+
return p.insts
201+
}
202+
203+
func acceptBlock(accept bool) asm.Instructions {
204+
var v int32
205+
if accept {
206+
v = 1
207+
}
208+
return []asm.Instruction{
209+
// R0 <- v
210+
asm.Mov.Imm32(asm.R0, v),
211+
asm.Return(),
212+
}
213+
}

0 commit comments

Comments
 (0)