fix 9P session/agent boundary enforcement

Bug: reading agent files (log, chat, state, etc.) via 9P returned the
same data regardless of which session or agent ID was specified. The
server did not validate agent IDs and had out-of-bounds slice access
on  when the path had fewer components than expected.

Fixes:
- fs/session/root.go: Added  guard before all
   checks in rootReaddir, rootStat, rootOpen.
  Added  guard before  access in rootStat.
  Added agent ID validation — returns error if the requested agent ID
  doesn't match the session's actual agent.
- cmd/olliesrv/server.go: Restructured pathType() to nest agent path
  checks inside a single
  block, preventing proc-path checks from incorrectly matching agent
  paths. Added agent ID validation (returns "" = not found).
  Added agent-path support in makeStat() for Qid version tracking on
  log/chat. Skip blocking files (chat, statewait) in stat-size
  computation to prevent hangs.
This commit is contained in:
Ollie Agent 2026-08-01 13:39:14 +02:00
parent f017486858
commit 3046afd8ed
2 changed files with 83 additions and 28 deletions

View File

@ -543,8 +543,37 @@ func (s *Server) pathType(path string) string {
return "dir"
}
// /session/{id}/agent — directory
if len(parts) == 2 && parts[1] == "agent" {
return "dir"
if len(parts) >= 2 && parts[1] == "agent" {
if len(parts) == 2 {
return "dir"
}
// Validate agent ID
expectedID := sess.Core.Agent().ID()
if expectedID == "" {
expectedID = "0"
}
if parts[2] != expectedID {
return ""
}
// /session/{id}/agent/{aid} — directory
if len(parts) == 3 {
return "dir"
}
// /session/{id}/agent/{aid}/proc — directory
if len(parts) == 4 && parts[3] == "proc" {
return "dir"
}
// /session/{id}/agent/{aid}/proc/{pid} — verify exists
if len(parts) == 5 && parts[3] == "proc" {
for _, p := range sess.Core.Agent().ListDetached() {
if fmt.Sprintf("%d", p.PID) == parts[4] {
return "file"
}
}
return ""
}
// /session/{id}/agent/{aid}/{file} — file
return "file"
}
// /session/{id}/proc/{pid} — verify PID exists in detached list
if len(parts) == 3 && parts[1] == "proc" {
@ -555,27 +584,6 @@ func (s *Server) pathType(path string) string {
}
return ""
}
// /session/{id}/agent/{aid} — directory
if len(parts) == 3 && parts[1] == "agent" {
return "dir"
}
// /session/{id}/agent/{aid}/proc — directory
if len(parts) == 4 && parts[1] == "agent" && parts[3] == "proc" {
return "dir"
}
// /session/{id}/agent/{aid}/proc/{pid} — verify exists
if len(parts) == 5 && parts[1] == "agent" && parts[3] == "proc" {
for _, p := range sess.Core.Agent().ListDetached() {
if fmt.Sprintf("%d", p.PID) == parts[4] {
return "file"
}
}
return ""
}
// /session/{id}/agent/{aid}/{file} — file
if len(parts) >= 4 && parts[1] == "agent" {
return "file"
}
// /session/{id}/{file} — file
return "file"
}
@ -1259,7 +1267,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
// For chat and tailable mutable files, report actual size and
// Qid version so polling tools (tail -f) can detect changes via stat.
// Path format: /session/{sessid}/{file}
// Path format: /session/{sessid}/{file} or /session/{sessid}/agent/{aid}/{file}
if strings.HasPrefix(path, fs.PathSessions) {
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
if len(parts) == 3 && parts[0] == "session" {
@ -1274,6 +1282,20 @@ func (s *Server) makeStat(path string) plan9.Dir {
}
}
}
// Also handle agent paths: /session/{sessid}/agent/{aid}/{file}
parts = strings.SplitN(strings.TrimPrefix(path, "/"), "/", 5)
if len(parts) == 5 && parts[0] == "session" && parts[2] == "agent" {
if sess := fs.Lookup(s.sessionTree, parts[1]); sess != nil {
if base == "log" || base == "chat" {
length, vers := sess.LogInfo()
if length > 64*1024 {
length = 64 * 1024
}
dir.Length = uint64(length)
dir.Qid.Vers = vers
}
}
}
}
// For all other readable files, compute content length so clients
@ -1291,6 +1313,12 @@ func (s *Server) makeStat(path string) plan9.Dir {
case strings.HasPrefix(path, fs.PathSessions):
// HACK: 9pfuse trusts stat Length — if we report 0, the kernel
// never issues a read. Compute size by reading the file content.
// Skip blocking/wait files (chat, statewait) — their size is dynamic.
base := pathBase(path)
if base == "chat" || base == "statewait" {
dir.Length = 4096
break
}
if entry, err := s.openEntry(path); err == nil {
if data, err := entry.Read(); err == nil {
dir.Length = uint64(len(data))

View File

@ -209,7 +209,7 @@ func rootReaddir(rs *rootState, name string) ([]os.DirEntry, error) {
}
return entries, nil
}
if parts[1] == "agent" {
if len(parts) >= 2 && parts[1] == "agent" {
if len(parts) == 2 {
aid := sess.Core.Agent().ID()
if aid == "" {
@ -218,6 +218,16 @@ func rootReaddir(rs *rootState, name string) ([]os.DirEntry, error) {
entries := []os.DirEntry{DirEntry(aid, 0755)}
return entries, nil
}
if len(parts) >= 3 {
// Validate agent ID matches the session's actual agent
expectedID := sess.Core.Agent().ID()
if expectedID == "" {
expectedID = "0"
}
if parts[2] != expectedID {
return nil, fmt.Errorf("agent %s not found in session %s", parts[2], sessID)
}
}
if len(parts) == 3 {
afs := openAgentTree(rs, sess)
entries, err := afs.List()
@ -254,14 +264,23 @@ func rootStat(rs *rootState, name string) (os.FileInfo, error) {
if len(parts) == 1 {
return &SyntheticFileInfo{Name_: sessID, Mode_: Perms[PathSessionDir].DirMode, IsDir_: true}, nil
}
if parts[1] == "agent" {
if len(parts) >= 2 && parts[1] == "agent" {
if len(parts) == 2 {
return &SyntheticFileInfo{Name_: "agent", Mode_: 0755, IsDir_: true}, nil
}
if len(parts) >= 3 {
expectedID := sess.Core.Agent().ID()
if expectedID == "" {
expectedID = "0"
}
if parts[2] != expectedID {
return nil, fmt.Errorf("agent %s not found in session %s", parts[2], sessID)
}
}
if len(parts) == 3 {
return &SyntheticFileInfo{Name_: parts[2], Mode_: 0755, IsDir_: true}, nil
}
if parts[3] == "proc" {
if len(parts) >= 4 && parts[3] == "proc" {
return &SyntheticFileInfo{Name_: "proc", Mode_: 0755, IsDir_: true}, nil
}
afs := openAgentTree(rs, sess)
@ -346,10 +365,18 @@ func rootOpen(rs *rootState, name string) (File, error) {
return nil, fmt.Errorf("%s: is a directory", name)
}
if parts[1] == "agent" {
if len(parts) >= 2 && parts[1] == "agent" {
if len(parts) < 4 {
return nil, fmt.Errorf("%s: is a directory", name)
}
// Validate agent ID
expectedID := sess.Core.Agent().ID()
if expectedID == "" {
expectedID = "0"
}
if parts[2] != expectedID {
return nil, fmt.Errorf("agent %s not found in session %s", parts[2], sessID)
}
afs := openAgentTree(rs, sess)
return afs.Open(parts[3])
}