Skip to content

Commit 7712280

Browse files
author
Christian Weichel
committed
[workspacekit] Support mount proc in a workspace using seccomp-notify
1 parent c109573 commit 7712280

File tree

23 files changed

+1000
-114
lines changed

23 files changed

+1000
-114
lines changed

components/supervisor/BUILD.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ packages:
2525
deps:
2626
- :app
2727
- components/supervisor/frontend:app
28-
- components/workspacekit/go:app
28+
- components/workspacekit:app
2929
argdeps:
3030
- imageRepoBase
3131
config:

components/workspacekit/BUILD.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ packages:
55
- go.mod
66
- go.sum
77
- "**/*.go"
8+
- "**/*.c"
89
deps:
910
- components/common-go:lib
1011
- components/ws-daemon-api/go:lib
1112
- components/content-service-api/go:lib
13+
prep:
14+
- ["sh", "-c", "pkg-config --atleast-version=2.5.0 libseccomp || (echo \"requires libseccomp > 2.5.0\"; exit 1)"]
1215
config:
1316
packaging: app
14-
buildCommand: ["go", "build", "-ldflags", "-w -extldflags \"-static\""]
17+
buildCommand: ["go", "build", "-ldflags", "-w -extldflags \"-static\""]
18+
- name: libseccomp
19+
type: generic
20+
config:
21+
commands:
22+
- ["sh", "-c", "curl -L https://github.com/seccomp/libseccomp/releases/download/v2.5.1/libseccomp-2.5.1.tar.gz | tar xz"]
23+
- ["sh", "-c", "cd libseccomp-2.5.1 && ./configure --prefix=$PWD/../lib && make && make install"]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2021 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+
package cmd
6+
7+
import (
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// handlerCmd represents the base command for all syscall handler
12+
var handlerMountCmd = &cobra.Command{
13+
Use: "mount",
14+
Short: "In-namespace mount handler",
15+
Args: cobra.ExactArgs(4),
16+
Run: func(cmd *cobra.Command, args []string) {
17+
18+
},
19+
}
20+
21+
func init() {
22+
handlerCmd.AddCommand(handlerMountCmd)
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) 2021 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+
package cmd
6+
7+
import (
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// handlerCmd represents the base command for all syscall handler
12+
var handlerCmd = &cobra.Command{
13+
Use: "handler",
14+
Short: "In-namespace calls for syscall handling",
15+
}
16+
17+
func init() {
18+
rootCmd.AddCommand(handlerCmd)
19+
}

components/workspacekit/cmd/rings.go

Lines changed: 177 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"fmt"
1010
"io/ioutil"
11+
"net"
1112
"os"
1213
"os/exec"
1314
"os/signal"
@@ -17,11 +18,13 @@ import (
1718
"time"
1819

1920
"github.com/gitpod-io/gitpod/common-go/log"
21+
"github.com/gitpod-io/gitpod/workspacekit/pkg/seccomp"
2022
daemonapi "github.com/gitpod-io/gitpod/ws-daemon/api"
2123

2224
"github.com/rootless-containers/rootlesskit/pkg/msgutil"
2325
"github.com/rootless-containers/rootlesskit/pkg/sigproxy"
2426
sigproxysignal "github.com/rootless-containers/rootlesskit/pkg/sigproxy/signal"
27+
libseccomp "github.com/seccomp/libseccomp-golang"
2528
"github.com/spf13/cobra"
2629
"golang.org/x/sys/unix"
2730
"google.golang.org/grpc"
@@ -36,6 +39,10 @@ const (
3639
// This time must give ring1 enough time to shut down (see time budgets in supervisor.go),
3740
// and to talk to ws-daemon within the terminationGracePeriod of the workspace pod.
3841
ring1ShutdownTimeout = 20 * time.Second
42+
43+
// ring2StartupTimeout is the maximum time we wait between starting ring2 and its
44+
// attempt to connect to the parent socket.
45+
ring2StartupTimeout = 5 * time.Second
3946
)
4047

4148
var ring0Cmd = &cobra.Command{
@@ -260,20 +267,20 @@ var ring1Cmd = &cobra.Command{
260267
}
261268
}
262269

263-
pipeR, pipeW, err := os.Pipe()
270+
socketFN := filepath.Join(os.TempDir(), fmt.Sprintf("workspacekit-ring1-%d.unix", time.Now().UnixNano()))
271+
skt, err := net.Listen("unix", socketFN)
264272
if err != nil {
265-
log.WithError(err).Error("cannot mount create pipe")
273+
log.WithError(err).Error("cannot create socket for ring2")
266274
failed = true
267275
return
268276
}
269-
defer pipeW.Close()
277+
defer skt.Close()
270278

271-
cmd := exec.Command("/proc/self/exe", "ring2")
279+
cmd := exec.Command("/proc/self/exe", "ring2", socketFN)
272280
cmd.SysProcAttr = &syscall.SysProcAttr{
273281
Pdeathsig: syscall.SIGKILL,
274282
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWPID,
275283
}
276-
cmd.ExtraFiles = []*os.File{pipeR}
277284
cmd.Dir = tmpdir
278285
cmd.Stdin = os.Stdin
279286
cmd.Stdout = os.Stdout
@@ -294,27 +301,107 @@ var ring1Cmd = &cobra.Command{
294301
failed = true
295302
return
296303
}
297-
_, err = client.MountProc(ctx, &daemonapi.MountProcRequest{
298-
Target: procLoc,
299-
Pid: int64(cmd.Process.Pid),
304+
resp, err := client.MountProc(ctx, &daemonapi.MountProcRequest{
305+
Pid: int64(cmd.Process.Pid),
300306
})
301307
if err != nil {
302308
log.WithError(err).Error("cannot mount proc")
303309
failed = true
304310
return
305311
}
306312

313+
// TODO(cw): this mount doesn't work because we need to be in the ring2 mount namespace.
314+
// Use nsenter/mount handler to do this.
315+
err = unix.Mount(resp.Location, procLoc, "", unix.MS_MOVE, "")
316+
if err != nil {
317+
log.WithError(err).WithFields(map[string]interface{}{"loc": resp.Location, "dest": procLoc}).Error("cannot move proc mount")
318+
failed = true
319+
return
320+
}
321+
322+
incoming := make(chan net.Conn, 1)
323+
errc := make(chan error, 1)
324+
go func() {
325+
defer close(incoming)
326+
defer close(errc)
327+
328+
// Accept stops the latest when we close the socket.
329+
c, err := skt.Accept()
330+
if err != nil {
331+
errc <- err
332+
return
333+
}
334+
incoming <- c
335+
}()
336+
var ring2Conn *net.UnixConn
337+
for {
338+
var brek bool
339+
select {
340+
case err = <-errc:
341+
if err != nil {
342+
brek = true
343+
}
344+
case c := <-incoming:
345+
if c == nil {
346+
continue
347+
}
348+
ring2Conn = c.(*net.UnixConn)
349+
brek = true
350+
case <-time.After(ring2StartupTimeout):
351+
err = fmt.Errorf("ring2 did not connect in time")
352+
brek = true
353+
}
354+
if brek {
355+
break
356+
}
357+
}
358+
if err != nil {
359+
log.WithError(err).Error("ring2 did not connect successfully")
360+
failed = true
361+
return
362+
}
363+
307364
log.Info("signaling to child process")
308-
_, err = msgutil.MarshalToWriter(pipeW, ringSyncMsg{
365+
_, err = msgutil.MarshalToWriter(ring2Conn, ringSyncMsg{
309366
Stage: 1,
310367
Rootfs: tmpdir,
311368
})
312369
if err != nil {
313-
log.WithError(err).Error("cannot send signal to child process")
370+
log.WithError(err).Error("cannot send ring sync msg to ring2")
371+
failed = true
372+
return
373+
}
374+
375+
log.Info("awaiting seccomp fd")
376+
scmpfd, err := receiveSeccmpFd(ring2Conn)
377+
if err != nil {
378+
log.WithError(err).Error("did not receive seccomp fd from ring2")
314379
failed = true
315380
return
316381
}
317382

383+
if scmpfd == 0 {
384+
log.Warn("received 0 as ring2 seccomp fd - syscall handling is broken")
385+
} else {
386+
stp, errchan := seccomp.Handle(scmpfd, cmd.Process.Pid, client)
387+
defer close(stp)
388+
go func() {
389+
t := time.NewTicker(10 * time.Millisecond)
390+
defer t.Stop()
391+
for {
392+
// We use the ticker to rate-limit the errors from the syscall handler.
393+
// We're only handling low-frequency syscalls (e.g. mount), and don't want
394+
// the handler to hog the CPU because it fails on its fd.
395+
<-t.C
396+
err := <-errchan
397+
if err == nil {
398+
return
399+
}
400+
log.WithError(err).Warn("syscall handler error")
401+
}
402+
}()
403+
}
404+
318405
err = cmd.Wait()
319406
if err != nil {
320407
log.WithError(err).Error("unexpected exit")
@@ -324,12 +411,52 @@ var ring1Cmd = &cobra.Command{
324411
},
325412
}
326413

414+
func receiveSeccmpFd(conn *net.UnixConn) (libseccomp.ScmpFd, error) {
415+
buf := make([]byte, unix.CmsgSpace(4))
416+
417+
err := conn.SetDeadline(time.Now().Add(5 * time.Second))
418+
if err != nil {
419+
return 0, err
420+
}
421+
422+
f, err := conn.File()
423+
if err != nil {
424+
return 0, err
425+
}
426+
defer f.Close()
427+
connfd := int(f.Fd())
428+
429+
_, _, _, _, err = unix.Recvmsg(connfd, nil, buf, 0)
430+
if err != nil {
431+
return 0, err
432+
}
433+
434+
msgs, err := unix.ParseSocketControlMessage(buf)
435+
if err != nil {
436+
return 0, err
437+
}
438+
if len(msgs) != 1 {
439+
return 0, fmt.Errorf("expected a single socket control message")
440+
}
441+
442+
fds, err := unix.ParseUnixRights(&msgs[0])
443+
if err != nil {
444+
return 0, err
445+
}
446+
if len(fds) == 0 {
447+
return 0, fmt.Errorf("expected a single socket FD")
448+
}
449+
450+
return libseccomp.ScmpFd(fds[0]), nil
451+
}
452+
327453
var ring2Opts struct {
328454
SupervisorPath string
329455
}
330456
var ring2Cmd = &cobra.Command{
331-
Use: "ring2",
457+
Use: "ring2 <ring1Socket>",
332458
Short: "starts ring2",
459+
Args: cobra.ExactArgs(1),
333460
Run: func(_cmd *cobra.Command, args []string) {
334461
log.Init(ServiceName, Version, true, true)
335462
log := log.WithField("ring", 2)
@@ -343,13 +470,21 @@ var ring2Cmd = &cobra.Command{
343470
sleepForDebugging()
344471
}()
345472

346-
// wait for /proc by listening on the parent's pipe.
347-
// fd=3 is the pipe's FD passed in from the parent via extraFiles.
348-
pipeR := os.NewFile(uintptr(3), "")
473+
// we talk to ring1 using a Unix socket, so that we can send the seccomp fd across.
474+
rconn, err := net.Dial("unix", args[0])
475+
if err != nil {
476+
log.WithError(err).Error("cannot connect to parent")
477+
failed = true
478+
return
479+
}
480+
conn := rconn.(*net.UnixConn)
481+
log.Info("connected to parent socket")
482+
483+
// Before we do anything, we wait for the parent to make /proc available to us.
349484
var msg ringSyncMsg
350-
_, err := msgutil.UnmarshalFromReader(pipeR, &msg)
485+
_, err = msgutil.UnmarshalFromReader(conn, &msg)
351486
if err != nil {
352-
log.WithError(err).Error("cannot read from parent pipe")
487+
log.WithError(err).Error("cannot read parent message")
353488
failed = true
354489
return
355490
}
@@ -366,6 +501,27 @@ var ring2Cmd = &cobra.Command{
366501
return
367502
}
368503

504+
// Now that we're in our new root filesystem, including proc and all, we can load
505+
// our seccomp filter, and tell our parent about it.
506+
scmpFd, err := seccomp.LoadFilter()
507+
if err != nil {
508+
log.WithError(err).Warn("cannot load seccomp filter - syscall handling will be broken")
509+
}
510+
connf, err := conn.File()
511+
if err != nil {
512+
log.WithError(err).Error("cannot get parent socket fd")
513+
failed = true
514+
return
515+
}
516+
sktfd := int(connf.Fd())
517+
err = unix.Sendmsg(sktfd, nil, unix.UnixRights(int(scmpFd)), nil, 0)
518+
connf.Close()
519+
if err != nil {
520+
log.WithError(err).Error("cannot send seccomp fd")
521+
failed = true
522+
return
523+
}
524+
369525
err = cap.SetGroups(33333)
370526
if err != nil {
371527
log.WithError(err).Error("cannot setgid")
@@ -380,7 +536,7 @@ var ring2Cmd = &cobra.Command{
380536
}
381537
err = unix.Exec(ring2Opts.SupervisorPath, []string{"supervisor", "run", "--inns"}, os.Environ())
382538
if err != nil {
383-
log.WithError(err).Error("cannot exec")
539+
log.WithError(err).WithField("cmd", ring2Opts.SupervisorPath).Error("cannot exec")
384540
failed = true
385541
return
386542
}
@@ -497,11 +653,12 @@ func init() {
497653

498654
supervisorPath := os.Getenv("GITPOD_WORKSPACEKIT_SUPERVISOR_PATH")
499655
if supervisorPath == "" {
500-
wd, err := os.Getwd()
656+
wd, err := os.Executable()
501657
if err == nil {
502-
supervisorPath = "supervisor"
503-
} else {
658+
wd = filepath.Dir(wd)
504659
supervisorPath = filepath.Join(wd, "supervisor")
660+
} else {
661+
supervisorPath = "/.supervisor/supervisor"
505662
}
506663
}
507664

0 commit comments

Comments
 (0)