server: stage forces the version dir 0755; uploadBlob logs store errors (#1050)
Two robustness fixes surfaced while verifying the blob mirror on a canary: - stageVersion's MkdirAll mode is umask-masked, so a hardened operator shell (umask 077) left /opt/lchat/versions/<ver> at 0700 — the sandboxed instance user then couldn't traverse in to exec its binary (systemd 203/EXEC 'Permission denied'), and only root could clean it up. Force 0755 explicitly, exactly as copyFileAtomic already forces the binary's +x. - uploadBlob answered a bare 500 on any store create/commit error, which made a misconfigured object store an opaque failure (cost real debugging time on the canary). Log the underlying error; the member-facing message stays generic.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+12
−1
main.go+2 −0versions.go+10 −1
diff --git a/main.go b/main.go| index 46def25..c69f4fd 100644 |
| --- a/main.go |
| +++ b/main.go |
| @@ -2925,6 +2925,7 @@ func (s *Server) uploadBlob(w http.ResponseWriter, r *http.Request) { |
| // integrity anchor the blob mirror/audit sweeps verify against (#992). |
| blob, err := s.db.blobs.create() |
| if err != nil { |
| + log.Printf("upload: blob create: %v", err) |
| httpError(w, http.StatusInternalServerError, "storage error") |
| return |
| } |
| @@ -2948,6 +2949,7 @@ func (s *Server) uploadBlob(w http.ResponseWriter, r *http.Request) { |
| return |
| } |
| if err := blob.commit(id); err != nil { |
| + log.Printf("upload: blob commit %s: %v", id, err) |
| httpError(w, http.StatusInternalServerError, "storage error") |
| return |
| } |
diff --git a/versions.go b/versions.go| index 001799e..ec3c655 100644 |
| --- a/versions.go |
| +++ b/versions.go |
| @@ -114,7 +114,16 @@ func stageVersion(srcPath, ver string) error { |
| if !validVersion(ver) { |
| return fmt.Errorf("invalid version %q (allowed: letters, digits, . _ -)", ver) |
| } |
| - if err := os.MkdirAll(filepath.Join(versionsDir(), ver), 0o755); err != nil { |
| + dir := filepath.Join(versionsDir(), ver) |
| + if err := os.MkdirAll(dir, 0o755); err != nil { |
| + return err |
| + } |
| + // MkdirAll's mode is masked by the caller's umask, so a tight operator |
| + // umask (e.g. 077 from a hardened shell) leaves the version dir 0700 — |
| + // the sandboxed instance user then can't traverse in to exec its binary |
| + // (203/EXEC "Permission denied", found staging a canary). Force it, like |
| + // copyFileAtomic already forces the binary's +x. |
| + if err := os.Chmod(dir, 0o755); err != nil { |
| return err |
| } |
| return copyFileAtomic(srcPath, versionBinPath(ver), 0o755) |