github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/kernel/module/module.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package module extracts .ko files from kernel modules. 16 package module 17 18 import ( 19 "bytes" 20 "context" 21 "debug/elf" 22 "errors" 23 "fmt" 24 "io" 25 "path/filepath" 26 "strings" 27 28 "github.com/google/osv-scalibr/extractor" 29 "github.com/google/osv-scalibr/extractor/filesystem" 30 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 31 modulemeta "github.com/google/osv-scalibr/extractor/filesystem/os/kernel/module/metadata" 32 "github.com/google/osv-scalibr/extractor/filesystem/os/osrelease" 33 "github.com/google/osv-scalibr/inventory" 34 "github.com/google/osv-scalibr/log" 35 "github.com/google/osv-scalibr/plugin" 36 "github.com/google/osv-scalibr/stats" 37 ) 38 39 const ( 40 // Name is the unique name of this extractor. 41 Name = "os/kernel/module" 42 43 // defaultMaxFileSizeBytes is the maximum file size an extractor will unmarshal. 44 // If Extract gets a bigger file, it will return an error. 45 defaultMaxFileSizeBytes = 100 * units.MiB 46 ) 47 48 // Config is the configuration for the Extractor. 49 type Config struct { 50 // Stats is a stats collector for reporting metrics. 51 Stats stats.Collector 52 // MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If 53 // `FileRequired` gets a bigger file, it will return false, 54 MaxFileSizeBytes int64 55 } 56 57 // DefaultConfig returns the default configuration for the kernel module extractor. 58 func DefaultConfig() Config { 59 return Config{ 60 Stats: nil, 61 MaxFileSizeBytes: defaultMaxFileSizeBytes, 62 } 63 } 64 65 // Extractor extracts packages from kernel module files (.ko). 66 type Extractor struct { 67 stats stats.Collector 68 maxFileSizeBytes int64 69 } 70 71 // New returns a kernel module extractor. 72 func New(cfg Config) *Extractor { 73 return &Extractor{ 74 stats: cfg.Stats, 75 maxFileSizeBytes: cfg.MaxFileSizeBytes, 76 } 77 } 78 79 // NewDefault returns an extractor with the default config settings. 80 func NewDefault() filesystem.Extractor { return New(DefaultConfig()) } 81 82 // Config returns the configuration of the extractor. 83 func (e Extractor) Config() Config { 84 return Config{ 85 Stats: e.stats, 86 MaxFileSizeBytes: e.maxFileSizeBytes, 87 } 88 } 89 90 // Name of the extractor. 91 func (e Extractor) Name() string { return Name } 92 93 // Version of the extractor. 94 func (e Extractor) Version() int { return 0 } 95 96 // Requirements of the extractor. 97 func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } 98 99 // FileRequired returns true if the specified file matches the *.ko file patterns. 100 func (e Extractor) FileRequired(api filesystem.FileAPI) bool { 101 path := api.Path() 102 103 if !strings.HasSuffix(filepath.Base(path), ".ko") { 104 return false 105 } 106 107 fileinfo, err := api.Stat() 108 if err != nil { 109 return false 110 } 111 112 if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes { 113 e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded) 114 return false 115 } 116 117 e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultOK) 118 return true 119 } 120 121 func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) { 122 if e.stats == nil { 123 return 124 } 125 e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{ 126 Path: path, 127 Result: result, 128 FileSizeBytes: fileSizeBytes, 129 }) 130 } 131 132 // Extract extracts packages from .ko files passed through the scan input. 133 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 134 pkgs, err := e.extractFromInput(input) 135 136 if e.stats != nil { 137 var fileSizeBytes int64 138 if input.Info != nil { 139 fileSizeBytes = input.Info.Size() 140 } 141 e.stats.AfterFileExtracted(e.Name(), &stats.FileExtractedStats{ 142 Path: input.Path, 143 Result: filesystem.ExtractorErrorToFileExtractedResult(err), 144 FileSizeBytes: fileSizeBytes, 145 }) 146 } 147 return inventory.Inventory{Packages: pkgs}, err 148 } 149 150 func (e Extractor) extractFromInput(input *filesystem.ScanInput) ([]*extractor.Package, error) { 151 packages := []*extractor.Package{} 152 153 m, err := osrelease.GetOSRelease(input.FS) 154 if err != nil { 155 log.Errorf("osrelease.ParseOsRelease(): %v", err) 156 } 157 158 var readerAt io.ReaderAt 159 if fileWithReaderAt, ok := input.Reader.(io.ReaderAt); ok { 160 readerAt = fileWithReaderAt 161 } else { 162 buf := bytes.NewBuffer([]byte{}) 163 _, err := io.Copy(buf, input.Reader) 164 if err != nil { 165 return []*extractor.Package{}, err 166 } 167 readerAt = bytes.NewReader(buf.Bytes()) 168 } 169 elfFile, err := elf.NewFile(readerAt) 170 if err != nil { 171 return nil, fmt.Errorf("failed to parse ELF file: %w", err) 172 } 173 174 // Note that it's possible to strip section names from the binary so we might not be able 175 // to identify malicious modules if the author intentionally stripped the module name. 176 section := elfFile.Section(".modinfo") 177 if section == nil { 178 return nil, errors.New("no .modinfo section found") 179 } 180 181 sectionData, err := section.Data() 182 if err != nil { 183 return nil, fmt.Errorf("failed to read .modinfo section: %w", err) 184 } 185 186 var metadata modulemeta.Metadata 187 188 // Sections are delimited by null bytes (\x00) 189 for line := range bytes.SplitSeq(sectionData, []byte{'\x00'}) { 190 if len(line) == 0 { 191 continue 192 } 193 194 entry := strings.SplitN(string(line), "=", 2) 195 if len(entry) != 2 { 196 return nil, fmt.Errorf("malformed .modinfo entry, expected 'key=value' but got: %s", string(line)) 197 } 198 199 key := entry[0] 200 value := entry[1] 201 202 switch key { 203 case "name": 204 metadata.PackageName = value 205 case "version": 206 metadata.PackageVersion = value 207 case "srcversion": 208 metadata.PackageSourceVersionIdentifier = value 209 case "vermagic": 210 metadata.PackageVermagic = strings.TrimSpace(value) 211 case "author": 212 metadata.PackageAuthor = value 213 } 214 } 215 216 metadata.OSID = m["ID"] 217 metadata.OSVersionCodename = m["VERSION_CODENAME"] 218 metadata.OSVersionID = m["VERSION_ID"] 219 220 p := &extractor.Package{ 221 Name: metadata.PackageName, 222 Version: metadata.PackageVersion, 223 Metadata: &metadata, 224 Locations: []string{input.Path}, 225 } 226 227 packages = append(packages, p) 228 229 return packages, nil 230 }