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
2 changes: 1 addition & 1 deletion docs/probes.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ The probe returns 1 true outcome if the project has no workflows "write" permiss

**Motivation**: Memory safety in software should be considered a continuum, rather than being binary. While some languages and tools are memory safe by default, it may still be possible, and sometimes unavoidable, to write unsafe code in them. Unsafe code allow developers to bypass normal safety checks and directly manipulate memory.

**Implementation**: The probe is ecosystem-specific and will surface non memory safe practices in the project by identifying unsafe code blocks. Unsafe code blocks are supported in rust, go, c#, and swift, but only go and c# are supported by this probe at this time: - for go the probe will look for the use of the `unsafe` include directive. - for c# the probe will look at the csproj and identify the use of the `AllowUnsafeBlocks` property.
**Implementation**: The probe is ecosystem-specific and will surface non memory safe practices in the project by identifying unsafe code blocks. Unsafe code blocks are supported in rust, go, c#, Java, and swift, but only go, c# and Java are supported by this probe at this time: - for go the probe will look for the use of the `unsafe` include directive. - for c# the probe will look at the csproj and identify the use of the `AllowUnsafeBlocks` property. - for Java the probe will look at references to either the `sun.misc.Unsafe` class or the `jdk.internal.misc.Unsafe` class.

**Outcomes**: For supported ecosystem, the probe returns OutcomeTrue per unsafe block.
If the project has no unsafe blocks, the probe returns OutcomeFalse.
Expand Down
4 changes: 3 additions & 1 deletion probes/unsafeblock/def.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ motivation: >
Unsafe code allow developers to bypass normal safety checks and directly manipulate memory.
implementation: >
The probe is ecosystem-specific and will surface non memory safe practices in the project by identifying unsafe code blocks.
Unsafe code blocks are supported in rust, go, c#, and swift, but only go and c# are supported by this probe at this time:
Unsafe code blocks are supported in rust, go, c#, Java, and swift, but only go, c# and Java are supported by this probe at this time:
- for go the probe will look for the use of the `unsafe` include directive.
- for c# the probe will look at the csproj and identify the use of the `AllowUnsafeBlocks` property.
- for Java the probe will look at references to either the `sun.misc.Unsafe` class or the `jdk.internal.misc.Unsafe` class.
outcome:
- For supported ecosystem, the probe returns OutcomeTrue per unsafe block.
- If the project has no unsafe blocks, the probe returns OutcomeFalse.
Expand All @@ -38,6 +39,7 @@ ecosystem:
languages:
- go
- c#
- java
clients:
- github
- gitlab
Expand Down
123 changes: 123 additions & 0 deletions probes/unsafeblock/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
package unsafeblock

import (
"bytes"
"embed"
"fmt"
"go/parser"
"go/token"
"reflect"
"regexp"
"strings"

"github.com/ossf/scorecard/v5/checker"
Expand Down Expand Up @@ -52,6 +54,11 @@ var languageMemorySafeSpecs = map[clients.LanguageName]languageMemoryCheckConfig
funcPointer: checkDotnetAllowUnsafeBlocks,
Desc: "Check if C# code uses unsafe blocks",
},

clients.Java: {
funcPointer: checkJavaUnsafeClass,
Desc: "Check if Java code uses the Unsafe class",
},
}

func init() {
Expand Down Expand Up @@ -208,3 +215,119 @@ func csProjAllosUnsafeBlocks(path string, content []byte, args ...interface{}) (

return true, nil
}

// Java

var (
// javaMultiLineCommentRe matches /* ... */ comments (including across newlines).
javaMultiLineCommentRe = regexp.MustCompile(`(?s)/\*.*?\*/`)
// javaSingleLineCommentRe matches // ... to end of line.
javaSingleLineCommentRe = regexp.MustCompile(`//[^\n]*`)
// javaMultiLineStringRe matches multi-line string literals.
javaMultiLineStringRe = regexp.MustCompile(`(?s)""".*?"""`)
// javaSingleLineStringRe matches single-line string literals.
javaSingleLineStringRe = regexp.MustCompile(`"(?:[^"\\]|\\.)*"`)

// javaUnsafeImportRe matches import statements for sun.misc.Unsafe or
// jdk.internal.misc.Unsafe, allowing optional whitespace between tokens
// (e.g. "import sun . misc . Unsafe ;").
javaUnsafeImportRe = regexp.MustCompile(
`\bimport\s+(?:sun\s*\.\s*misc|jdk\s*\.\s*internal\s*\.\s*misc)\s*\.\s*Unsafe\s*;`)

// javaUnsafeFQNRe matches fully-qualified references to sun.misc.Unsafe or
// jdk.internal.misc.Unsafe in code (including inside import statements).
javaUnsafeFQNRe = regexp.MustCompile(
`\b(?:sun\s*\.\s*misc|jdk\s*\.\s*internal\s*\.\s*misc)\s*\.\s*Unsafe\b`)
)

// stripJavaComments removes single-line and multi-line comments from Java
// source, preserving newlines so that line numbers remain accurate.
func stripJavaComments(content []byte) []byte {
// Replace multi-line comments with an equal number of newlines.
src := javaMultiLineCommentRe.ReplaceAllFunc(content, func(match []byte) []byte {
return bytes.Repeat([]byte("\n"), bytes.Count(match, []byte("\n")))
})
// Remove single-line comments (the newline itself is not part of the match).
return javaSingleLineCommentRe.ReplaceAll(src, nil)
}

// stripJavaStringLiterals removes single-line and multi-line string literals from Java
// source, preserving newlines so that line numbers remain accurate.
func stripJavaStringLiterals(content []byte) []byte {
// Replace multi-line string literals with an equal number of newlines.
src := javaMultiLineStringRe.ReplaceAllFunc(content, func(match []byte) []byte {
return bytes.Repeat([]byte("\n"), bytes.Count(match, []byte("\n")))
})
// Remove single-line string literals (the newline itself is not part of the match).
return javaSingleLineStringRe.ReplaceAll(src, nil)
}

// javaLineNumber returns the 1-based line number of the byte at offset within src.
func javaLineNumber(src []byte, offset int) uint {
return uint(bytes.Count(src[:offset], []byte("\n")) + 1)
}

func checkJavaUnsafeClass(client *checker.CheckRequest) ([]finding.Finding, error) {
findings := []finding.Finding{}
if err := fileparser.OnMatchingFileContentDo(client.RepoClient, fileparser.PathMatcher{
Pattern: "*.java",
CaseSensitive: false,
}, javaCodeUsesUnsafeClass, &findings); err != nil {
return nil, err
}

return findings, nil
}

func javaCodeUsesUnsafeClass(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])))
}

src := stripJavaStringLiterals(stripJavaComments(content))

// Report each import of an Unsafe class.
importLocs := javaUnsafeImportRe.FindAllIndex(src, -1)
for _, loc := range importLocs {
line := javaLineNumber(src, loc[0])
found, err := finding.NewWith(fs, Probe,
"Java code imports the Unsafe class", &finding.Location{
Path: path, LineStart: &line,
}, finding.OutcomeTrue)
if err != nil {
return false, fmt.Errorf("create finding: %w", err)
}
*findings = append(*findings, *found)
}

// Report fully-qualified usages of an Unsafe class that are not part of
// an import statement (i.e. direct usage without a prior import).
for _, loc := range javaUnsafeFQNRe.FindAllIndex(src, -1) {
if withinAnyRange(loc[0], importLocs) {
continue
}
line := javaLineNumber(src, loc[0])
found, err := finding.NewWith(fs, Probe,
"Java code uses the Unsafe class", &finding.Location{
Path: path, LineStart: &line,
}, finding.OutcomeTrue)
if err != nil {
return false, fmt.Errorf("create finding: %w", err)
}
*findings = append(*findings, *found)
}

return true, nil
}

// withinAnyRange reports whether offset falls inside any of the given [start, end) ranges.
func withinAnyRange(offset int, ranges [][]int) bool {
for _, r := range ranges {
if offset >= r[0] && offset < r[1] {
return true
}
}
return false
}
Loading