Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions sensor/common/scan/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ func (s *LocalScan) EnrichLocalImageInNamespace(ctx context.Context, centralClie

// Check if there is a local Scanner.
// No need to continue if there is no local Scanner.
if s.scannerClientSingleton() == nil {
scannerClient := s.scannerClientSingleton()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at all of these changes in this file - is there actually any change happening here? If I'm reading this all correctly aren't both implementations doing the exact same thing.

I'll admit the new way feels cleaner and more readable (and doesn't rely on implicit global state) but I just want to make sure I'm not missing something :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, nvm - I see that this is the fix for the TOCTOU mentioned.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s.scannerClientSingleton() could return non-nil on the first invocation and nil on the second invocation in scanImage which would cause a panic. This PR re-uses the returned object to avoid that.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies, didn't see your followup when I was typing ^^ message.

if scannerClient == nil {
return nil, errors.Join(ErrNoLocalScanner, ErrEnrichNotStarted)
}

Expand Down Expand Up @@ -197,7 +198,7 @@ func (s *LocalScan) EnrichLocalImageInNamespace(ctx context.Context, centralClie
// Only proceed if metadata was fetched successfully
if errorList.Empty() {
// Perform partial scan (image analysis / identify components) via local scanner.
scannerResp = s.fetchImageAnalysis(ctx, errorList, reg, pullSourceImage)
scannerResp = s.fetchImageAnalysis(ctx, errorList, reg, pullSourceImage, scannerClient)

// Fetch signatures associated with image from registry. Do this even if the scan above failed, because that
// doesn't necessarily mean signatures cannot be fetched
Expand Down Expand Up @@ -413,9 +414,9 @@ func (s *LocalScan) enrichImageWithMetadata(ctx context.Context, errorList *erro
}

// fetchImageAnalysis analyzes an image via the local scanner.
func (s *LocalScan) fetchImageAnalysis(ctx context.Context, errorList *errorhelpers.ErrorList, registry registryTypes.ImageRegistry, image *storage.Image) *scannerclient.ImageAnalysis {
func (s *LocalScan) fetchImageAnalysis(ctx context.Context, errorList *errorhelpers.ErrorList, registry registryTypes.ImageRegistry, image *storage.Image, scannerClient scannerclient.ScannerClient) *scannerclient.ImageAnalysis {
// Scan the image via local scanner.
scannerResp, err := s.scanImg(ctx, image, registry, s.scannerClientSingleton())
scannerResp, err := s.scanImg(ctx, image, registry, scannerClient)
if err != nil {
log.Debugf("Scan for image %q with id %v failed: %v", image.GetName().GetFullName(), image.GetId(), err)
image.Notes = append(image.Notes, storage.Image_MISSING_SCAN_DATA)
Expand Down
59 changes: 59 additions & 0 deletions sensor/common/scan/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,65 @@ func (suite *scanTestSuite) TestLocalEnrichment() {
suite.Assert().True(getRegistriesTriggered, "get registry was not triggered")
}

// TestScannerClientCapturedOnce is a regression guard for a TOCTOU nil-client
// dereference: the scanner client singleton must be consulted once per
// enrichment and the client captured at the nil-check must be the one handed to
// the scan. Otherwise a concurrent reset of the singleton (e.g. on Central
// reconnect) could return non-nil at the check but nil at the scan, panicking
// when a method is invoked on the nil client.
func (suite *scanTestSuite) TestScannerClientCapturedOnce() {
fakeRegStore := &fakeRegistryStore{}
mirrorStore := mirrorStoreMocks.NewMockStore(gomock.NewController(suite.T()))

// Returns a valid client on the first call, then nil, simulating a
// concurrent singleton reset occurring mid-enrichment.
capturedClient := &emptyClient{}
singletonCalls := 0
singleton := func() scannerclient.ScannerClient {
singletonCalls++
if singletonCalls == 1 {
return capturedClient
}
return nil
}

var scannedWithClient scannerclient.ScannerClient
recordingScan := func(_ context.Context, _ *storage.Image,
reg registryTypes.ImageRegistry, client scannerclient.ScannerClient) (*scannerclient.ImageAnalysis, error) {
scannedWithClient = client
return successfulScan(context.Background(), nil, reg, client)
}

scan := LocalScan{
scanImg: recordingScan,
fetchSignaturesWithRetry: successfulFetchSignatures,
getPullSecretRegistries: func(*storage.ImageName, string, []string) ([]registryTypes.ImageRegistry, error) {
return []registryTypes.ImageRegistry{&fakeRegistry{fail: false}}, nil
},
getGlobalRegistries: func(*storage.ImageName) ([]registryTypes.ImageRegistry, error) {
return []registryTypes.ImageRegistry{&fakeRegistry{fail: false}}, nil
},
scannerClientSingleton: singleton,
scanSemaphore: semaphore.NewWeighted(10),
getCentralRegistries: fakeRegStore.GetMatchingCentralRegistryIntegrations,
mirrorStore: mirrorStore,
maxSemaphoreWaitTime: defaultMaxSemaphoreWaitTime,
}

containerImg, err := utils.GenerateImageFromString("docker.io/nginx")
suite.Require().NoError(err, "failed creating test image")

imageServiceClient := suite.createMockImageServiceClient(types.ToImage(containerImg), false)
mirrorStore.EXPECT().PullSources(containerImg.GetName().GetFullName())

// Must not panic even though the singleton starts returning nil after the check.
_, err = scan.EnrichLocalImageInNamespace(context.Background(), imageServiceClient, genScanReq(containerImg, "fake-namespace", "", false))
suite.Require().NoError(err, "unexpected error when enriching image")

suite.Assert().Equal(1, singletonCalls, "scanner client singleton should be consulted exactly once")
suite.Assert().Same(capturedClient, scannedWithClient, "scan should use the client captured at the nil-check")
}

func (suite *scanTestSuite) TestEnrichImageFailures() {
type testCase struct {
scanImg func(ctx context.Context, image *storage.Image,
Expand Down
103 changes: 0 additions & 103 deletions sensor/common/scannerclient/grpc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,18 @@ package scannerclient
import (
"context"
"fmt"
"strings"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/pkg/errors"
v4 "github.com/stackrox/rox/generated/internalapi/scanner/v4"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/clientconn"
"github.com/stackrox/rox/pkg/env"
"github.com/stackrox/rox/pkg/images/utils"
"github.com/stackrox/rox/pkg/logging"
"github.com/stackrox/rox/pkg/mtls"
"github.com/stackrox/rox/pkg/registries/types"
pkgscanner "github.com/stackrox/rox/pkg/scannerv4"
"github.com/stackrox/rox/pkg/scannerv4/client"
"github.com/stackrox/rox/sensor/common/centralcabundle"
scannerV1 "github.com/stackrox/scanner/generated/scanner/api/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

var (
Expand All @@ -45,13 +39,6 @@ type ImageAnalysis struct {
V4Contents *v4.Contents
}

// v2Client is the client for StackRox Scanner based on Clair V2, also
// known as Scanner V2.
type v2Client struct {
scannerV1.ImageScanServiceClient
conn *grpc.ClientConn
}

// v4Client is the client for StackRox Scanner Indexer, also
// known as Scanner V4 Indexer.
type v4Client struct {
Expand Down Expand Up @@ -103,57 +90,6 @@ func (i *ImageAnalysis) GetIndexerVersion() string {
return ""
}

// getScannerEndpoint reads and validate the Scanner gRPC endpoint setting. If
// the endpoint is empty or not configured properly (invalid) the value is
// returned and error will be set.
func getScannerEndpoint(s env.Setting) (string, error) {
e := s.Setting()
if e == "" {
return e, errors.Errorf("%s is not set or empty", s.EnvVar())
}
e = strings.TrimPrefix(e, "https://")
if strings.Contains(e, "://") {
return e, errors.Errorf("%s has unsupported scheme: %s", s.EnvVar(), e)
}
return e, nil
}

// dial the scanner and returns a new ScannerClient. The function is non-blocking and
// returns a non-nil error upon configuration error.
func dial(endpoint string, certID mtls.Subject) (*grpc.ClientConn, error) {
tlsConfig, err := clientconn.TLSConfig(certID, clientconn.TLSConfigOptions{
UseClientCert: clientconn.MustUseClientCert,
})
if err != nil {
return nil, fmt.Errorf("TLS config failed: %w", err)
}
// This is non-blocking. If we ever want this to block, then add the
// grpc.WithBlock() DialOption.
log.Infof("dialing scanner at %s", endpoint)
conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
if err != nil {
return nil, fmt.Errorf("grpc dial failed: %w", err)
}
return conn, nil
}

// dialV2 connect to scanner V1 gRPC and return a new ScannerClient.
func dialV2() (ScannerClient, error) {
endpoint, err := getScannerEndpoint(env.ScannerSlimGRPCEndpoint)
if err != nil {
return nil, err
}
log.Infof("dialing scanner-v2 client: %s", endpoint)
conn, err := dial(endpoint, mtls.ScannerSubject)
if err != nil {
return nil, err
}
return &v2Client{
ImageScanServiceClient: scannerV1.NewImageScanServiceClient(conn),
conn: conn,
}, nil
}

// dialV4 connect to scanner V4 gRPC and return a new ScannerClient.
func dialV4() (ScannerClient, error) {
ctx := context.Background()
Expand All @@ -172,45 +108,6 @@ func dialV4() (ScannerClient, error) {
return &v4Client{client: c}, nil
}

// GetImageAnalysis retrieves the image analysis results for the given image.
func (c *v2Client) GetImageAnalysis(ctx context.Context, image *storage.Image, cfg *types.Config) (*ImageAnalysis, error) {
imgName := image.GetName().GetFullName()

// The WaitForReady option will cause invocations to block (until server ready or
// ctx done/expires) This was added so that on fresh installation of sensor when
// scanner is not ready yet, local scans will not all fail and have to wait for
// next reprocess to succeed
resp, err := c.GetImageComponents(ctx, &scannerV1.GetImageComponentsRequest{
Image: utils.GetFullyQualifiedFullName(image),
Registry: &scannerV1.RegistryData{
Url: cfg.URL,
Username: cfg.Username,
Password: cfg.Password,
Insecure: cfg.Insecure,
},
}, grpc.WaitForReady(true))
if err != nil {
log.Debugf("Unable to get image components from local Scanner for image %s: %v", imgName, err)
return nil, errors.Wrap(err, "getting image components from scanner")
}

log.Debugf("Received image components from local Scanner for image: %q", imgName)

return &ImageAnalysis{
ScanStatus: resp.GetStatus(),
ScanNotes: resp.GetNotes(),
V1Components: resp.GetComponents(),
}, nil
}

// Close closes and cleanup the client connection.
func (c *v2Client) Close() error {
if err := c.conn.Close(); err != nil {
return errors.Wrap(err, "closing v2 scanner gRPC connection")
}
return nil
}

func convertIndexReportToAnalysis(ir *v4.IndexReport, indexerVersion string) *ImageAnalysis {
var st scannerV1.ScanStatus
switch ir.GetState() {
Expand Down
27 changes: 19 additions & 8 deletions sensor/common/scannerclient/singleton.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ var (
)

// GRPCClientSingleton returns a gRPC ScannerClient to a local Scanner.
// Only one ScannerClient per Sensor is required.
// Only one ScannerClient per Sensor is required. A nil client may
// be returned.
func GRPCClientSingleton() ScannerClient {
scannerClientMutex.Lock()
defer scannerClientMutex.Unlock()
Expand All @@ -31,14 +32,24 @@ func GRPCClientSingleton() ScannerClient {
env.LocalImageScanningEnabled.EnvVar())
return nil
}
var err error
if isScannerV4Enabled && centralcaps.Has(centralsensor.ScannerV4Supported) {
log.Info("Creating Scanner V4 client")
scannerClient, err = dialV4()
} else {
log.Info("Creating Scanner V2 client")
scannerClient, err = dialV2()

// Scanner V4 is the only local scanner Sensor connects to. Using it requires
// both Scanner V4 to be installed locally and Central to advertise support
// for Scanner V4.
// When either is missing there is no local scanner to talk to, so we return a nil
// client.
if !isScannerV4Enabled {
log.Warn("Local image scanning is enabled but Scanner V4 is disabled in Sensor; no local scanner will be used")
return nil
}
if !centralcaps.Has(centralsensor.ScannerV4Supported) {
log.Warn("Local image scanning is enabled but Central does not support Scanner V4; no local scanner will be used")
return nil
}

log.Info("Creating Scanner V4 client")
var err error
scannerClient, err = dialV4()
utils.Should(err)

return scannerClient
Expand Down
Loading