-
Notifications
You must be signed in to change notification settings - Fork 513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ New Probe: Memory safety #4499
base: main
Are you sure you want to change the base?
Changes from all commits
62a5754
acc54b1
4c08d96
ebff76f
af1f8a0
d6cf0ab
7e1edaf
56ba2e1
f4eb0fd
83d1fd2
56f3ef7
fac9482
a047af7
7765eae
0c47404
7e27479
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# Copyright 2025 OpenSSF Scorecard Authors | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
id: memorysafe | ||
lifecycle: experimental | ||
short: Flags non memory safe practices in this project. | ||
motivation: > | ||
Memory safety in software should be considered a continuum, rather than being binary. | ||
This probe does not consider a specific ecosystem more or less memory safe than another, but rather tries to surface non memory safe code or practices in the project, in the context of the ecosystems it is using. | ||
implementation: > | ||
The probe is ecosystem-specific and tries to flag non memory safe code blocks in the project by looking at the code and practices used in the project. | ||
It may look for specific memory safety practices, such as the use tools or non memory-safe patterns and code. | ||
The probe supports multiple checks for each ecosystem, and the outcome is based on the results of these checks. | ||
outcome: | ||
- For supported ecosystem, the probe returns OutcomeTrue per safe method or tool. | ||
- For supported ecosystem, the probe returns OutcomeFalse per unsafe code or method. | ||
- If the project has no supported ecosystems, the probe returns OutcomeNotApplicable. | ||
remediation: | ||
onOutcome: False | ||
effort: Medium | ||
text: | ||
- Visit the OpenSSF Memory Safety SIG guidance on how to make your project memory safe. | ||
- Guidance for [Memory-Safe By Default Languages](https://github.com/ossf/Memory-Safety/blob/main/docs/best-practice-memory-safe-by-default-languages.md) | ||
- Guidance for [Non Memory-Safe By Default Languages](https://github.com/ossf/Memory-Safety/blob/main/docs/best-practice-non-memory-safe-by-default-languages.md) | ||
ecosystem: | ||
languages: | ||
- go | ||
- c# | ||
clients: | ||
- github | ||
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just based on file content? so it could support all of them?
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
// Copyright 2025 OpenSSF Scorecard Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package memorysafe | ||
|
||
import ( | ||
"embed" | ||
"fmt" | ||
"go/parser" | ||
"go/token" | ||
"reflect" | ||
"strings" | ||
|
||
"github.com/ossf/scorecard/v5/checker" | ||
"github.com/ossf/scorecard/v5/checks/fileparser" | ||
"github.com/ossf/scorecard/v5/clients" | ||
"github.com/ossf/scorecard/v5/finding" | ||
"github.com/ossf/scorecard/v5/internal/dotnet/csproj" | ||
"github.com/ossf/scorecard/v5/internal/probes" | ||
) | ||
|
||
//go:embed *.yml | ||
var fs embed.FS | ||
|
||
const ( | ||
Probe = "memorysafe" | ||
) | ||
|
||
type languageMemoryCheckConfig struct { | ||
Desc string | ||
|
||
funcPointers []func(client *checker.CheckRequest) ([]finding.Finding, error) | ||
} | ||
|
||
var languageMemorySafeSpecs = map[clients.LanguageName]languageMemoryCheckConfig{ | ||
clients.Go: { | ||
funcPointers: []func(client *checker.CheckRequest) ([]finding.Finding, error){ | ||
checkGoUnsafePackage, | ||
}, | ||
Desc: "Check if Go code uses the unsafe package", | ||
}, | ||
|
||
clients.CSharp: { | ||
funcPointers: []func(client *checker.CheckRequest) ([]finding.Finding, error){ | ||
checkDotnetAllowUnsafeBlocks, | ||
}, | ||
Desc: "Check if C# code uses unsafe blocks", | ||
}, | ||
} | ||
|
||
func init() { | ||
probes.MustRegisterIndependent(Probe, Run) | ||
} | ||
|
||
func Run(raw *checker.CheckRequest) (found []finding.Finding, probeName string, err error) { | ||
prominentLangs := getRepositoryLanguageChecks(raw) | ||
findings := []finding.Finding{} | ||
|
||
for _, lang := range prominentLangs { | ||
for _, langFunc := range lang.funcPointers { | ||
if langFunc == nil { | ||
raw.Dlogger.Warn(&checker.LogMessage{ | ||
Text: fmt.Sprintf("no function pointer found for language %s", lang.Desc), | ||
}) | ||
} | ||
Comment on lines
+72
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the goal for this? it would still panic below because we call We could avoid the panic by adding a My vote is to remove this block, since the warning wouldn't be visible to anyone due to the panic. Thoughts? |
||
langFindings, err := langFunc(raw) | ||
if err != nil { | ||
return nil, Probe, fmt.Errorf("error while running function for language %s: %w", lang.Desc, err) | ||
} | ||
findings = append(findings, langFindings...) | ||
} | ||
} | ||
return findings, Probe, nil | ||
} | ||
|
||
func getRepositoryLanguageChecks(raw *checker.CheckRequest) []languageMemoryCheckConfig { | ||
langs, err := raw.RepoClient.ListProgrammingLanguages() | ||
if err != nil { | ||
raw.Dlogger.Warn(&checker.LogMessage{ | ||
Text: fmt.Sprintf("RepoClient retured error for ListProgrammingLanguages: %v", err), | ||
}) | ||
Comment on lines
+87
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of handling the error and continuing, let's just return it. We're not going to be able to do much without the repo's languages. func getLanguageChecks(raw *checker.CheckRequest) ([]languageMemoryCheckConfig, error) { |
||
return nil | ||
} | ||
if len(langs) == 0 { | ||
return []languageMemoryCheckConfig{} | ||
} | ||
if len(langs) == 1 && langs[0].Name == clients.All { | ||
return getAllLanguages() | ||
} | ||
ret := []languageMemoryCheckConfig{} | ||
for _, language := range langs { | ||
if lang, ok := languageMemorySafeSpecs[clients.LanguageName(strings.ToLower(string(language.Name)))]; ok { | ||
ret = append(ret, lang) | ||
} | ||
} | ||
return ret | ||
} | ||
|
||
func getAllLanguages() []languageMemoryCheckConfig { | ||
allLanguages := make([]languageMemoryCheckConfig, 0, len(languageMemorySafeSpecs)) | ||
for l := range languageMemorySafeSpecs { | ||
allLanguages = append(allLanguages, languageMemorySafeSpecs[l]) | ||
} | ||
return allLanguages | ||
} | ||
|
||
// Golang | ||
|
||
func checkGoUnsafePackage(client *checker.CheckRequest) ([]finding.Finding, error) { | ||
findings := []finding.Finding{} | ||
|
||
if err := fileparser.OnMatchingFileContentDo(client.RepoClient, fileparser.PathMatcher{ | ||
Pattern: "*.go", | ||
CaseSensitive: false, | ||
}, goCodeUsesUnsafePackage, &findings, client.Dlogger); err != nil { | ||
return nil, err | ||
} | ||
if len(findings) == 0 { | ||
found, err := finding.NewWith(fs, Probe, | ||
"Golang code does not use the unsafe package", nil, finding.OutcomeTrue) | ||
Comment on lines
+129
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could also move this (and the c# version) to one combined top level check in |
||
if err != nil { | ||
return nil, fmt.Errorf("create finding: %w", err) | ||
} | ||
findings = append(findings, *found) | ||
} | ||
return findings, nil | ||
} | ||
|
||
func goCodeUsesUnsafePackage(path string, content []byte, args ...interface{}) (bool, error) { | ||
findings, ok := args[0].(*[]finding.Finding) | ||
if !ok { | ||
// panic if it is not correct type | ||
panic(fmt.Sprintf("expected type findings, got %v", reflect.TypeOf(args[0]))) | ||
} | ||
fset := token.NewFileSet() | ||
f, err := parser.ParseFile(fset, "", content, parser.ImportsOnly) | ||
if err != nil { | ||
dl, ok := args[1].(checker.DetailLogger) | ||
if !ok { | ||
// panic if it is not correct type | ||
panic(fmt.Sprintf("expected type checker.DetailLogger, got %v", reflect.TypeOf(args[1]))) | ||
Comment on lines
+149
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should move the type checking out of the |
||
} | ||
|
||
dl.Warn(&checker.LogMessage{ | ||
Text: fmt.Sprintf("malformed golang file: %v", err), | ||
}) | ||
return true, nil | ||
} | ||
for _, i := range f.Imports { | ||
if i.Path.Value == `"unsafe"` { | ||
found, err := finding.NewWith(fs, Probe, | ||
"Golang code uses the unsafe package", &finding.Location{ | ||
Path: path, | ||
}, finding.OutcomeFalse) | ||
Comment on lines
+163
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have much more info than path available if we use : Something like:
and then we have access to line info too and can set |
||
if err != nil { | ||
return false, fmt.Errorf("create finding: %w", err) | ||
} | ||
*findings = append(*findings, *found) | ||
} | ||
} | ||
|
||
return true, nil | ||
} | ||
|
||
// CSharp | ||
|
||
func checkDotnetAllowUnsafeBlocks(client *checker.CheckRequest) ([]finding.Finding, error) { | ||
findings := []finding.Finding{} | ||
|
||
if err := fileparser.OnMatchingFileContentDo(client.RepoClient, fileparser.PathMatcher{ | ||
Pattern: "*.csproj", | ||
CaseSensitive: false, | ||
}, csProjAllosUnsafeBlocks, &findings, client.Dlogger); err != nil { | ||
return nil, err | ||
} | ||
if len(findings) == 0 { | ||
found, err := finding.NewWith(fs, Probe, | ||
"C# code does not allow unsafe blocks", nil, finding.OutcomeTrue) | ||
if err != nil { | ||
return nil, fmt.Errorf("create finding: %w", err) | ||
} | ||
findings = append(findings, *found) | ||
} | ||
return findings, nil | ||
} | ||
|
||
func csProjAllosUnsafeBlocks(path string, content []byte, args ...interface{}) (bool, error) { | ||
findings, ok := args[0].(*[]finding.Finding) | ||
if !ok { | ||
// panic if it is not correct type | ||
panic(fmt.Sprintf("expected type findings, got %v", reflect.TypeOf(args[0]))) | ||
} | ||
unsafe, err := csproj.IsAllowUnsafeBlocksEnabled(content) | ||
if err != nil { | ||
dl, ok := args[1].(checker.DetailLogger) | ||
if !ok { | ||
// panic if it is not correct type | ||
panic(fmt.Sprintf("expected type checker.DetailLogger, got %v", reflect.TypeOf(args[1]))) | ||
Comment on lines
+206
to
+209
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment about moving type check |
||
} | ||
|
||
dl.Warn(&checker.LogMessage{ | ||
Text: fmt.Sprintf("malformed csproj file: %v", err), | ||
}) | ||
return true, nil | ||
} | ||
if unsafe { | ||
found, err := finding.NewWith(fs, Probe, | ||
"C# code allows the use of unsafe blocks", &finding.Location{ | ||
Path: path, | ||
}, finding.OutcomeFalse) | ||
if err != nil { | ||
return false, fmt.Errorf("create finding: %w", err) | ||
} | ||
*findings = append(*findings, *found) | ||
} | ||
|
||
return true, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would need to be altered to focus on just
unsafe
detection if the probe does just one thing. Thoughunsafe
is too generic.So maybe something like
unsafeblock
? Open to suggestions/brainstorming.