Skip to content

Out-of-Bounds Pagination with JOINs Leaks Connections **Severity:** ⚠️ CRITICAL #9

Description

@LukasGPC

Severity: ⚠️ CRITICAL

Description:
When OFFSET exceeds available data in queries with JOINs, the driver doesn't properly clean up connections, leading to connection pool exhaustion over time.

Symptoms:

  • Connections remain open after query completes
  • Connection pool gradually exhausts
  • Eventually causes "too many connections" errors
  • Problem worsens with smaller connection pool sizes

Minimal Reproduction:

func getOrganizationsPage999(db *sql.DB) error {
    // Assume table only has 10 rows
    query := `
        SELECT
            o.Org_ID, o.Org_Name,
            COALESCE(ot.Org_Type_Name, '')
        FROM GPC.Organization o
        LEFT JOIN GPC.Org_Type ot ON o.Org_Org_Type_ID = ot.Org_Type_ID
        ORDER BY o.Org_Name
        OFFSET 10000 ROWS FETCH NEXT 50 ROWS ONLY
    `

    rows, err := db.Query(query)
    if err != nil {
        return err
    }
    defer rows.Close() // Connection still leaks despite defer

    // No rows returned, but connection is not properly cleaned up
    for rows.Next() {
        // Never executed - no rows
    }

    return nil
}

func main() {
    db, _ := sql.Open("iris", "iris://_SYSTEM:password@localhost:1972/USER")
    db.SetMaxOpenConns(5)
    db.SetMaxIdleConns(2)
    defer db.Close()

    // Call repeatedly - connections leak each time
    for i := 0; i < 20; i++ {
        getOrganizationsPage999(db)
    }
    // Connection pool now exhausted
}

Workaround:
Validate pagination bounds before executing query:

func listOrganizations(db *sql.DB, page, pageSize int) ([]Organization, int, error) {
    // Get total count first
    var totalCount int
    db.QueryRow(`SELECT COUNT(*) FROM GPC.Organization`).Scan(&totalCount)

    // Validate pagination bounds BEFORE executing main query
    totalPages := (totalCount + pageSize - 1) / pageSize
    if totalCount == 0 {
        totalPages = 1
    }

    offset := (page - 1) * pageSize
    if page > totalPages || offset >= totalCount {
        // Return empty result without executing query - prevents leak
        return []Organization{}, totalCount, nil
    }

    // Safe to execute query - offset is valid
    query := fmt.Sprintf(`
        SELECT o.Org_ID, o.Org_Name, COALESCE(ot.Org_Type_Name, '')
        FROM GPC.Organization o
        LEFT JOIN GPC.Org_Type ot ON o.Org_Org_Type_ID = ot.Org_Type_ID
        ORDER BY o.Org_Name
        OFFSET %d ROWS FETCH NEXT %d ROWS ONLY
    `, offset, pageSize)

    rows, _ := db.Query(query)
    defer rows.Close() // Now safe - no leak

    // ... process results

    return organizations, totalCount, nil
}

Impact: High - requires pagination validation in all list endpoints, can cause production outages if overlooked.


Metadata

Metadata

Assignees

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