Skip to content

Commit 18f5ba7

Browse files
committed
Stream processor for image layer unpack
Created binary stream processors to extract tar layers and then convert to a VHD for LCOW or WCOW (via tar2ext4.Convert or ociwclayer.ImportLayerFromTar, respectively). Currently, binary re-execs itself using a restricted token with limited privileges and reduced access. Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com>
1 parent 6b84925 commit 18f5ba7

181 files changed

Lines changed: 33657 additions & 1241 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/differ/app.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"errors"
7+
"fmt"
8+
"io/ioutil"
9+
"os"
10+
"os/exec"
11+
"syscall"
12+
13+
"github.com/Microsoft/go-winio/pkg/etw"
14+
"github.com/Microsoft/go-winio/pkg/etwlogrus"
15+
"github.com/Microsoft/go-winio/pkg/guid"
16+
"github.com/sirupsen/logrus"
17+
cli "github.com/urfave/cli/v2"
18+
"go.opencensus.io/trace"
19+
"go.opencensus.io/trace/propagation"
20+
"golang.org/x/sys/windows"
21+
22+
"github.com/Microsoft/hcsshim/internal/log"
23+
"github.com/Microsoft/hcsshim/internal/logfields"
24+
"github.com/Microsoft/hcsshim/internal/oc"
25+
"github.com/Microsoft/hcsshim/internal/winapi"
26+
)
27+
28+
/*
29+
todo:
30+
restricted token
31+
run on not default-desktop
32+
https://docs.microsoft.com/en-us/windows/win32/secauthz/restricted-tokens
33+
*/
34+
35+
var appCommands = []*cli.Command{
36+
decompressCommand,
37+
convertCommand,
38+
wclayerCommand,
39+
}
40+
41+
func app() *cli.App {
42+
app := &cli.App{
43+
Name: "hcsshim-differ",
44+
Usage: "Containerd stream processors for applying for Windows container (WCOW and LCOW) diffs and layers",
45+
Commands: appCommands,
46+
ExitErrHandler: errHandler,
47+
Before: beforeApp,
48+
Flags: []cli.Flag{
49+
&cli.BoolFlag{
50+
Name: reExecFlagName,
51+
Usage: "set after re-execing into this command with proper permissions and environment variables",
52+
Hidden: true,
53+
},
54+
},
55+
}
56+
return app
57+
}
58+
59+
func beforeApp(c *cli.Context) (err error) {
60+
if err := setupLogging(); err != nil {
61+
return fmt.Errorf("logging setup: %w", err)
62+
}
63+
return nil
64+
}
65+
66+
func errHandler(c *cli.Context, err error) {
67+
if err == nil {
68+
return
69+
}
70+
// reexec will return an exit code, so check for that edge case and
71+
if ee := (&exec.ExitError{}); errors.As(err, &ee) {
72+
err = cli.Exit("", ee.ExitCode())
73+
} else {
74+
n := c.App.Name
75+
if nn := c.Command.FullName(); nn != "" {
76+
n += " " + nn
77+
}
78+
err = cli.Exit(fmt.Errorf("%s: %w", n, err), 1)
79+
}
80+
cli.HandleExitCoder(err)
81+
}
82+
83+
// actionReExecWrapper returns a cli.ActionFunc that first checks if the re-exec flag
84+
// is set, and if not, re-execs the command, with the flag set, and a stripped
85+
// set of permissions. If r != nil, it will be run after creating the cmd to re-exec
86+
func actionReExecWrapper(f cli.ActionFunc, opts ...reExecOpts) cli.ActionFunc {
87+
conf := reExecConfig{}
88+
var confErr error // cant return an error here, so punt error checking till action
89+
opts = append(opts, defaultPrivileges)
90+
for _, o := range opts {
91+
if confErr := o(&conf); confErr != nil {
92+
break
93+
}
94+
}
95+
return func(c *cli.Context) (err error) {
96+
if confErr != nil {
97+
return fmt.Errorf("could not properly initialize re-exec config: %w", confErr)
98+
}
99+
100+
if c.Bool(reExecFlagName) {
101+
if sc, ok := spanContextFromEnv(); ok {
102+
// rather than starting a new span, fake it by adding span and trace ID to all logs
103+
c.Context, _ = log.S(c.Context, logrus.Fields{
104+
logfields.TraceID: sc.TraceID.String(),
105+
logfields.SpanID: sc.SpanID.String(),
106+
})
107+
}
108+
return f(c)
109+
}
110+
111+
span := startSpan(c, c.App.Name+"::"+c.Command.FullName())
112+
defer span.End()
113+
defer func() { oc.SetSpanStatus(span, err) }()
114+
115+
cmd := exec.CommandContext(c.Context, os.Args[0], append([]string{"-" + reExecFlagName}, os.Args[1:]...)...)
116+
cmd.Stdin = os.Stdin
117+
cmd.Stdout = os.Stdout
118+
cmd.Stderr = os.Stderr
119+
120+
cmd.Env = []string{}
121+
for _, k := range []string{
122+
mediaTypeEnvVar,
123+
payloadPineEnvVar,
124+
logLevelEnvVar,
125+
logETWProviderEnvVar,
126+
} {
127+
if v, ok := os.LookupEnv(k); ok {
128+
cmd.Env = append(cmd.Env, k+"="+v)
129+
}
130+
}
131+
if sc, ok := spanContextToString(c.Context); ok {
132+
cmd.Env = append(cmd.Env, spanContextEnvVar+"="+sc)
133+
}
134+
135+
var etoken windows.Token
136+
if err := windows.OpenProcessToken(windows.CurrentProcess(),
137+
windows.TOKEN_DUPLICATE|windows.TOKEN_ASSIGN_PRIMARY|windows.TOKEN_QUERY|windows.TOKEN_WRITE,
138+
&etoken,
139+
); err != nil {
140+
return fmt.Errorf("could not open process token: %w", err)
141+
}
142+
143+
log.G(c.Context).WithField("privileges", conf.keepPrivleges).Debug("needed privileges")
144+
deleteLUIDs, err := privilegesToDelete(etoken, conf.keepPrivleges)
145+
if err != nil {
146+
return fmt.Errorf("could not get privileges to delete: %w", err)
147+
}
148+
149+
var token windows.Token
150+
if err := winapi.CreateRestrictedToken(
151+
etoken,
152+
0, // flags
153+
nil, // SIDs to disable
154+
deleteLUIDs,
155+
nil, // SIDs to restrict
156+
&token,
157+
); err != nil {
158+
return fmt.Errorf("could not create restricted token: %w", err)
159+
}
160+
defer token.Close()
161+
cmd.SysProcAttr = &syscall.SysProcAttr{
162+
HideWindow: true,
163+
Token: syscall.Token(token),
164+
}
165+
166+
if err := cmd.Start(); err != nil {
167+
return fmt.Errorf("could not start command: %w", err)
168+
}
169+
return cmd.Wait()
170+
}
171+
}
172+
173+
func setupLogging() error {
174+
logrus.SetOutput(ioutil.Discard)
175+
logrus.AddHook(log.NewHook())
176+
if lvl, err := logrus.ParseLevel(os.Getenv(logLevelEnvVar)); err == nil {
177+
logrus.SetLevel(lvl)
178+
}
179+
180+
f := func(guid.GUID, etw.ProviderState, etw.Level, uint64, uint64, uintptr) {}
181+
prov := "Containerd"
182+
if p, ok := os.LookupEnv(logETWProviderEnvVar); ok {
183+
prov = p
184+
}
185+
provider, err := etw.NewProvider(prov, f)
186+
if err != nil {
187+
return err
188+
}
189+
190+
hook, err := etwlogrus.NewHookFromProvider(provider)
191+
if err != nil {
192+
return err
193+
}
194+
logrus.AddHook(hook)
195+
196+
trace.ApplyConfig(trace.Config{DefaultSampler: oc.DefaultSampler})
197+
trace.RegisterExporter(&oc.LogrusExporter{})
198+
return nil
199+
}
200+
201+
func startSpan(c *cli.Context, n string, o ...trace.StartOption) (s *trace.Span) {
202+
if sc, ok := spanContextFromEnv(); ok {
203+
c.Context, s = oc.StartSpanWithRemoteParent(c.Context, n, sc, o...)
204+
} else {
205+
c.Context, s = oc.StartSpan(c.Context, n, o...)
206+
}
207+
return s
208+
}
209+
210+
func spanContextFromEnv() (sc trace.SpanContext, ok bool) {
211+
s, ok := os.LookupEnv(spanContextEnvVar)
212+
if !ok {
213+
return sc, ok
214+
}
215+
// b := make([]byte, base64.StdEncoding.DecodedLen(len(s)))
216+
// if _, err := base64.StdEncoding.Decode(b, []byte(s)); err != nil {
217+
// return sc, false
218+
// }
219+
b, err := base64.StdEncoding.DecodeString(s)
220+
if err != nil {
221+
return sc, false
222+
}
223+
return propagation.FromBinary(b)
224+
}
225+
226+
func spanContextToString(ctx context.Context) (string, bool) {
227+
span := trace.FromContext(ctx)
228+
if span == nil {
229+
return "", false
230+
}
231+
b := propagation.Binary(span.SpanContext())
232+
return base64.StdEncoding.EncodeToString(b), true
233+
}

cmd/differ/app_opts.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
type reExecOpts func(*reExecConfig) error
4+
5+
// reExecOpts are options to change how a subcommand is re-execed
6+
type reExecConfig struct {
7+
keepPrivleges []string
8+
}
9+
10+
var defaultPrivileges = withPrivileges([]string{"SeChangeNotifyPrivilege"})
11+
12+
func withPrivileges(keep []string) reExecOpts {
13+
return func(o *reExecConfig) error {
14+
o.keepPrivleges = append(o.keepPrivleges, keep...)
15+
return nil
16+
}
17+
}

cmd/differ/decompress.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build windows
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"io"
8+
"os"
9+
10+
"github.com/containerd/containerd/archive/compression"
11+
"github.com/containerd/containerd/images"
12+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
13+
"github.com/urfave/cli/v2"
14+
)
15+
16+
var decompressCommand = &cli.Command{
17+
Name: "decompress",
18+
Aliases: []string{"decomp", "d"},
19+
Usage: fmt.Sprintf("Decompress a %q stream into a %q", images.MediaTypeDockerSchema2LayerGzip, ocispec.MediaTypeImageLayer),
20+
Action: actionReExecWrapper(decompress),
21+
}
22+
23+
func decompress(c *cli.Context) error {
24+
dc, err := compression.DecompressStream(os.Stdin)
25+
if err != nil {
26+
return fmt.Errorf("decompress stream creation: %w", err)
27+
}
28+
if _, err = io.Copy(os.Stdout, dc); err != nil {
29+
return fmt.Errorf("io copy to std out: %w", err)
30+
}
31+
return nil
32+
}

cmd/differ/main.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io/ioutil"
7+
"log"
8+
"os"
9+
10+
"github.com/Microsoft/go-winio"
11+
"github.com/gogo/protobuf/proto"
12+
"github.com/gogo/protobuf/types"
13+
14+
"github.com/Microsoft/hcsshim/cmd/differ/payload"
15+
)
16+
17+
const reExecFlagName = "reexec"
18+
19+
const (
20+
mediaTypeEnvVar = "STREAM_PROCESSOR_MEDIATYPE"
21+
payloadPineEnvVar = "STREAM_PROCESSOR_PIPE"
22+
logLevelEnvVar = "STREAM_PROCESSOR_LOG_LEVEL"
23+
logETWProviderEnvVar = "STREAM_PROCESSOR_LOG_ETW_PROVIDER"
24+
spanContextEnvVar = "STREAM_PROCESSOR_SPAN_CONTEXT"
25+
)
26+
27+
func main() {
28+
// Run() should not return an error because of ExitErrHandler, but just in case ...
29+
if err := app().Run(os.Args); err != nil {
30+
log.New(os.Stderr, "", 0).Fatal(err)
31+
}
32+
}
33+
34+
func getMediaType() string {
35+
return os.Getenv(mediaTypeEnvVar)
36+
}
37+
38+
func getPayload(ctx context.Context, p payload.FromAny) error {
39+
b, err := readAllEnvPipe(ctx, payloadPineEnvVar)
40+
if err != nil || b == nil {
41+
return err
42+
}
43+
44+
a := &types.Any{}
45+
if err := proto.Unmarshal(b, a); err != nil {
46+
return fmt.Errorf("proto.Unmarshal(): %w", err)
47+
}
48+
return p.FromAny(a)
49+
}
50+
51+
func readAllEnvPipe(ctx context.Context, env string) ([]byte, error) {
52+
n := os.Getenv(env)
53+
if n == "" {
54+
return nil, nil
55+
}
56+
57+
p, err := winio.DialPipeContext(ctx, n)
58+
if err != nil {
59+
return nil, fmt.Errorf("dial pipe %s from env var %v: %w", n, env, err)
60+
}
61+
defer p.Close()
62+
63+
return ioutil.ReadAll(p)
64+
}

cmd/differ/mediatype/mediatype.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// This package deals with media types and extensions specific to Windows containers (LCOW and WCOW).
2+
package mediatype
3+
4+
const (
5+
ExtensionIsolated = "isolated"
6+
ExtensionLCOW = "lcow"
7+
ExtensionWCOW = "wcow"
8+
9+
MediaTypeMicrosoftBase = "application/vnd.microsoft"
10+
MediaTypeMicrosoftImageLayerVHD = "application/vnd.microsoft.image.layer.v1.vhd"
11+
MediaTypeMicrosoftImageLayerExt4 = "application/vnd.microsoft.image.layer.v1.vhd+ext4"
12+
MediaTypeMicrosoftImageLayerWCLayer = "application/vnd.microsoft.image.layer.v1.vhd+wclayer"
13+
)

cmd/differ/payload/payload.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package payload
2+
3+
import "github.com/gogo/protobuf/types"
4+
5+
type FromAny interface {
6+
FromAny(a *types.Any) error
7+
}

0 commit comments

Comments
 (0)