aboutsummaryrefslogtreecommitdiffhomepage
path: root/container/initoverlay.go
blob: 2fec63645f35188830983c9220beb6001cdda52e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package container

import (
	"encoding/gob"
	"fmt"
	"slices"
	"strings"

	"hakurei.app/container/check"
	"hakurei.app/container/fhs"
)

const (
	// intermediate root file name pattern for [MountOverlayOp.Upper];
	// remains after apply returns
	intermediatePatternOverlayUpper = "overlay.upper.*"
	// intermediate root file name pattern for [MountOverlayOp.Work];
	// remains after apply returns
	intermediatePatternOverlayWork = "overlay.work.*"
)

func init() { gob.Register(new(MountOverlayOp)) }

const (
	// OverlayEphemeralUnexpectedUpper is set when [MountOverlayOp.Work] is nil
	// and [MountOverlayOp.Upper] holds an unexpected value.
	OverlayEphemeralUnexpectedUpper = iota
	// OverlayReadonlyLower is set when [MountOverlayOp.Lower] contains less than
	// two entries when mounting readonly.
	OverlayReadonlyLower
	// OverlayEmptyLower is set when [MountOverlayOp.Lower] has length of zero.
	OverlayEmptyLower
)

// OverlayArgumentError is returned for [MountOverlayOp] supplied with invalid argument.
type OverlayArgumentError struct {
	Type  uintptr
	Value string
}

func (e *OverlayArgumentError) Error() string {
	switch e.Type {
	case OverlayEphemeralUnexpectedUpper:
		return fmt.Sprintf("upperdir has unexpected value %q", e.Value)

	case OverlayReadonlyLower:
		return "readonly overlay requires at least two lowerdir"

	case OverlayEmptyLower:
		return "overlay requires at least one lowerdir"

	default:
		return fmt.Sprintf("invalid overlay argument error %#x", e.Type)
	}
}

// Overlay is a helper for appending [MountOverlayOp] to [Ops].
func (f *Ops) Overlay(
	target, state, work *check.Absolute,
	layers ...*check.Absolute,
) *Ops {
	*f = append(*f, &MountOverlayOp{
		Target: target,
		Lower:  layers,
		Upper:  state,
		Work:   work,
	})
	return f
}

// OverlayEphemeral appends a [MountOverlayOp] with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) *Ops {
	return f.Overlay(target, fhs.AbsRoot, nil, layers...)
}

// OverlayReadonly appends a readonly [MountOverlayOp].
func (f *Ops) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) *Ops {
	return f.Overlay(target, nil, nil, layers...)
}

// MountOverlayOp mounts [FstypeOverlay] on container path Target.
type MountOverlayOp struct {
	Target *check.Absolute

	// Any filesystem, does not need to be on a writable filesystem.
	Lower []*check.Absolute
	// Formatted for [OptionOverlayLowerdir].
	//
	// Resolved, prefixed and escaped during early.
	lower []string

	// The upperdir is normally on a writable filesystem.
	//
	// If Work is nil and Upper holds the special value [fhs.AbsRoot], an
	// ephemeral upperdir and workdir will be set up.
	//
	// If both Work and Upper are nil, upperdir and workdir is omitted and the
	// overlay is mounted readonly.
	Upper *check.Absolute
	// Formatted for [OptionOverlayUpperdir].
	//
	// Resolved, prefixed and escaped during early.
	upper string

	// The workdir needs to be an empty directory on the same filesystem as upperdir.
	Work *check.Absolute
	// Formatted for [OptionOverlayWorkdir].
	//
	// Resolved, prefixed and escaped during early.
	work string

	ephemeral bool

	// Used internally for mounting to the intermediate root.
	noPrefix bool
}

func (o *MountOverlayOp) Valid() bool {
	if o == nil {
		return false
	}
	if o.Work != nil && o.Upper == nil {
		return false
	}
	if slices.Contains(o.Lower, nil) {
		return false
	}
	return o.Target != nil
}

func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
	if o.Work == nil && o.Upper != nil {
		switch o.Upper.String() {
		case fhs.Root: // ephemeral
			o.ephemeral = true // intermediate root not yet available

		default:
			return &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, o.Upper.String()}
		}
	}
	// readonly handled in apply

	if !o.ephemeral {
		if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
			// unreachable
			return OpStateError("overlay")
		}

		if o.Upper != nil {
			if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
				return err
			} else {
				o.upper = check.EscapeOverlayDataSegment(toHost(v))
			}
		}

		if o.Work != nil {
			if v, err := k.evalSymlinks(o.Work.String()); err != nil {
				return err
			} else {
				o.work = check.EscapeOverlayDataSegment(toHost(v))
			}
		}
	}

	o.lower = make([]string, len(o.Lower))
	for i, a := range o.Lower { // nil checked in Valid
		if v, err := k.evalSymlinks(a.String()); err != nil {
			return err
		} else {
			o.lower[i] = check.EscapeOverlayDataSegment(toHost(v))
		}
	}
	return nil
}

func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
	target := o.Target.String()
	if !o.noPrefix {
		target = toSysroot(target)
	}
	if err := k.mkdirAll(target, state.ParentPerm); err != nil {
		return err
	}

	if o.ephemeral {
		var err error
		// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
		if o.upper, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayUpper); err != nil {
			return err
		}
		if o.work, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayWork); err != nil {
			return err
		}
	}

	options := make([]string, 0, 4)

	if o.upper == zeroString && o.work == zeroString { // readonly
		if len(o.Lower) < 2 {
			return &OverlayArgumentError{OverlayReadonlyLower, zeroString}
		}
		// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
	} else {
		if len(o.Lower) == 0 {
			return &OverlayArgumentError{OverlayEmptyLower, zeroString}
		}
		options = append(options,
			OptionOverlayUpperdir+"="+o.upper,
			OptionOverlayWorkdir+"="+o.work)
	}
	options = append(options,
		OptionOverlayLowerdir+"="+strings.Join(o.lower, check.SpecialOverlayPath),
		OptionOverlayUserxattr)

	return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
}

func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }

func (o *MountOverlayOp) Is(op Op) bool {
	vo, ok := op.(*MountOverlayOp)
	return ok && o.Valid() && vo.Valid() &&
		o.Target.Is(vo.Target) &&
		slices.EqualFunc(o.Lower, vo.Lower, func(a, v *check.Absolute) bool { return a.Is(v) }) &&
		o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
}
func (*MountOverlayOp) prefix() (string, bool) { return "mounting", true }
func (o *MountOverlayOp) String() string {
	return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
}