Skip to content

fix: harden security and fix resource leaks#63

Merged
poyrazK merged 34 commits intomainfrom
fix/lint-security-and-leaks
Feb 18, 2026
Merged

fix: harden security and fix resource leaks#63
poyrazK merged 34 commits intomainfrom
fix/lint-security-and-leaks

Conversation

@poyrazK
Copy link
Copy Markdown
Owner

@poyrazK poyrazK commented Feb 16, 2026

This PR addresses multiple security vulnerabilities and resource leaks identified by strict linting rules.

Changes:

  • Security (gosec):
    • Tightened file and directory permissions (0750/0600) (G301, G306).
    • Added path sanitization to prevent file inclusion/traversal (G304, G305).
    • Fixed potential integer overflows in cache stats and storage (G115).
    • Added ReadHeaderTimeout to HTTP server to prevent Slowloris attacks (G112).
    • Audited and documented usage of unsafe functions (G106, G204, G101).
  • Resource Leaks (bodyclose):
    • Ensured all HTTP response bodies are closed in tests.
  • Error Handling (errcheck):
    • Explicitly ignored unchecked errors where safe to do so.
  • CI:
    • Enabled and linters in .

Verification:

  • passes cleanly.
  • Tests updated to match new security constraints.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented resource leaks by ensuring HTTP responses are closed and added overflow/overflow-safety checks.
  • Security & Permissions

    • Tightened file and directory permissions and normalized path handling to reduce exposure and path-traversal risks.
  • Stability Improvements

    • Added an HTTP header read timeout, cryptographically stronger randomness for peer/node selection, and limits/validation for file extraction and flags.
  • Tests

    • Large expansion of unit and integration tests across services to improve coverage and detect regressions.
  • Code Quality

    • Expanded linters and applied targeted suppressions; improved test/resource cleanup.

Copilot AI review requested due to automatic review settings February 16, 2026 12:14
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Enables gosec/bodyclose linters; tightens filesystem permissions (dirs 0750, files 0600); applies filepath.Clean() to file I/O; fixes HTTP response-body leaks; replaces math/rand with crypto/rand; adds overflow/clamping checks; limits zip extraction size and file count; expands and adds many unit tests.

Changes

Cohort / File(s) Summary
Linter Configuration
\.golangci\.yml
Enable gosec and bodyclose; add gosec settings, test-pattern excludes, and align formatting.
HTTP Server
cmd/api/main.go, cmd/api/main_test.go
Set ReadHeaderTimeout: 10s on created http.Server (code + tests).
CLI / Cloud: perms & paths
cmd/cloud/...
cmd/cloud/auth.go, cmd/cloud/auth_test.go, cmd/cloud/function.go, cmd/cloud/function_cli_test.go, cmd/cloud/iac.go, cmd/cloud/iac_cli_test.go, cmd/cloud/ssh_key.go, cmd/cloud/storage.go, cmd/cloud/logs.go, cmd/cloud/secrets.go, cmd/doccheck/main_test.go
Tighten file/dir modes (0755→0750, 0644→0600); use filepath.Clean() for file reads/writes; add selective //nolint:gosec and assign ignored errors to _ where applicable.
WebSocket & HTTP tests / helpers
internal/handlers/ws/..., tests/..., tests/helpers/helpers_test.go, pkg/httputil/httputil_test.go, cmd/cloud/*_test.go
Capture *http.Response returns in tests/helpers, nil-check and close resp.Body when present; add gosec nolint for test API-key constants.
Zip extraction & file handling
internal/core/services/function.go, internal/core/services/function_internal_test.go
Limit extracted files to 1000, limit per-file decompression (CopyN 10MB) and total zip size cap retained; sanitize paths and enforce tmpDir prefix; set extracted file mode to 0600.
Memory & numeric safety
internal/core/services/cache.go
Convert Docker memory values to int64 with overflow protection and clamping to math.MaxInt64.
Storage randomness & permissions
internal/storage/...
internal/storage/coordinator/service.go, internal/storage/node/gossip.go, internal/storage/node/store.go
Replace math/rand selection with crypto/rand and math/big; tighten perms (0750/0600), sanitize paths with filepath.Clean(), clamp/encode timestamps and handle unsigned values defensively.
Filesystem repository changes
internal/repositories/filesystem/adapter.go, internal/repositories/...
Use filepath.Clean() for opens, replace os.Create with os.OpenFile(..., 0600) for explicit modes, tighten directory perms to 0750, add comments about gosec G304 where applicable.
Libvirt: testability & safety
internal/repositories/libvirt/...
adapter.go, real_client.go, lb_proxy.go, adapter_lifecycle_test.go, lb_proxy_test.go, mock_client_test.go
Add injectable hooks (execCommand, lookPath, osOpen) for testing; enforce 5-minute IP wait timeout; add int32 overflow checks for libvirt flags; tighten file perms for cloud-init and nginx files; update tests accordingly; removed integration build tag.
SSH / K8s SSH behavior
internal/repositories/k8s/node_executor.go, pkg/sshutil/client.go
Set HostKeyCallback: ssh.InsecureIgnoreHostKey() and move/add gosec nolint comments adjacent to use; no other behavioral changes.
Websocket tests: response handling
internal/handlers/ws/client_test.go, internal/handlers/ws/ws_test.go
Capture HTTP response from websocket dial, nil-check and defer resp.Body.Close() to avoid leaks; add nil guards in auth failure paths.
SSH key & auth file I/O
cmd/cloud/ssh_key.go, cmd/cloud/auth.go
Use filepath.Clean() when reading key/config files; tighten dir and file modes (0750/0600); ensure cleaned paths on load.
Assemble / store changes
internal/storage/node/store.go, internal/repositories/...
Use explicit OpenFile with 0600, create directories with 0750, sanitize paths, update metadata encoding to unsigned/clamped timestamps, and improve read fallback to file modtime.
Gosec nolint annotations
cmd/cloud/secrets.go, various test constants
Add specific //nolint:gosec annotations to suppress credential/format-string warnings in tests/constants.
Test additions & refactors
many internal/core/services/*_unit_test.go, cmd/cloud/*_test.go, internal/repositories/..._test.go, tests/...
Large number of new and updated unit tests across services (accounting, audit, auth, autoscaling, cache, cluster, container, cron, database, dns, elastic IP, encryption, function, health, iam, identity, image, instance, lifecycle, loadbalancer, notify, queue, rbac, secret, snapshot, sshkey, stack, storage, subnet, volume, vpc, libvirt, postgres repos, etc.). Many tests add mocks, rename packages/types, or adjust signatures where helpers now return *http.Response.
CI Workflow
.github/workflows/ci.yml
Replace repetitive Postgres waits with timeout-wrapped readiness loop and constrain go test parallelism (-p 1) for identified test groups.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"I hopped through paths both clean and neat,
Locked file doors so secrets keep,
Random picks now truly blind,
Responses closed, no leaks to find,
Zip bounds set — peace of mind!" 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: security hardening (file permissions, path sanitization, integer overflow checks, HTTP timeouts) and resource leak fixes (HTTP response body closure).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/lint-security-and-leaks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/storage/node/store.go (1)

183-188: ⚠️ Potential issue | 🟡 Minor

Dead code in traversal check: len(rel) < 2 && rel == ".." is always false.

".." has length 2, so len(rel) < 2 && rel == ".." can never be true. The check relies entirely on the second clause. Consider simplifying to strings.HasPrefix(rel, "..") or just rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)).

Proposed fix
-	if err != nil || len(rel) < 2 && rel == ".." || (len(rel) >= 2 && rel[:2] == "..") {
+	if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {

Note: This would require adding "strings" to imports.

🤖 Fix all issues with AI agents
In `@cmd/cloud/logs.go`:
- Around line 108-115: Replace the blank-identifier error discards for
table.Append and table.Render with proper error handling: capture the returned
errors from table.Append(...) and table.Render(), and if non-nil write a clear
error message to stderr (or the module's logger) including context (e.g., the
log entry timestamp/level/resource) so failures aren’t silent; specifically
update the calls to table.Append and table.Render to check their error returns
and log/report any error instead of using `_ =`.

In `@internal/core/services/function.go`:
- Around line 335-342: io.CopyN(dst, src, maxFileSize) will silently truncate
files larger than maxFileSize; change the logic so after calling io.CopyN you
detect the "too large" case: keep the existing io.CopyN call with maxFileSize
and then, if err == nil (meaning exactly maxFileSize were read but more data may
remain), attempt to read one byte from src (e.g. buf := make([]byte,1); n, err2
:= src.Read(buf)); if n>0 || (err2 != nil && err2 != io.EOF) return a specific
"file too large" error; preserve the existing error handling for err != nil &&
err != io.EOF and treat err == io.EOF as success. Ensure you reference
maxFileSize, io.CopyN, dst and src in the change.

In `@internal/repositories/filesystem/adapter.go`:
- Around line 108-109: The Assemble function currently uses
os.Create(filepath.Clean(destPath)) which yields files with default 0666
permissions; change this to use os.OpenFile with flags
os.O_CREATE|os.O_WRONLY|os.O_TRUNC and explicit mode 0600 (keeping
filepath.Clean(destPath)) so the created file honors the same 0600 hardening
used by Write; ensure the returned file handle is used/closed as before.

In `@internal/storage/node/store.go`:
- Around line 164-166: The write to the metadata file currently ignores errors
(_ = os.WriteFile(metaPath, buf, 0600)), causing silent failures; update the
code in store.go to capture the error from os.WriteFile (e.g., err :=
os.WriteFile(metaPath, buf, 0600)) and if non-nil return or propagate that error
from the enclosing function (the function that assembles the object which sets
uNow, buf, and metaPath), ensuring callers get a failure instead of proceeding
without a .meta file.

In `@tests/elastic_ip_e2e_test.go`:
- Around line 153-155: Replace the silent response closes with the existing
helper: instead of calling `_ = termResp.Body.Close()` directly, call the test
helper closeResponse(t, termResp) so close errors are logged; ensure you still
nil-check termResp before calling closeResponse (i.e., if termResp != nil {
closeResponse(t, termResp) }) and apply this change to both cleanup paths that
currently use `_ = termResp.Body.Close()`.
🧹 Nitpick comments (6)
cmd/api/main.go (1)

77-81: Extract ReadHeaderTimeout value into a named constant.

The 10 * time.Second literal is a magic number. As per coding guidelines, "Do not use magic numbers - use named constants instead".

Suggested fix

Add near the existing defaultDBInitTimeout constant:

 const (
 	defaultDBInitTimeout = 120 * time.Second
+	defaultReadHeaderTimeout = 10 * time.Second
 )

Then reference it:

 			return &http.Server{
 				Addr:              addr,
 				Handler:           handler,
-				ReadHeaderTimeout: 10 * time.Second,
+				ReadHeaderTimeout: defaultReadHeaderTimeout,
 			}
internal/repositories/docker/adapter_unit_test.go (1)

338-339: Permission tightening to 0750 looks good.

Minor note: the MkdirAll error is silently discarded. If the directory creation fails, the test will likely fail with a confusing error downstream. Consider using require.NoError or t.Fatal instead of _ =. As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".

Suggested fix
-	_ = os.MkdirAll(configPath, 0750)
+	require.NoError(t, os.MkdirAll(configPath, 0750))
cmd/cloud/storage.go (1)

144-144: os.Create uses permissive default mode (0666 before umask).

Other parts of this PR tighten file permissions to 0600. For consistency and to match the PR's security hardening intent, consider using os.OpenFile with explicit 0600 permissions here, since the downloaded content could be sensitive.

💡 Suggested change
-		out, err := os.Create(filepath.Clean(dest))
+		out, err := os.OpenFile(filepath.Clean(dest), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
.golangci.yml (2)

31-34: Global G106 exclusion makes per-file //nolint:gosec // G106 directives redundant.

G106 (ssh.InsecureIgnoreHostKey) is excluded globally here, yet internal/repositories/k8s/node_executor.go line 78 also has a //nolint:gosec // G106 directive. The per-line suppression is harmless but unnecessary. Consider keeping only one approach for consistency — either the global exclusion or the per-line nolint with its explanatory comment (the latter is more self-documenting).


47-49: Blanket gosec exclusion for all test files.

This suppresses all gosec rules in test files. While convenient, it may hide genuine security issues in test helpers or fixtures that get reused. Consider excluding only specific gosec rules (e.g., G101, G304) in tests instead of the entire linter.

internal/core/services/function.go (1)

291-294: Use a named constant for the max file count.

The limit 1000 is a magic number. Per coding guidelines, use a named constant.

Proposed fix
 func (s *FunctionService) extractZip(rc io.Reader, tmpDir string) error {
 	// Limit extraction to 50MB to prevent Zip bombs
 	const maxZipSize = 50 * 1024 * 1024
+	const maxZipFiles = 1000
 	lr := io.LimitReader(rc, maxZipSize)
 	...
-	if len(zr.File) > 1000 {
+	if len(zr.File) > maxZipFiles {
 		return fmt.Errorf("too many files in zip: %d", len(zr.File))
 	}

Comment thread cmd/cloud/logs.go
Comment on lines +108 to +115
_ = table.Append([]string{
e.Timestamp.Format(time.RFC3339),
e.Level,
resource,
e.Message,
})
}
table.Render()
_ = table.Render()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Blank identifier assignments violate the coding guideline against silent failures.

Both _ = table.Append(...) and _ = table.Render() silently discard errors. Consider logging failures to stderr, which satisfies both the errcheck linter and the project guideline.

Proposed fix
-		_ = table.Append([]string{
+		if err := table.Append([]string{
 			e.Timestamp.Format(time.RFC3339),
 			e.Level,
 			resource,
 			e.Message,
-		})
+		}); err != nil {
+			fmt.Fprintf(os.Stderr, "Error appending log entry: %v\n", err)
+		}
 	}
-	_ = table.Render()
+	if err := table.Render(); err != nil {
+		fmt.Fprintf(os.Stderr, "Error rendering table: %v\n", err)
+	}

As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_ = table.Append([]string{
e.Timestamp.Format(time.RFC3339),
e.Level,
resource,
e.Message,
})
}
table.Render()
_ = table.Render()
if err := table.Append([]string{
e.Timestamp.Format(time.RFC3339),
e.Level,
resource,
e.Message,
}); err != nil {
fmt.Fprintf(os.Stderr, "Error appending log entry: %v\n", err)
}
}
if err := table.Render(); err != nil {
fmt.Fprintf(os.Stderr, "Error rendering table: %v\n", err)
}
🤖 Prompt for AI Agents
In `@cmd/cloud/logs.go` around lines 108 - 115, Replace the blank-identifier error
discards for table.Append and table.Render with proper error handling: capture
the returned errors from table.Append(...) and table.Render(), and if non-nil
write a clear error message to stderr (or the module's logger) including context
(e.g., the log entry timestamp/level/resource) so failures aren’t silent;
specifically update the calls to table.Append and table.Render to check their
error returns and log/report any error instead of using `_ =`.

Comment on lines +335 to +342
// Prevent decompression bomb: limit copy size to 10MB per file
// and check for excessive total size if needed.
const maxFileSize = 10 * 1024 * 1024
_, err = io.CopyN(dst, src, maxFileSize)
if err != nil && err != io.EOF {
return err
}
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

io.CopyN silently truncates files larger than 10 MB.

io.CopyN(dst, src, maxFileSize) copies exactly maxFileSize bytes and returns nil error when there is more data remaining. Files larger than 10 MB will be silently truncated with no error. You need to detect the "file too large" case and return an error.

Proposed fix
 	const maxFileSize = 10 * 1024 * 1024
-	_, err = io.CopyN(dst, src, maxFileSize)
-	if err != nil && err != io.EOF {
-		return err
+	written, err := io.CopyN(dst, src, maxFileSize+1)
+	if err != nil && err != io.EOF {
+		return err
+	}
+	if written > maxFileSize {
+		return fmt.Errorf("file exceeds maximum size of %d bytes: %s", maxFileSize, file.Name)
 	}
 	return nil
🤖 Prompt for AI Agents
In `@internal/core/services/function.go` around lines 335 - 342, io.CopyN(dst,
src, maxFileSize) will silently truncate files larger than maxFileSize; change
the logic so after calling io.CopyN you detect the "too large" case: keep the
existing io.CopyN call with maxFileSize and then, if err == nil (meaning exactly
maxFileSize were read but more data may remain), attempt to read one byte from
src (e.g. buf := make([]byte,1); n, err2 := src.Read(buf)); if n>0 || (err2 !=
nil && err2 != io.EOF) return a specific "file too large" error; preserve the
existing error handling for err != nil && err != io.EOF and treat err == io.EOF
as success. Ensure you reference maxFileSize, io.CopyN, dst and src in the
change.

Comment on lines +108 to +109
// G304: Explicitly clean path before creation
f, err := os.Create(filepath.Clean(destPath))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

os.Create defaults to 0666 permissions — inconsistent with the 0600 hardening elsewhere.

Write (line 43) correctly uses os.OpenFile(..., 0600), but Assemble uses os.Create which creates files with mode 0666 (before umask). This undermines the permission tightening goal of this PR.

Proposed fix
-	// G304: Explicitly clean path before creation
-	f, err := os.Create(filepath.Clean(destPath))
+	// G304/G306: Explicitly clean path and set restrictive permissions
+	f, err := os.OpenFile(filepath.Clean(destPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// G304: Explicitly clean path before creation
f, err := os.Create(filepath.Clean(destPath))
// G304/G306: Explicitly clean path and set restrictive permissions
f, err := os.OpenFile(filepath.Clean(destPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
🤖 Prompt for AI Agents
In `@internal/repositories/filesystem/adapter.go` around lines 108 - 109, The
Assemble function currently uses os.Create(filepath.Clean(destPath)) which
yields files with default 0666 permissions; change this to use os.OpenFile with
flags os.O_CREATE|os.O_WRONLY|os.O_TRUNC and explicit mode 0600 (keeping
filepath.Clean(destPath)) so the created file honors the same 0600 hardening
used by Write; ensure the returned file handle is used/closed as before.

Comment on lines +164 to +166

binary.LittleEndian.PutUint64(buf, uNow)
_ = os.WriteFile(metaPath, buf, 0600)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent failure writing metadata for assembled object.

If os.WriteFile fails here, the assembled object will have no .meta file, causing reads to fall back to ModTime—which may be inaccurate in distributed scenarios. At minimum, return this error since the function already returns an error value. As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".

Proposed fix
-	_ = os.WriteFile(metaPath, buf, 0600)
-
-	return totalSize, nil
+	if err := os.WriteFile(metaPath, buf, 0600); err != nil {
+		return 0, err
+	}
+	return totalSize, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
binary.LittleEndian.PutUint64(buf, uNow)
_ = os.WriteFile(metaPath, buf, 0600)
binary.LittleEndian.PutUint64(buf, uNow)
if err := os.WriteFile(metaPath, buf, 0600); err != nil {
return 0, err
}
return totalSize, nil
🤖 Prompt for AI Agents
In `@internal/storage/node/store.go` around lines 164 - 166, The write to the
metadata file currently ignores errors (_ = os.WriteFile(metaPath, buf, 0600)),
causing silent failures; update the code in store.go to capture the error from
os.WriteFile (e.g., err := os.WriteFile(metaPath, buf, 0600)) and if non-nil
return or propagate that error from the enclosing function (the function that
assembles the object which sets uNow, buf, and metaPath), ensuring callers get a
failure instead of proceeding without a .meta file.

Comment on lines +153 to +155
if termResp != nil {
_ = termResp.Body.Close()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use the existing closeResponse helper for consistency.

Both cleanup paths use _ = termResp.Body.Close(), but the file already defines closeResponse(t, resp) (Line 27) which properly logs close errors. Every other response close in this file uses that helper. As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".

Proposed fix

At lines 153–155:

 		if termResp != nil {
-			_ = termResp.Body.Close()
+			closeResponse(t, termResp)
 		}

At lines 186–188:

 		if termResp != nil {
-			_ = termResp.Body.Close()
+			closeResponse(t, termResp)
 		}

Also applies to: 186-188

🤖 Prompt for AI Agents
In `@tests/elastic_ip_e2e_test.go` around lines 153 - 155, Replace the silent
response closes with the existing helper: instead of calling `_ =
termResp.Body.Close()` directly, call the test helper closeResponse(t, termResp)
so close errors are logged; ensure you still nil-check termResp before calling
closeResponse (i.e., if termResp != nil { closeResponse(t, termResp) }) and
apply this change to both cleanup paths that currently use `_ =
termResp.Body.Close()`.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements security hardening and resource leak fixes based on strict linting rules (gosec, bodyclose, errcheck). The changes span multiple areas: tightened file permissions, path sanitization, integer overflow protection, HTTP server security improvements, cryptographically secure random number generation, and proper resource cleanup in tests.

Changes:

  • Enabled gosec and bodyclose linters with inline nolint directives for audited exceptions
  • Hardened file/directory permissions from 0755/0644 to 0750/0600 across the codebase
  • Added path sanitization with filepath.Clean and traversal checks
  • Fixed integer overflow risks in type conversions (uint64→int64, uint32→int32)
  • Added ReadHeaderTimeout to HTTP servers to prevent Slowloris attacks
  • Replaced math/rand with crypto/rand for security-sensitive random selection
  • Added zip bomb protections (file count limit, per-file size limit) to function extraction
  • Ensured HTTP response bodies are properly closed in tests

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
.golangci.yml Enabled gosec and bodyclose linters; configured exclusions
tests/multitenancy_e2e_test.go Added response body closure in deleteInstance
tests/helpers/helpers_test.go Added response body closure in test helpers
tests/elastic_ip_e2e_test.go Inlined response body closure (inconsistent with file's pattern)
pkg/testutil/constants.go Added nolint directive for test constant
pkg/sshutil/client.go Added nolint directive with documentation for InsecureIgnoreHostKey
pkg/httputil/httputil_test.go Added nolint directive for test constant
internal/storage/node/store.go Tightened permissions, added path sanitization and overflow checks
internal/storage/node/gossip.go Switched to crypto/rand for peer selection
internal/storage/coordinator/service.go Switched to crypto/rand for node selection
internal/repositories/libvirt/real_client.go Added overflow checks for uint32→int32 flag conversions
internal/repositories/libvirt/lb_proxy_test.go Tightened permissions and added path sanitization
internal/repositories/libvirt/lb_proxy.go Tightened file permissions to 0750/0600
internal/repositories/libvirt/adapter.go Tightened cloud-init file permissions
internal/repositories/k8s/node_executor.go Added nolint directive with justification for InsecureIgnoreHostKey
internal/repositories/filesystem/adapter.go Tightened permissions, added path sanitization (but checks are insufficient)
internal/repositories/docker/adapter_unit_test.go Tightened directory permissions in test
internal/handlers/ws/ws_test.go Added response body closure for websocket dial responses
internal/handlers/ws/client_test.go Added response body closure for websocket dial responses
internal/core/services/function.go Added zip bomb protections and path sanitization (but checks are insufficient)
internal/core/services/cache.go Added overflow checks for memory stats uint64→int64 conversions
cmd/doccheck/main_test.go Tightened file permissions in test
cmd/cloud/storage.go Added filepath.Clean to file operations
cmd/cloud/ssh_key.go Added filepath.Clean to ReadFile
cmd/cloud/secrets.go Added nolint directive for format string constant
cmd/cloud/logs.go Ignored Append/Render return values per .golangci.yml config
cmd/cloud/kubernetes_test.go Added nolint directive for test constant
cmd/cloud/instance.go Added nolint directive for syscall.Exec with user args
cmd/cloud/iac_cli_test.go Tightened file permissions in test
cmd/cloud/iac.go Added filepath.Clean to ReadFile calls
cmd/cloud/function_cli_test.go Tightened file permissions in test
cmd/cloud/function.go Added filepath.Clean to ReadFile calls
cmd/cloud/auth_test.go Tightened permissions in test
cmd/cloud/auth.go Tightened config permissions and added filepath.Clean
cmd/api/main_test.go Added ReadHeaderTimeout to test HTTP server
cmd/api/main.go Added ReadHeaderTimeout to production HTTP server

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +153 to +155
if termResp != nil {
_ = termResp.Body.Close()
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling pattern: This file defines a closeResponse helper function at line 27 that already handles nil checks and is used throughout the file. These two call sites should use the existing closeResponse helper for consistency instead of inlining the nil check. Replace with closeResponse(t, termResp) to match the pattern used elsewhere in this file (lines 48, 73, 91, 113, 133, 165, 181, 194, 200).

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +188
if termResp != nil {
_ = termResp.Body.Close()
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling pattern: This file defines a closeResponse helper function at line 27 that already handles nil checks and is used throughout the file. This call site should use the existing closeResponse helper for consistency instead of inlining the nil check. Replace with closeResponse(t, termResp) to match the pattern used elsewhere in this file (lines 48, 73, 91, 113, 133, 165, 181, 194, 200).

Copilot uses AI. Check for mistakes.
Comment thread .golangci.yml Outdated
@@ -1,4 +1,5 @@
run:
version: 1
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "version" field under "run" section is not a valid golangci-lint configuration option. This field does not exist in the golangci-lint configuration schema and should be removed. The golangci-lint version is specified in the GitHub Actions workflow (line 36 of .github/workflows/ci.yml) or as a CLI argument, not in the configuration file.

Suggested change
version: 1

Copilot uses AI. Check for mistakes.
Comment on lines 100 to 120
func (s *LocalFileStore) Assemble(ctx context.Context, bucket, key string, parts []string) (int64, error) {
bucketPath := filepath.Join(s.basePath, filepath.Clean(bucket))
destPath := filepath.Join(bucketPath, filepath.Clean(key))

if !strings.HasPrefix(destPath, filepath.Clean(s.basePath)) {
return 0, errors.New(errors.InvalidInput, errTraversal)
}

f, err := os.Create(destPath)
// G304: Explicitly clean path before creation
f, err := os.Create(filepath.Clean(destPath))
if err != nil {
return 0, errors.Wrap(errors.Internal, "failed to create dest file", err)
}
defer func() { _ = f.Close() }()

var totalSize int64
for _, partKey := range parts {
partPath := filepath.Join(bucketPath, filepath.Clean(partKey))
if !strings.HasPrefix(partPath, filepath.Clean(s.basePath)) {
return 0, errors.New(errors.InvalidInput, errTraversal)
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal check is insufficient in multiple locations: Lines 104-105 and 118-119 use HasPrefix checks which can be bypassed. All these checks should be replaced with filepath.Rel validation. See internal/storage/node/store.go:184-187 for the correct pattern.

Copilot uses AI. Check for mistakes.
Comment thread pkg/sshutil/client.go
Comment on lines +36 to +37
//nolint:gosec // G106: Default to insecure for legacy compatibility, but now configurable
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading comment: The comment states "now configurable" but the HostKeyCallback field is set to ssh.InsecureIgnoreHostKey() without providing any way to configure it through the NewClientWithKey constructor. While the Client struct has a public HostKeyCallback field that could be modified after creation, the comment suggests this PR made it configurable, which is not the case. Either add a parameter to make it actually configurable, or update the comment to accurately reflect that it remains hardcoded but can be modified on the returned Client struct.

Copilot uses AI. Check for mistakes.
Comment on lines +307 to +309
//nolint:gosec // G305: Path sanitization and prefix check are performed below
path := filepath.Join(cleanTmpDir, file.Name)
if !strings.HasPrefix(filepath.Clean(path), cleanTmpDir+string(os.PathSeparator)) {
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal check is insufficient: The check !strings.HasPrefix(filepath.Clean(path), cleanTmpDir+string(os.PathSeparator)) will fail to detect traversal for paths like tmpDir/.. because filepath.Clean(path) would result in the parent of tmpDir, which won't have tmpDir as a prefix. However, the more critical issue is that this check happens AFTER the filepath.Join on line 308. If file.Name contains path traversal sequences like ../../../etc/passwd, the join may escape the tmpDir. The correct approach is to check if filepath.Clean(file.Name) contains .. components BEFORE joining, and also verify the final path stays within tmpDir using filepath.Rel or filepath.EvalSymlinks.

Suggested change
//nolint:gosec // G305: Path sanitization and prefix check are performed below
path := filepath.Join(cleanTmpDir, file.Name)
if !strings.HasPrefix(filepath.Clean(path), cleanTmpDir+string(os.PathSeparator)) {
// Sanitize the file name before joining to prevent path traversal
cleanName := filepath.Clean(file.Name)
// Skip entries that resolve to the current directory
if cleanName == "." {
return nil
}
// Reject absolute paths
if filepath.IsAbs(cleanName) {
return fmt.Errorf("invalid absolute file path in zip: %s", file.Name)
}
// Reject any path containing ".." components
for _, part := range strings.Split(cleanName, string(os.PathSeparator)) {
if part == ".." {
return fmt.Errorf("invalid file path in zip (path traversal): %s", file.Name)
}
}
// Construct the target path within the cleaned tmpDir
path := filepath.Join(cleanTmpDir, cleanName)
// Ensure the resulting path is still within tmpDir
rel, err := filepath.Rel(cleanTmpDir, path)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {

Copilot uses AI. Check for mistakes.
Comment on lines 31 to 37
func (s *LocalFileStore) Write(ctx context.Context, bucket, key string, r io.Reader) (int64, error) {
bucketPath := filepath.Join(s.basePath, filepath.Clean(bucket))
filePath := filepath.Join(bucketPath, filepath.Clean(key))

if !strings.HasPrefix(filePath, filepath.Clean(s.basePath)) {
return 0, errors.New(errors.InvalidInput, errTraversal)
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal check is insufficient: The check on line 35 uses !strings.HasPrefix(filePath, filepath.Clean(s.basePath)) which can be bypassed. After filepath.Join(bucketPath, filepath.Clean(key)) on line 33, if key contains traversal sequences, the result may escape basePath. A more robust approach is to use filepath.Rel (like internal/storage/node/store.go:184-187 does) to verify the path stays within basePath: rel, err := filepath.Rel(filepath.Clean(s.basePath), filePath); if err != nil || strings.HasPrefix(rel, "..") { return error }. The HasPrefix check alone is insufficient because filepath.Clean may resolve symlinks or handle edge cases differently.

Copilot uses AI. Check for mistakes.
Comment on lines 57 to 61
func (s *LocalFileStore) Read(ctx context.Context, bucket, key string) (io.ReadCloser, error) {
filePath := filepath.Join(s.basePath, filepath.Clean(bucket), filepath.Clean(key))
if !strings.HasPrefix(filePath, filepath.Clean(s.basePath)) {
return nil, errors.New(errors.InvalidInput, errTraversal)
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal check is insufficient: Same issue as in Write method - the HasPrefix check on line 59 can be bypassed. Use filepath.Rel to verify the path stays within basePath instead of string prefix checking. See internal/storage/node/store.go:184-187 for the correct pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +157 to +165
now := time.Now().UnixNano()
var uNow uint64
if now < 0 {
uNow = 0
} else {
uNow = uint64(now)
}

binary.LittleEndian.PutUint64(buf, uNow)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary defensive check: time.Now().UnixNano() cannot be negative in practice unless the system clock is set to before January 1, 1970 (Unix epoch), which would indicate a serious system misconfiguration. While technically possible, this check adds complexity for an extremely unlikely edge case. If you want to keep the check for robustness, consider adding a comment explaining why it's needed, or simply use uint64(now) directly since Go's conversion truncates the value predictably.

Suggested change
now := time.Now().UnixNano()
var uNow uint64
if now < 0 {
uNow = 0
} else {
uNow = uint64(now)
}
binary.LittleEndian.PutUint64(buf, uNow)
now := uint64(time.Now().UnixNano())
binary.LittleEndian.PutUint64(buf, now)

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +54
var uTimestamp uint64
if timestamp < 0 {
uTimestamp = 0
} else {
uTimestamp = uint64(timestamp)
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent handling: This negative timestamp check might make sense if timestamps come from external sources (like they do in the Write method parameter), but it's inconsistent with the approach in Assemble where time.Now().UnixNano() is checked. Consider documenting why negative timestamps need to be handled, or remove the check in Assemble (lines 157-163) if external negative timestamps are the only concern.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 16, 2026 20:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .golangci.yml
Comment on lines +30 to +33
gosec:
excludes:
- G101
- G106
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gosec is enabled, but G101 and G106 are globally excluded here, which means newly introduced hardcoded credentials or ssh.InsecureIgnoreHostKey() usages won’t be flagged anywhere. Since the PR already adds targeted //nolint:gosec annotations, consider removing these global excludes (or scoping them to specific paths) so future regressions still get caught by CI.

Suggested change
gosec:
excludes:
- G101
- G106

Copilot uses AI. Check for mistakes.
Comment on lines 35 to +39
if !strings.HasPrefix(filePath, filepath.Clean(s.basePath)) {
return 0, errors.New(errors.InvalidInput, errTraversal)
}

if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(filePath), 0750); err != nil {
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The traversal guard strings.HasPrefix(filePath, filepath.Clean(s.basePath)) is vulnerable to prefix-bypass (e.g., base /tmp/base, joined path /tmp/base2/... still matches). This can allow escaping the base directory with inputs like bucket ../base2. Use filepath.Rel from the base to the computed path and reject rel == ".." or any rel starting with ".."+PathSeparator, or ensure the prefix check includes a path separator boundary.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +133
resp, err := client.Do(req)
if err == nil {
defer func() { _ = resp.Body.Close() }()
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http.Client.Do can return a non-nil resp even when err is non-nil; in that case the response body still needs to be closed. The current if err == nil { ... Close() } can leak. Close the body whenever resp != nil (and consider asserting on err/status if this request is expected to succeed).

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 16, 2026 20:38
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@internal/repositories/libvirt/adapter.go`:
- Around line 284-293: The 5-minute magic duration in waitInitialIP should be
extracted to a named constant (e.g., waitIPHardTimeout) and the time.After call
replaced with a time.NewTimer so the timer can be stopped/GC'd if the function
returns early; add const waitIPHardTimeout = 5 * time.Minute at top of the file,
create timer := time.NewTimer(waitIPHardTimeout) before the loop, defer
timer.Stop() (or stop it when returning), and change the select to read from
timer.C instead of time.After; update any error text to reference the constant
usage (the timeout remains for instance id in waitInitialIP).

In `@tests/database_replication_e2e_test.go`:
- Around line 110-117: Replace the blank-identifier close calls for the HTTP
responses with explicit error handling: instead of `_ = resp1.Body.Close()` and
`_ = resp2.Body.Close()`, call `err := resp1.Body.Close()` / `err :=
resp2.Body.Close()` and if `err != nil` use the test logger (e.g.,
`t.Logf("closing response body: %v", err)`) to record the error; apply the same
change for the other response closes in this test file (variables resp1, resp2
and any other resp* returned by deleteRequest) to satisfy the
no-blank-identifier guideline and errcheck.
🧹 Nitpick comments (2)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed.


In `@internal/repositories/libvirt/adapter.go`:
- Around line 284-293: The 5-minute magic duration in waitInitialIP should be
extracted to a named constant (e.g., waitIPHardTimeout) and the time.After call
replaced with a time.NewTimer so the timer can be stopped/GC'd if the function
returns early; add const waitIPHardTimeout = 5 * time.Minute at top of the file,
create timer := time.NewTimer(waitIPHardTimeout) before the loop, defer
timer.Stop() (or stop it when returning), and change the select to read from
timer.C instead of time.After; update any error text to reference the constant
usage (the timeout remains for instance id in waitInitialIP).

In `@tests/database_replication_e2e_test.go`:
- Around line 110-117: Replace the blank-identifier close calls for the HTTP
responses with explicit error handling: instead of `_ = resp1.Body.Close()` and
`_ = resp2.Body.Close()`, call `err := resp1.Body.Close()` / `err :=
resp2.Body.Close()` and if `err != nil` use the test logger (e.g.,
`t.Logf("closing response body: %v", err)`) to record the error; apply the same
change for the other response closes in this test file (variables resp1, resp2
and any other resp* returned by deleteRequest) to satisfy the
no-blank-identifier guideline and errcheck.
tests/database_replication_e2e_test.go (1)

110-117: Good fix: nil-guarded response body closure prevents resource leaks.

The nil check before deferring Body.Close() is the right pattern here, since deleteRequest may return a nil response on failure.

One note: the _ = resp.Body.Close() pattern on lines 112 and 116 conflicts with the repository's coding guideline against blank identifier assignments. If you want to stay compliant while satisfying errcheck, you could log the error in tests instead:

♻️ Optional: log close errors instead of discarding
 		resp1 := deleteRequest(t, client, fmt.Sprintf("%s/databases/%s", testutil.TestBaseURL, primaryID), token)
 		if resp1 != nil {
-			defer func() { _ = resp1.Body.Close() }()
+			defer func() {
+				if err := resp1.Body.Close(); err != nil {
+					t.Logf("failed to close resp1 body: %v", err)
+				}
+			}()
 		}
 		resp2 := deleteRequest(t, client, fmt.Sprintf("%s/databases/%s", testutil.TestBaseURL, replicaID), token)
 		if resp2 != nil {
-			defer func() { _ = resp2.Body.Close() }()
+			defer func() {
+				if err := resp2.Body.Close(); err != nil {
+					t.Logf("failed to close resp2 body: %v", err)
+				}
+			}()
 		}

This same pattern is used in unchanged lines (42, 61, 78, 92, 98) throughout the file — worth addressing holistically if you agree.

As per coding guidelines, **/*.go: "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/database_replication_e2e_test.go` around lines 110 - 117, Replace the
blank-identifier close calls for the HTTP responses with explicit error
handling: instead of `_ = resp1.Body.Close()` and `_ = resp2.Body.Close()`, call
`err := resp1.Body.Close()` / `err := resp2.Body.Close()` and if `err != nil`
use the test logger (e.g., `t.Logf("closing response body: %v", err)`) to record
the error; apply the same change for the other response closes in this test file
(variables resp1, resp2 and any other resp* returned by deleteRequest) to
satisfy the no-blank-identifier guideline and errcheck.
internal/repositories/libvirt/adapter.go (1)

284-293: Extract the 5-minute timeout as a named constant.

The 5 * time.Minute duration is a magic number. Per coding guidelines, use a named constant (e.g., waitIPTimeout). Also, note that time.After allocates a timer that cannot be stopped/GC'd until it fires — if waitInitialIP returns early, the timer lingers for the remaining duration. Consider time.NewTimer with an explicit Stop() in a defer for cleaner resource management.

Proposed fix

Add a constant near the top of the file:

const waitIPHardTimeout = 5 * time.Minute

Then in waitInitialIP:

-	// Safety limit: max 5 minutes regardless of context
-	timeout := time.After(5 * time.Minute)
+	// Safety limit: hard cap regardless of context
+	timer := time.NewTimer(waitIPHardTimeout)
+	defer timer.Stop()

And update the select case:

-	case <-timeout:
+	case <-timer.C:

As per coding guidelines, "Do not use magic numbers - use named constants".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repositories/libvirt/adapter.go` around lines 284 - 293, The
5-minute magic duration in waitInitialIP should be extracted to a named constant
(e.g., waitIPHardTimeout) and the time.After call replaced with a time.NewTimer
so the timer can be stopped/GC'd if the function returns early; add const
waitIPHardTimeout = 5 * time.Minute at top of the file, create timer :=
time.NewTimer(waitIPHardTimeout) before the loop, defer timer.Stop() (or stop it
when returning), and change the select to read from timer.C instead of
time.After; update any error text to reference the constant usage (the timeout
remains for instance id in waitInitialIP).

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 59 to +63
if !strings.HasPrefix(filePath, filepath.Clean(s.basePath)) {
return nil, errors.New(errors.InvalidInput, errTraversal)
}
f, err := os.Open(filePath)
// filepath.Clean is used above, but calling it again inside Open to satisfy gosec G304 explicitly
f, err := os.Open(filepath.Clean(filePath))
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The traversal check strings.HasPrefix(filePath, filepath.Clean(s.basePath)) is bypassable due to common-prefix matches (e.g., "/tmp/base2" has prefix "/tmp/base"). Use a filepath.Rel-based containment check (with a path-separator-aware ".." prefix test) to reliably enforce that reads stay under basePath.

Copilot uses AI. Check for mistakes.
Comment on lines 104 to +109
if !strings.HasPrefix(destPath, filepath.Clean(s.basePath)) {
return 0, errors.New(errors.InvalidInput, errTraversal)
}

f, err := os.Create(destPath)
// G304: Explicitly clean path before creation
f, err := os.Create(filepath.Clean(destPath))
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The traversal check strings.HasPrefix(destPath, filepath.Clean(s.basePath)) is bypassable via prefix tricks (e.g., escaping to a sibling directory whose name shares the base prefix). Use filepath.Rel containment validation (as used elsewhere in the repo) to ensure destPath is truly inside basePath before creating/writing the assembled object.

Copilot uses AI. Check for mistakes.
Comment on lines 118 to +122
if !strings.HasPrefix(partPath, filepath.Clean(s.basePath)) {
return 0, errors.New(errors.InvalidInput, errTraversal)
}
pf, err := os.Open(partPath)
// G304: Explicitly clean path before opening
pf, err := os.Open(filepath.Clean(partPath))
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The part path traversal check uses strings.HasPrefix(partPath, filepath.Clean(s.basePath)), which can be bypassed via common-prefix paths (e.g., base "/tmp/base" vs "/tmp/base2"). Switch to a filepath.Rel-based containment check for partPath before opening/removing parts.

Copilot uses AI. Check for mistakes.
Comment on lines 286 to +294
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
if err != nil {
return err
}

// Limit total number of files to prevent resource exhaustion
if len(zr.File) > 1000 {
return fmt.Errorf("too many files in zip: %d", len(zr.File))
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractZip reads the entire zip from an io.LimitReader into buf and then calls zip.NewReader. If the incoming zip exceeds maxZipSize, the data will be silently truncated by the limit and may produce confusing parse/extract errors. Consider explicitly detecting truncation (e.g., via an io.LimitedReader and checking N == 0 after copy, or probing for one extra byte) and returning a clear "zip too large" error.

Copilot uses AI. Check for mistakes.
Comment on lines +284 to +292

// Safety limit: max 5 minutes regardless of context
timeout := time.After(5 * time.Minute)

for {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-timeout:
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using time.After(5 * time.Minute) inside waitInitialIP can leak the underlying timer if the function returns early (common when IP is obtained quickly), because the timer can't be stopped/drained. Prefer time.NewTimer with defer timer.Stop() (and drain if needed) to avoid accumulating timers over time.

Suggested change
// Safety limit: max 5 minutes regardless of context
timeout := time.After(5 * time.Minute)
for {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-timeout:
// Safety limit: max 5 minutes regardless of context
timer := time.NewTimer(5 * time.Minute)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-timer.C:

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 18, 2026 11:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 84 out of 84 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +335 to +342
// Prevent decompression bomb: limit copy size to 10MB per file
// and check for excessive total size if needed.
const maxFileSize = 10 * 1024 * 1024
_, err = io.CopyN(dst, src, maxFileSize)
if err != nil && err != io.EOF {
return err
}
return nil
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of io.CopyN with maxFileSize will always attempt to read exactly maxFileSize bytes. If the file is smaller than 10MB, io.CopyN returns io.EOF error which is then ignored. However, if the file is exactly or larger than 10MB, data beyond 10MB will be silently truncated without any error being raised. This could lead to corrupted function deployments where files larger than 10MB are silently truncated.

Consider using io.LimitReader instead to properly handle files of any size up to the limit, or return an error if a file exceeds the limit.

Copilot uses AI. Check for mistakes.
Comment on lines +235 to +238
if flags > math.MaxInt32 {
return libvirt.StorageVol{}, fmt.Errorf("flags overflow int32: %d", flags)
}
return r.conn.StorageVolCreateXML(pool, xml, libvirt.StorageVolCreateFlags(int32(flags)))
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overflow check only validates that flags doesn't exceed math.MaxInt32, but doesn't handle the case where flags is a valid uint32 value between math.MaxInt32+1 and math.MaxUint32. When cast to int32, these values will wrap around to negative numbers due to two's complement representation, potentially causing unexpected behavior in the libvirt library.

Consider either: 1) Documenting that only flags in the range 0 to MaxInt32 are supported, or 2) Using a bitmask approach to preserve the bit pattern if negative int32 values are valid flag representations.

Copilot uses AI. Check for mistakes.
Comment on lines +292 to +300
// Safety limit: max 5 minutes regardless of context
timeout := time.After(5 * time.Minute)

for {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-timeout:
return "", fmt.Errorf("timed out waiting for IP for instance %s", id)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout added here creates a hard 5-minute limit that runs in parallel with the context's deadline. If the context has a shorter deadline, it will cancel first (correct behavior). However, if the context has a longer deadline or no deadline, this timeout will trigger and return a generic error message instead of respecting the caller's context.

Consider using context.WithTimeout to create a child context with the 5-minute limit, which would properly integrate with the existing context cancellation mechanism and return context.DeadlineExceeded consistently.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +65 to +68
timeout 60s bash -c 'until pg_isready -h localhost -p 5433 -U cloud; do
echo "Waiting for postgres..."
sleep 2
done
done'
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout 60s bash -c '...' command structure will timeout the bash subprocess, but won't timeout the pg_isready loop inside if it hangs. The timeout command will send SIGTERM to bash, but the loop might not respond to it properly.

A more robust approach would be to use timeout 60s sh -c 'until ...' without wrapping in bash, or add explicit signal handling. However, this is likely fine in practice since pg_isready itself should timeout quickly.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +255 to +259
go test -p 1 -v -run TestInstanceService ./internal/core/services/...

# Test with Libvirt backend
export COMPUTE_BACKEND=libvirt
go test -v -run TestInstanceService ./internal/core/services/...
go test -p 1 -v -run TestInstanceService ./internal/core/services/...
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The -p 1 flag limits test package parallelism to 1, which significantly increases test execution time. This change should be documented in the PR description as it's a notable performance impact.

Consider if this is truly necessary or if there are specific tests that have race conditions that should be fixed instead of globally disabling parallelism.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/core/services/auth_unit_test.go (2)

28-65: ⚠️ Potential issue | 🟡 Minor

Missing AssertExpectations calls leave mock expectations unverified.

  • Register_Success (lines 28–44): mockAuditSvc.On("Log", ...) is set up with .Once() but mockAuditSvc.AssertExpectations(t) is never called. Only mockUserRepo and mockTenantSvc are verified.
  • Login_Success (lines 46–65): mockUserRepo, mockIdentitySvc, and mockAuditSvc all have .Once() expectations but no AssertExpectations call exists at all.

AssertExpectationsForObjects (and AssertExpectations) asserts that everything specified with On and Return was in fact called as expected — calls may have occurred in any order. Without these calls, the tests will pass even if the audit log or identity key creation paths are never exercised.

♻️ Proposed fix
 	t.Run("Register_Success", func(t *testing.T) {
 		...
 		mockUserRepo.AssertExpectations(t)
 		mockTenantSvc.AssertExpectations(t)
+		mockAuditSvc.AssertExpectations(t)
 	})

 	t.Run("Login_Success", func(t *testing.T) {
 		...
 		assert.Equal(t, "test-key", token)
+		mockUserRepo.AssertExpectations(t)
+		mockIdentitySvc.AssertExpectations(t)
+		mockAuditSvc.AssertExpectations(t)
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/services/auth_unit_test.go` around lines 28 - 65, The tests are
missing mock expectation verifications; after the Register_Success subtest call
svc.Register, add mockAuditSvc.AssertExpectations(t) (in addition to existing
mockUserRepo.AssertExpectations(t) and mockTenantSvc.AssertExpectations(t)) to
ensure the audit Log was invoked, and after the Login_Success subtest (after
asserting token) call mockUserRepo.AssertExpectations(t),
mockIdentitySvc.AssertExpectations(t), and mockAuditSvc.AssertExpectations(t) to
verify all .On(...).Once() expectations for GetByEmail, CreateKey, and Log were
met.

15-75: ⚠️ Potential issue | 🟠 Major

Security test regressions: WeakPassword and DuplicateEmail subtests were removed.

The AI summary confirms these two subtests were deleted. Both represent critical validation paths for an auth service:

  • WeakPassword — ensures the service rejects passwords that don't meet strength requirements.
  • DuplicateEmail — ensures the service rejects registration for an already-registered email.

Removing them leaves these code paths untested. Given the PR's stated security-hardening goals, this is a step backwards.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/services/auth_unit_test.go` around lines 15 - 75, Re-add two
subtests to TestAuthService_Unit_Extended: "WeakPassword" should call
svc.Register with a deliberately weak password, assert it returns a
validation/error and that mockUserRepo.On("Create", ...) was not invoked (use
mockUserRepo.AssertNotCalled), and "DuplicateEmail" should have
mockUserRepo.On("GetByEmail", mock.Anything, email).Return(&domain.User{ID:
...}, nil) and then call svc.Register expecting an error and verifying
mockUserRepo.Create was not called (and mockAuditSvc.Log not called) to ensure
duplicate-email registration is rejected; use the existing svc, mockUserRepo,
and mockAuditSvc symbols to add these expectations and assertions.


// Create a valid 32-byte hex master key
masterKey := make([]byte, 32)
rand.Read(masterKey)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

rand.Read error return is unchecked — CI lint blocker.

This is the confirmed errcheck failure causing CI to fail. While crypto/rand.Read calls io.ReadFull on Reader and crashes the program irrecoverably if an error is returned, with the default Reader using OS APIs documented to never return an error on modern systems, the function signature still returns (int, error) and errcheck flags the unhandled return.

🐛 Proposed fix
-	rand.Read(masterKey)
+	_, err = rand.Read(masterKey)
+	require.NoError(t, err)

This requires changing the err declaration on line 39 from := to a pre-declared var err error before the rand.Read call, or restructuring slightly:

 	masterKey := make([]byte, 32)
-	rand.Read(masterKey)
+	_, err := rand.Read(masterKey)
+	require.NoError(t, err)
 	masterKeyHex := hex.EncodeToString(masterKey)
 
-	svc, err := services.NewEncryptionService(mockRepo, masterKeyHex)
+	svc, err := services.NewEncryptionService(mockRepo, masterKeyHex) // err is already declared above; use = here

Or more simply keep separate declarations:

 	masterKey := make([]byte, 32)
-	rand.Read(masterKey)
+	_, randErr := rand.Read(masterKey)
+	require.NoError(t, randErr)
 	masterKeyHex := hex.EncodeToString(masterKey)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rand.Read(masterKey)
_, randErr := rand.Read(masterKey)
require.NoError(t, randErr)
🧰 Tools
🪛 GitHub Check: lint

[failure] 36-36:
Error return value of rand.Read is not checked (errcheck)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/services/encryption_svc_unit_test.go` at line 36, The call to
rand.Read(masterKey) currently ignores its (int, error) return and fails
errcheck; update the test to capture and handle the error from rand.Read — e.g.,
assign the return to _, err (or predeclare var err error and then call
rand.Read(masterKey)) and then check if err != nil and fail the test with
t.Fatalf or t.Fatal including the error; change the existing := usage so the err
variable is properly set and checked around the rand.Read(masterKey) call.

Comment on lines +46 to +51
var (
execCommand = exec.Command
execCommandContext = exec.CommandContext
lookPath = exec.LookPath
osOpen = os.Open
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove the dead package-level var block — it breaks CI and violates the no-globals guideline.

The static-analysis report confirms all four variables (execCommand, execCommandContext, lookPath, osOpen) are flagged as unused by the linter and are causing CI lint failures. The identical symbols are already declared as struct fields on LibvirtAdapter (lines 71–74) and wired up in NewLibvirtAdapter (lines 131–134), so the package-level declarations are pure dead code. Beyond the lint failures, keeping package-level mutable function vars is exactly the anti-pattern the guideline prohibits.

Additionally, the execCommandContext struct field itself is initialised in the constructor but never called in any adapter method — it can also be removed to keep the surface minimal.

🗑️ Proposed fix
-var (
-	execCommand        = exec.Command
-	execCommandContext = exec.CommandContext
-	lookPath           = exec.LookPath
-	osOpen             = os.Open
-)
-
 // LibvirtAdapter implements compute backend operations using libvirt/KVM.

And in the struct definition / constructor, drop execCommandContext if it has no callers:

 	// OS dependencies for testability
 	execCommand        func(name string, arg ...string) *exec.Cmd
-	execCommandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
 	lookPath           func(file string) (string, error)
 	osOpen             func(name string) (*os.File, error)
 		execCommand:        exec.Command,
-		execCommandContext: exec.CommandContext,
 		lookPath:           exec.LookPath,
 		osOpen:             os.Open,

As per coding guidelines, **/*.go: "Do not use global variables (e.g., var DB *sql.DB)".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var (
execCommand = exec.Command
execCommandContext = exec.CommandContext
lookPath = exec.LookPath
osOpen = os.Open
)
🧰 Tools
🪛 GitHub Check: lint

[failure] 50-50:
var osOpen is unused (unused)


[failure] 49-49:
var lookPath is unused (unused)


[failure] 48-48:
var execCommandContext is unused (unused)


[failure] 47-47:
var execCommand is unused (unused)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repositories/libvirt/adapter.go` around lines 46 - 51, Remove the
dead package-level var block (execCommand, execCommandContext, lookPath, osOpen)
and delete the unused execCommandContext field from the LibvirtAdapter struct
and its initialization in NewLibvirtAdapter; scan for any remaining references
to execCommandContext and replace them to use the struct's execCommand,
lookPath, or osOpen fields as appropriate (or remove those calls if unused) so
the package has no global mutable function vars and no unused struct field.

Copilot AI review requested due to automatic review settings February 18, 2026 11:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 84 out of 84 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +247 to +250
if flags > math.MaxInt32 {
return fmt.Errorf("flags overflow int32: %d", flags)
}
return r.conn.StorageVolDelete(vol, libvirt.StorageVolDeleteFlags(int32(flags)))
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same overflow check issue as in StorageVolCreateXML above - flags > math.MaxInt32 doesn't properly validate uint32 to int32 conversion. Use flags > 0x7FFFFFFF instead.

Copilot uses AI. Check for mistakes.
Comment on lines +335 to +342
// Prevent decompression bomb: limit copy size to 10MB per file
// and check for excessive total size if needed.
const maxFileSize = 10 * 1024 * 1024
_, err = io.CopyN(dst, src, maxFileSize)
if err != nil && err != io.EOF {
return err
}
return nil
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of io.CopyN with maxFileSize as the limit will truncate files larger than 10MB without error. If a file in the zip is exactly 10MB or larger, it will be silently truncated. Consider checking if the full file was copied by comparing the number of bytes written, or reading one extra byte to detect if the file exceeds the limit and returning an explicit error in that case.

Copilot uses AI. Check for mistakes.
Comment on lines +309 to 310
if !strings.HasPrefix(filepath.Clean(path), cleanTmpDir+string(os.PathSeparator)) {
return fmt.Errorf("invalid file path in zip: %s", file.Name)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path traversal check has a potential issue. The check !strings.HasPrefix(filepath.Clean(path), cleanTmpDir+string(os.PathSeparator)) would allow a file named exactly as cleanTmpDir (without separator). For example, if cleanTmpDir is "/tmp/fn-123", a zip entry named "fn-123" would pass the check. Consider using filepath.Rel and checking for ".." prefix instead, consistent with the pattern seen elsewhere in the codebase (see internal/storage/node/store.go:184-186).

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +65 to +68
timeout 60s bash -c 'until pg_isready -h localhost -p 5433 -U cloud; do
echo "Waiting for postgres..."
sleep 2
done
done'
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout command usage here wraps the entire bash command, which is good. However, if pg_isready hangs indefinitely, the timeout will kill the bash process but may not necessarily kill the pg_isready subprocess. Consider adding set -e inside the bash command or using timeout directly on pg_isready in a loop for more robust timeout handling.

Copilot uses AI. Check for mistakes.
Comment on lines +235 to +238
if flags > math.MaxInt32 {
return libvirt.StorageVol{}, fmt.Errorf("flags overflow int32: %d", flags)
}
return r.conn.StorageVolCreateXML(pool, xml, libvirt.StorageVolCreateFlags(int32(flags)))
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overflow check flags > math.MaxInt32 is incorrect for unsigned to signed conversion. A uint32 value of 2^31 (0x80000000) would pass this check but when cast to int32 would become -2147483648 (negative), potentially causing unexpected behavior. The correct check should be flags > 0x7FFFFFFF or use a different pattern to ensure the value fits properly in the signed int32 range.

Copilot uses AI. Check for mistakes.
@poyrazK poyrazK merged commit 8570820 into main Feb 18, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants