Skip to content

Substr panics with slice bounds out of range when start > stop #5607

@kajaaz

Description

@kajaaz

Describe the bug

core/stringx.Substr validates start and stop individually against [0, len(str)] but does not check that start <= stop. When start > stop (both values individually within bounds), the slice expression rs[start:stop] panics at runtime with slice bounds out of range. The function's error-returning signature gives callers a false sense of safety: they reasonably expect all invalid inputs to produce an error, not an unrecovered panic.

To Reproduce

  1. The code is

    package main
    
    import (
        "errors"
        "fmt"
    )
    
    var (
        ErrInvalidStartPosition = errors.New("start position is invalid")
        ErrInvalidStopPosition  = errors.New("stop position is invalid")
    )
    
    // verbatim copy of core/stringx/strings.go
    func Substr(str string, start, stop int) (string, error) {
        rs := []rune(str)
        length := len(rs)
        if start < 0 || start > length {
            return "", ErrInvalidStartPosition
        }
        if stop < 0 || stop > length {
            return "", ErrInvalidStopPosition
        }
        return string(rs[start:stop]), nil
    }
    
    func main() {
        fmt.Println(Substr("hello", 0, 3)) // works fine
        fmt.Println(Substr("hello", 3, 2)) // panics: start > stop, both in range
    }
  2. The error is

    $ go run main.go
    hel <nil>
    panic: runtime error: slice bounds out of range [3:2]
    
    goroutine 1 [running]:
    main.Substr(...)
        /tmp/sandbox2887458380/prog.go:23
    main.main()
        /tmp/sandbox2887458380/prog.go:28 +0x1bb
    

Expected behavior

Substr("hello", 3, 2) should return ("", ErrInvalidStopPosition) (or a dedicated ErrInvalidRange error) instead of panicking. All invalid input combinations should produce an error value; the function should never panic.

Screenshots

N/A

Environments (please complete the following information):

  • OS: Linux
  • go-zero version: v1.10.1

More description

The function checks start and stop independently against the string length, but never checks that start <= stop. The Go slice expression rs[start:stop] requires start <= stop at runtime — violating this causes a panic rather than returning an error.

The fix is to add one extra guard between the two existing range checks:

// core/stringx/strings.go

func Substr(str string, start, stop int) (string, error) {
    rs := []rune(str)
    length := len(rs)

    if start < 0 || start > length {
        return "", ErrInvalidStartPosition
    }

    if stop < 0 || stop > length {
        return "", ErrInvalidStopPosition
    }

    // ADD THIS: reject start > stop before the slice operation
    if start > stop {
        return "", ErrInvalidStopPosition
    }

    return string(rs[start:stop]), nil
}

Without this guard, any caller passing start > stop (both values otherwise in range) gets a panic instead of an error. This can happen from reversed pagination parameters, off-by-one errors in slice computations, or user-controlled indices. In a web server or microservice built on go-zero, an unrecovered panic in a handler goroutine will kill the request and may crash the service process.

Note: the existing test suite in core/stringx/strings_test.go covers start < 0, start > length, stop < 0, and stop > length but has no test case for start > stop, so this path was not caught by tests.

Discovery method

This bug was found using Zorya, a concolic execution engine for Go binaries. Zorya ran symbolic exploration on a standalone binary wrapping the slice bounds logic and had Z3 produce a satisfying assignment within 183 seconds.

The exact command used:

zorya zorya_substr_real \
  --mode function 0x4b72c0 \
  --thread-scheduling main-only \
  --lang go \
  --compiler gc \
  --arg "0200000000000000 0a00000000000000" \
  --negate-path-exploration

--arg "0200000000000000 0a00000000000000" provides concrete seed values (start=2, stop=10, both as little-endian 8-byte hex) from which Zorya initialises symbolic variables. Z3 witness: start=2, stop=1; both pass the individual range checks, but rs[2:1] panics. Confirmed natively with go run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions