Switch to explicit file system Contexts - #1231

Merged
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts
Aug 29, 2026
Merged

Switch to explicit file system Contexts#1231
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts

Conversation

@jaybosamiya-ms

Copy link
Copy Markdown
Member

This PR switches the file system resolver to explicit Contexts, so that the underlying file system(s) and the context that they are used in are separated. Essentially, this means that nothing within the file system is itself aware of CWD (current working dir) or acting user now, and the Context object explicitly carries this. This means that the Linux shim no longer needs to maintain its own cwd: String field and manipulation of it, allowing resolution + permission decisions to live in one place.

Along with this, I also updated the in-mem backend to use the resolver context rather than maintain its own user management, closing out yet another place of unnecessary duplication and potential inconsistency.

Finally, as a drive-by fix: getcwd no longer returns a trailing /, making it more consistent with Linux.

@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

Again, easiest to review one commit at a time :)

@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) marked this pull request as ready for review August 27, 2026 21:03
Base automatically changed from jayb/retire-filesystem-trait to mainAugust 28, 2026 13:39
@wdcui

Copy link
Copy Markdown
Member

GPT/Opus reported the following issues:

  1. Root and mount-root chmod / chown bypass ownership checks

Files: litebox/src/fs/resolver.rs:277-287,824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

This PR moves ownership authorization for chmod and chown from InMem into the resolver. However, filesystem roots and Composer mount roots are represented as PermissionCheck::ByBackend , and may_change_metadata treats that value as automatically authorized. At the same time, the ownership checks were removed from InMem , leaving neither layer responsible for authorization.

For example, a guest running as UID 1000 can call chmod("/") or chown("/") on a root directory owned by UID 0, and the operation reaches InMem without any ownership check.

There is a related TODO in resolver.rs noting that write permission on root directories is currently unchecked, but that TODO concerns permission to add or remove directory entries. It does not recognize this chmod / chown bypass. In fact, path_handle says the backend is expected to enforce metadata permissions, which is no longer true for InMem .

Suggested fix: Return resolver-checkable permission metadata for filesystem and mount roots, or preserve ownership enforcement in mutable backends for ByBackend handles.

  1. Resolver-side metadata authorization introduces a TOCTOU race

Files: litebox/src/fs/resolver.rs:824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

The resolver now authorizes chmod and chown using a copied PermissionInfo , then separately calls the backend to mutate the node. The backend acquires its metadata write lock only during the mutation, so another thread can change ownership between the authorization check and the update.

For example:

  1. UID 1000 starts chmod on a file currently owned by UID 1000 and passes the resolver check.
  2. Another thread changes the owner to UID 2000.
  3. The first thread resumes and changes the mode of a file it no longer owns.

Before this PR, InMem checked ownership while holding the same write lock used for the mutation, so this race did not exist there.

Suggested fix: Pass the acting user into the backend metadata operation and perform authorization while holding the same lock used to update the metadata. Re-reading status in the resolver would still leave a race window.

  1. Overlay copy-up can silently change ownership with a 9P upper layer

Files: litebox/src/fs/overlay.rs:271-283,474-495 , litebox/src/fs/nine_p/mod.rs:500-539 
Severity: Medium
Recognition: Partially known

Overlay previously created the upper node and then called chown to preserve the lower node’s ownership. This PR removes that chown , relying on NewNode.owner being applied atomically by every upper backend.

The 9P backend explicitly documents that Tlcreate and Tmkdir cannot set the requested owning UID: the new node is owned by the user attached to the 9P connection. Therefore, when Overlay copies up a lower file or directory owned by another user, the operation can succeed while silently changing its ownership.

The 9P protocol limitation is clearly documented in the code, so that part is known. What does not appear to be recognized is that removing Overlay’s post-create chown turns that limitation into incorrect copy-up behavior.

Suggested fix: Preserve the post-create chown and rollback for backends that cannot honor NewNode.owner , or make creation fail when the requested ownership cannot be applied rather than returning a differently owned node.

  1.  chdir("") now succeeds instead of returning ENOENT 

File: litebox_shim_linux/src/syscalls/file.rs:1745-1753 
Severity: Medium

The old implementation routed chdir through resolve_path , which explicitly rejected an empty pathname with ENOENT . The new implementation calls Context::resolve directly.

 Context::resolve("") ignores the empty path component and returns the current working directory. Because that path exists and is a directory, sys_chdir returns success without changing anything. Linux requires chdir("") to fail with ENOENT .

Suggested fix: Explicitly reject an empty pathname with ENOENT before calling Context::resolve .

@wdcuiWeidong Cui (wdcui) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment threadlitebox/src/fs/backend.rs
Comment threadlitebox/src/fs/backend.rs Outdated
@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

For the agent findings:

  1. / is a known + documented "permissions are weird here, need to fix up", nothing to be done in this PR
  2. this is again the "atomicity of operations" thing I've mentioned quite a few times before; agent's suggestion is straight up bad and breaks all the layering
  3. not a particularly important concern for any use cases I'm aware of
  4. valid even though unimportant compatibility thing, will fix before merging

@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/method_parameter_count_changed.ron
Failed in:
litebox::fs::resolver::Resolver::open takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:438, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:469
litebox::fs::resolver::Resolver::chmod takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:787, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:833
litebox::fs::resolver::Resolver::chown takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:800, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:848
litebox::fs::resolver::Resolver::unlink takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:818, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:869
litebox::fs::resolver::Resolver::mkdir takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:843, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:893
litebox::fs::resolver::Resolver::rmdir takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:868, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:926
litebox::fs::resolver::Resolver::file_status takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:932, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:989

Merged via the queue into main with commit 49f7231Aug 29, 2026
14 checks passed
@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) deleted the jayb/file-system-contexts branch August 29, 2026 02:19
Jay Bosamiya (Microsoft) (jaybosamiya-ms) added a commit that referenced this pull request Sep 2, 2026
This PR switches the file system resolver to explicit `Context`s, so
that the underlying file system(s) and the context that they are used in
are separated. Essentially, this means that nothing within the file
system is itself aware of CWD (current working dir) or acting user now,
and the `Context` object explicitly carries this. This means that the
Linux shim no longer needs to maintain its own `cwd: String` field and
manipulation of it, allowing resolution + permission decisions to live
in one place.
Along with this, I also updated the in-mem backend to use the resolver
context rather than maintain its own user management, closing out yet
another place of unnecessary duplication and potential inconsistency.
Finally, as a drive-by fix: `getcwd` no longer returns a trailing `/`,
making it more consistent with Linux.
Sign up for freeto 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

@jaybosamiya-ms@wdcui
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Switch to explicit file system Contexts - #1231

Merged
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts
Aug 29, 2026
Merged

Switch to explicit file system Contexts#1231
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts

Conversation

@jaybosamiya-ms

Copy link
Copy Markdown
Member

This PR switches the file system resolver to explicit Contexts, so that the underlying file system(s) and the context that they are used in are separated. Essentially, this means that nothing within the file system is itself aware of CWD (current working dir) or acting user now, and the Context object explicitly carries this. This means that the Linux shim no longer needs to maintain its own cwd: String field and manipulation of it, allowing resolution + permission decisions to live in one place.

Along with this, I also updated the in-mem backend to use the resolver context rather than maintain its own user management, closing out yet another place of unnecessary duplication and potential inconsistency.

Finally, as a drive-by fix: getcwd no longer returns a trailing /, making it more consistent with Linux.

@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

Again, easiest to review one commit at a time :)

@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) marked this pull request as ready for review August 27, 2026 21:03
Base automatically changed from jayb/retire-filesystem-trait to mainAugust 28, 2026 13:39
@wdcui

Copy link
Copy Markdown
Member

GPT/Opus reported the following issues:

  1. Root and mount-root chmod / chown bypass ownership checks

Files: litebox/src/fs/resolver.rs:277-287,824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

This PR moves ownership authorization for chmod and chown from InMem into the resolver. However, filesystem roots and Composer mount roots are represented as PermissionCheck::ByBackend , and may_change_metadata treats that value as automatically authorized. At the same time, the ownership checks were removed from InMem , leaving neither layer responsible for authorization.

For example, a guest running as UID 1000 can call chmod("/") or chown("/") on a root directory owned by UID 0, and the operation reaches InMem without any ownership check.

There is a related TODO in resolver.rs noting that write permission on root directories is currently unchecked, but that TODO concerns permission to add or remove directory entries. It does not recognize this chmod / chown bypass. In fact, path_handle says the backend is expected to enforce metadata permissions, which is no longer true for InMem .

Suggested fix: Return resolver-checkable permission metadata for filesystem and mount roots, or preserve ownership enforcement in mutable backends for ByBackend handles.

  1. Resolver-side metadata authorization introduces a TOCTOU race

Files: litebox/src/fs/resolver.rs:824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

The resolver now authorizes chmod and chown using a copied PermissionInfo , then separately calls the backend to mutate the node. The backend acquires its metadata write lock only during the mutation, so another thread can change ownership between the authorization check and the update.

For example:

  1. UID 1000 starts chmod on a file currently owned by UID 1000 and passes the resolver check.
  2. Another thread changes the owner to UID 2000.
  3. The first thread resumes and changes the mode of a file it no longer owns.

Before this PR, InMem checked ownership while holding the same write lock used for the mutation, so this race did not exist there.

Suggested fix: Pass the acting user into the backend metadata operation and perform authorization while holding the same lock used to update the metadata. Re-reading status in the resolver would still leave a race window.

  1. Overlay copy-up can silently change ownership with a 9P upper layer

Files: litebox/src/fs/overlay.rs:271-283,474-495 , litebox/src/fs/nine_p/mod.rs:500-539 
Severity: Medium
Recognition: Partially known

Overlay previously created the upper node and then called chown to preserve the lower node’s ownership. This PR removes that chown , relying on NewNode.owner being applied atomically by every upper backend.

The 9P backend explicitly documents that Tlcreate and Tmkdir cannot set the requested owning UID: the new node is owned by the user attached to the 9P connection. Therefore, when Overlay copies up a lower file or directory owned by another user, the operation can succeed while silently changing its ownership.

The 9P protocol limitation is clearly documented in the code, so that part is known. What does not appear to be recognized is that removing Overlay’s post-create chown turns that limitation into incorrect copy-up behavior.

Suggested fix: Preserve the post-create chown and rollback for backends that cannot honor NewNode.owner , or make creation fail when the requested ownership cannot be applied rather than returning a differently owned node.

  1.  chdir("") now succeeds instead of returning ENOENT 

File: litebox_shim_linux/src/syscalls/file.rs:1745-1753 
Severity: Medium

The old implementation routed chdir through resolve_path , which explicitly rejected an empty pathname with ENOENT . The new implementation calls Context::resolve directly.

 Context::resolve("") ignores the empty path component and returns the current working directory. Because that path exists and is a directory, sys_chdir returns success without changing anything. Linux requires chdir("") to fail with ENOENT .

Suggested fix: Explicitly reject an empty pathname with ENOENT before calling Context::resolve .

@wdcuiWeidong Cui (wdcui) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment threadlitebox/src/fs/backend.rs
Comment threadlitebox/src/fs/backend.rs Outdated
@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

For the agent findings:

  1. / is a known + documented "permissions are weird here, need to fix up", nothing to be done in this PR
  2. this is again the "atomicity of operations" thing I've mentioned quite a few times before; agent's suggestion is straight up bad and breaks all the layering
  3. not a particularly important concern for any use cases I'm aware of
  4. valid even though unimportant compatibility thing, will fix before merging

@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/method_parameter_count_changed.ron
Failed in:
litebox::fs::resolver::Resolver::open takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:438, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:469
litebox::fs::resolver::Resolver::chmod takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:787, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:833
litebox::fs::resolver::Resolver::chown takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:800, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:848
litebox::fs::resolver::Resolver::unlink takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:818, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:869
litebox::fs::resolver::Resolver::mkdir takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:843, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:893
litebox::fs::resolver::Resolver::rmdir takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:868, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:926
litebox::fs::resolver::Resolver::file_status takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:932, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:989

Merged via the queue into main with commit 49f7231Aug 29, 2026
14 checks passed
@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) deleted the jayb/file-system-contexts branch August 29, 2026 02:19
Jay Bosamiya (Microsoft) (jaybosamiya-ms) added a commit that referenced this pull request Sep 2, 2026
This PR switches the file system resolver to explicit `Context`s, so
that the underlying file system(s) and the context that they are used in
are separated. Essentially, this means that nothing within the file
system is itself aware of CWD (current working dir) or acting user now,
and the `Context` object explicitly carries this. This means that the
Linux shim no longer needs to maintain its own `cwd: String` field and
manipulation of it, allowing resolution + permission decisions to live
in one place.
Along with this, I also updated the in-mem backend to use the resolver
context rather than maintain its own user management, closing out yet
another place of unnecessary duplication and potential inconsistency.
Finally, as a drive-by fix: `getcwd` no longer returns a trailing `/`,
making it more consistent with Linux.
Sign up for freeto 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

@jaybosamiya-ms@wdcui
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Switch to explicit file system Contexts - #1231

Merged
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts
Aug 29, 2026
Merged

Switch to explicit file system Contexts#1231
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts

Conversation

@jaybosamiya-ms

Copy link
Copy Markdown
Member

This PR switches the file system resolver to explicit Contexts, so that the underlying file system(s) and the context that they are used in are separated. Essentially, this means that nothing within the file system is itself aware of CWD (current working dir) or acting user now, and the Context object explicitly carries this. This means that the Linux shim no longer needs to maintain its own cwd: String field and manipulation of it, allowing resolution + permission decisions to live in one place.

Along with this, I also updated the in-mem backend to use the resolver context rather than maintain its own user management, closing out yet another place of unnecessary duplication and potential inconsistency.

Finally, as a drive-by fix: getcwd no longer returns a trailing /, making it more consistent with Linux.

@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

Again, easiest to review one commit at a time :)

@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) marked this pull request as ready for review August 27, 2026 21:03
Base automatically changed from jayb/retire-filesystem-trait to mainAugust 28, 2026 13:39
@wdcui

Copy link
Copy Markdown
Member

GPT/Opus reported the following issues:

  1. Root and mount-root chmod / chown bypass ownership checks

Files: litebox/src/fs/resolver.rs:277-287,824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

This PR moves ownership authorization for chmod and chown from InMem into the resolver. However, filesystem roots and Composer mount roots are represented as PermissionCheck::ByBackend , and may_change_metadata treats that value as automatically authorized. At the same time, the ownership checks were removed from InMem , leaving neither layer responsible for authorization.

For example, a guest running as UID 1000 can call chmod("/") or chown("/") on a root directory owned by UID 0, and the operation reaches InMem without any ownership check.

There is a related TODO in resolver.rs noting that write permission on root directories is currently unchecked, but that TODO concerns permission to add or remove directory entries. It does not recognize this chmod / chown bypass. In fact, path_handle says the backend is expected to enforce metadata permissions, which is no longer true for InMem .

Suggested fix: Return resolver-checkable permission metadata for filesystem and mount roots, or preserve ownership enforcement in mutable backends for ByBackend handles.

  1. Resolver-side metadata authorization introduces a TOCTOU race

Files: litebox/src/fs/resolver.rs:824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

The resolver now authorizes chmod and chown using a copied PermissionInfo , then separately calls the backend to mutate the node. The backend acquires its metadata write lock only during the mutation, so another thread can change ownership between the authorization check and the update.

For example:

  1. UID 1000 starts chmod on a file currently owned by UID 1000 and passes the resolver check.
  2. Another thread changes the owner to UID 2000.
  3. The first thread resumes and changes the mode of a file it no longer owns.

Before this PR, InMem checked ownership while holding the same write lock used for the mutation, so this race did not exist there.

Suggested fix: Pass the acting user into the backend metadata operation and perform authorization while holding the same lock used to update the metadata. Re-reading status in the resolver would still leave a race window.

  1. Overlay copy-up can silently change ownership with a 9P upper layer

Files: litebox/src/fs/overlay.rs:271-283,474-495 , litebox/src/fs/nine_p/mod.rs:500-539 
Severity: Medium
Recognition: Partially known

Overlay previously created the upper node and then called chown to preserve the lower node’s ownership. This PR removes that chown , relying on NewNode.owner being applied atomically by every upper backend.

The 9P backend explicitly documents that Tlcreate and Tmkdir cannot set the requested owning UID: the new node is owned by the user attached to the 9P connection. Therefore, when Overlay copies up a lower file or directory owned by another user, the operation can succeed while silently changing its ownership.

The 9P protocol limitation is clearly documented in the code, so that part is known. What does not appear to be recognized is that removing Overlay’s post-create chown turns that limitation into incorrect copy-up behavior.

Suggested fix: Preserve the post-create chown and rollback for backends that cannot honor NewNode.owner , or make creation fail when the requested ownership cannot be applied rather than returning a differently owned node.

  1.  chdir("") now succeeds instead of returning ENOENT 

File: litebox_shim_linux/src/syscalls/file.rs:1745-1753 
Severity: Medium

The old implementation routed chdir through resolve_path , which explicitly rejected an empty pathname with ENOENT . The new implementation calls Context::resolve directly.

 Context::resolve("") ignores the empty path component and returns the current working directory. Because that path exists and is a directory, sys_chdir returns success without changing anything. Linux requires chdir("") to fail with ENOENT .

Suggested fix: Explicitly reject an empty pathname with ENOENT before calling Context::resolve .

@wdcuiWeidong Cui (wdcui) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment threadlitebox/src/fs/backend.rs
Comment threadlitebox/src/fs/backend.rs Outdated
@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

For the agent findings:

  1. / is a known + documented "permissions are weird here, need to fix up", nothing to be done in this PR
  2. this is again the "atomicity of operations" thing I've mentioned quite a few times before; agent's suggestion is straight up bad and breaks all the layering
  3. not a particularly important concern for any use cases I'm aware of
  4. valid even though unimportant compatibility thing, will fix before merging

@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/method_parameter_count_changed.ron
Failed in:
litebox::fs::resolver::Resolver::open takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:438, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:469
litebox::fs::resolver::Resolver::chmod takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:787, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:833
litebox::fs::resolver::Resolver::chown takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:800, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:848
litebox::fs::resolver::Resolver::unlink takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:818, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:869
litebox::fs::resolver::Resolver::mkdir takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:843, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:893
litebox::fs::resolver::Resolver::rmdir takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:868, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:926
litebox::fs::resolver::Resolver::file_status takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:932, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:989

Merged via the queue into main with commit 49f7231Aug 29, 2026
14 checks passed
@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) deleted the jayb/file-system-contexts branch August 29, 2026 02:19
Jay Bosamiya (Microsoft) (jaybosamiya-ms) added a commit that referenced this pull request Sep 2, 2026
This PR switches the file system resolver to explicit `Context`s, so
that the underlying file system(s) and the context that they are used in
are separated. Essentially, this means that nothing within the file
system is itself aware of CWD (current working dir) or acting user now,
and the `Context` object explicitly carries this. This means that the
Linux shim no longer needs to maintain its own `cwd: String` field and
manipulation of it, allowing resolution + permission decisions to live
in one place.
Along with this, I also updated the in-mem backend to use the resolver
context rather than maintain its own user management, closing out yet
another place of unnecessary duplication and potential inconsistency.
Finally, as a drive-by fix: `getcwd` no longer returns a trailing `/`,
making it more consistent with Linux.
Sign up for freeto 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

@jaybosamiya-ms@wdcui
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Switch to explicit file system Contexts - #1231

Merged
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts
Aug 29, 2026
Merged

Switch to explicit file system Contexts#1231
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts

Conversation

@jaybosamiya-ms

Copy link
Copy Markdown
Member

This PR switches the file system resolver to explicit Contexts, so that the underlying file system(s) and the context that they are used in are separated. Essentially, this means that nothing within the file system is itself aware of CWD (current working dir) or acting user now, and the Context object explicitly carries this. This means that the Linux shim no longer needs to maintain its own cwd: String field and manipulation of it, allowing resolution + permission decisions to live in one place.

Along with this, I also updated the in-mem backend to use the resolver context rather than maintain its own user management, closing out yet another place of unnecessary duplication and potential inconsistency.

Finally, as a drive-by fix: getcwd no longer returns a trailing /, making it more consistent with Linux.

@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

Again, easiest to review one commit at a time :)

@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) marked this pull request as ready for review August 27, 2026 21:03
Base automatically changed from jayb/retire-filesystem-trait to mainAugust 28, 2026 13:39
@wdcui

Copy link
Copy Markdown
Member

GPT/Opus reported the following issues:

  1. Root and mount-root chmod / chown bypass ownership checks

Files: litebox/src/fs/resolver.rs:277-287,824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

This PR moves ownership authorization for chmod and chown from InMem into the resolver. However, filesystem roots and Composer mount roots are represented as PermissionCheck::ByBackend , and may_change_metadata treats that value as automatically authorized. At the same time, the ownership checks were removed from InMem , leaving neither layer responsible for authorization.

For example, a guest running as UID 1000 can call chmod("/") or chown("/") on a root directory owned by UID 0, and the operation reaches InMem without any ownership check.

There is a related TODO in resolver.rs noting that write permission on root directories is currently unchecked, but that TODO concerns permission to add or remove directory entries. It does not recognize this chmod / chown bypass. In fact, path_handle says the backend is expected to enforce metadata permissions, which is no longer true for InMem .

Suggested fix: Return resolver-checkable permission metadata for filesystem and mount roots, or preserve ownership enforcement in mutable backends for ByBackend handles.

  1. Resolver-side metadata authorization introduces a TOCTOU race

Files: litebox/src/fs/resolver.rs:824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

The resolver now authorizes chmod and chown using a copied PermissionInfo , then separately calls the backend to mutate the node. The backend acquires its metadata write lock only during the mutation, so another thread can change ownership between the authorization check and the update.

For example:

  1. UID 1000 starts chmod on a file currently owned by UID 1000 and passes the resolver check.
  2. Another thread changes the owner to UID 2000.
  3. The first thread resumes and changes the mode of a file it no longer owns.

Before this PR, InMem checked ownership while holding the same write lock used for the mutation, so this race did not exist there.

Suggested fix: Pass the acting user into the backend metadata operation and perform authorization while holding the same lock used to update the metadata. Re-reading status in the resolver would still leave a race window.

  1. Overlay copy-up can silently change ownership with a 9P upper layer

Files: litebox/src/fs/overlay.rs:271-283,474-495 , litebox/src/fs/nine_p/mod.rs:500-539 
Severity: Medium
Recognition: Partially known

Overlay previously created the upper node and then called chown to preserve the lower node’s ownership. This PR removes that chown , relying on NewNode.owner being applied atomically by every upper backend.

The 9P backend explicitly documents that Tlcreate and Tmkdir cannot set the requested owning UID: the new node is owned by the user attached to the 9P connection. Therefore, when Overlay copies up a lower file or directory owned by another user, the operation can succeed while silently changing its ownership.

The 9P protocol limitation is clearly documented in the code, so that part is known. What does not appear to be recognized is that removing Overlay’s post-create chown turns that limitation into incorrect copy-up behavior.

Suggested fix: Preserve the post-create chown and rollback for backends that cannot honor NewNode.owner , or make creation fail when the requested ownership cannot be applied rather than returning a differently owned node.

  1.  chdir("") now succeeds instead of returning ENOENT 

File: litebox_shim_linux/src/syscalls/file.rs:1745-1753 
Severity: Medium

The old implementation routed chdir through resolve_path , which explicitly rejected an empty pathname with ENOENT . The new implementation calls Context::resolve directly.

 Context::resolve("") ignores the empty path component and returns the current working directory. Because that path exists and is a directory, sys_chdir returns success without changing anything. Linux requires chdir("") to fail with ENOENT .

Suggested fix: Explicitly reject an empty pathname with ENOENT before calling Context::resolve .

@wdcuiWeidong Cui (wdcui) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment threadlitebox/src/fs/backend.rs
Comment threadlitebox/src/fs/backend.rs Outdated
@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

For the agent findings:

  1. / is a known + documented "permissions are weird here, need to fix up", nothing to be done in this PR
  2. this is again the "atomicity of operations" thing I've mentioned quite a few times before; agent's suggestion is straight up bad and breaks all the layering
  3. not a particularly important concern for any use cases I'm aware of
  4. valid even though unimportant compatibility thing, will fix before merging

@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/method_parameter_count_changed.ron
Failed in:
litebox::fs::resolver::Resolver::open takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:438, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:469
litebox::fs::resolver::Resolver::chmod takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:787, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:833
litebox::fs::resolver::Resolver::chown takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:800, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:848
litebox::fs::resolver::Resolver::unlink takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:818, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:869
litebox::fs::resolver::Resolver::mkdir takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:843, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:893
litebox::fs::resolver::Resolver::rmdir takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:868, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:926
litebox::fs::resolver::Resolver::file_status takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:932, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:989

Merged via the queue into main with commit 49f7231Aug 29, 2026
14 checks passed
@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) deleted the jayb/file-system-contexts branch August 29, 2026 02:19
Jay Bosamiya (Microsoft) (jaybosamiya-ms) added a commit that referenced this pull request Sep 2, 2026
This PR switches the file system resolver to explicit `Context`s, so
that the underlying file system(s) and the context that they are used in
are separated. Essentially, this means that nothing within the file
system is itself aware of CWD (current working dir) or acting user now,
and the `Context` object explicitly carries this. This means that the
Linux shim no longer needs to maintain its own `cwd: String` field and
manipulation of it, allowing resolution + permission decisions to live
in one place.
Along with this, I also updated the in-mem backend to use the resolver
context rather than maintain its own user management, closing out yet
another place of unnecessary duplication and potential inconsistency.
Finally, as a drive-by fix: `getcwd` no longer returns a trailing `/`,
making it more consistent with Linux.
Sign up for freeto 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

@jaybosamiya-ms@wdcui
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Switch to explicit file system Contexts - #1231

Merged
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts
Aug 29, 2026
Merged

Switch to explicit file system Contexts#1231
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts

Conversation

@jaybosamiya-ms

Copy link
Copy Markdown
Member

This PR switches the file system resolver to explicit Contexts, so that the underlying file system(s) and the context that they are used in are separated. Essentially, this means that nothing within the file system is itself aware of CWD (current working dir) or acting user now, and the Context object explicitly carries this. This means that the Linux shim no longer needs to maintain its own cwd: String field and manipulation of it, allowing resolution + permission decisions to live in one place.

Along with this, I also updated the in-mem backend to use the resolver context rather than maintain its own user management, closing out yet another place of unnecessary duplication and potential inconsistency.

Finally, as a drive-by fix: getcwd no longer returns a trailing /, making it more consistent with Linux.

@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

Again, easiest to review one commit at a time :)

@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) marked this pull request as ready for review August 27, 2026 21:03
Base automatically changed from jayb/retire-filesystem-trait to mainAugust 28, 2026 13:39
@wdcui

Copy link
Copy Markdown
Member

GPT/Opus reported the following issues:

  1. Root and mount-root chmod / chown bypass ownership checks

Files: litebox/src/fs/resolver.rs:277-287,824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

This PR moves ownership authorization for chmod and chown from InMem into the resolver. However, filesystem roots and Composer mount roots are represented as PermissionCheck::ByBackend , and may_change_metadata treats that value as automatically authorized. At the same time, the ownership checks were removed from InMem , leaving neither layer responsible for authorization.

For example, a guest running as UID 1000 can call chmod("/") or chown("/") on a root directory owned by UID 0, and the operation reaches InMem without any ownership check.

There is a related TODO in resolver.rs noting that write permission on root directories is currently unchecked, but that TODO concerns permission to add or remove directory entries. It does not recognize this chmod / chown bypass. In fact, path_handle says the backend is expected to enforce metadata permissions, which is no longer true for InMem .

Suggested fix: Return resolver-checkable permission metadata for filesystem and mount roots, or preserve ownership enforcement in mutable backends for ByBackend handles.

  1. Resolver-side metadata authorization introduces a TOCTOU race

Files: litebox/src/fs/resolver.rs:824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

The resolver now authorizes chmod and chown using a copied PermissionInfo , then separately calls the backend to mutate the node. The backend acquires its metadata write lock only during the mutation, so another thread can change ownership between the authorization check and the update.

For example:

  1. UID 1000 starts chmod on a file currently owned by UID 1000 and passes the resolver check.
  2. Another thread changes the owner to UID 2000.
  3. The first thread resumes and changes the mode of a file it no longer owns.

Before this PR, InMem checked ownership while holding the same write lock used for the mutation, so this race did not exist there.

Suggested fix: Pass the acting user into the backend metadata operation and perform authorization while holding the same lock used to update the metadata. Re-reading status in the resolver would still leave a race window.

  1. Overlay copy-up can silently change ownership with a 9P upper layer

Files: litebox/src/fs/overlay.rs:271-283,474-495 , litebox/src/fs/nine_p/mod.rs:500-539 
Severity: Medium
Recognition: Partially known

Overlay previously created the upper node and then called chown to preserve the lower node’s ownership. This PR removes that chown , relying on NewNode.owner being applied atomically by every upper backend.

The 9P backend explicitly documents that Tlcreate and Tmkdir cannot set the requested owning UID: the new node is owned by the user attached to the 9P connection. Therefore, when Overlay copies up a lower file or directory owned by another user, the operation can succeed while silently changing its ownership.

The 9P protocol limitation is clearly documented in the code, so that part is known. What does not appear to be recognized is that removing Overlay’s post-create chown turns that limitation into incorrect copy-up behavior.

Suggested fix: Preserve the post-create chown and rollback for backends that cannot honor NewNode.owner , or make creation fail when the requested ownership cannot be applied rather than returning a differently owned node.

  1.  chdir("") now succeeds instead of returning ENOENT 

File: litebox_shim_linux/src/syscalls/file.rs:1745-1753 
Severity: Medium

The old implementation routed chdir through resolve_path , which explicitly rejected an empty pathname with ENOENT . The new implementation calls Context::resolve directly.

 Context::resolve("") ignores the empty path component and returns the current working directory. Because that path exists and is a directory, sys_chdir returns success without changing anything. Linux requires chdir("") to fail with ENOENT .

Suggested fix: Explicitly reject an empty pathname with ENOENT before calling Context::resolve .

@wdcuiWeidong Cui (wdcui) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment threadlitebox/src/fs/backend.rs
Comment threadlitebox/src/fs/backend.rs Outdated
@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

For the agent findings:

  1. / is a known + documented "permissions are weird here, need to fix up", nothing to be done in this PR
  2. this is again the "atomicity of operations" thing I've mentioned quite a few times before; agent's suggestion is straight up bad and breaks all the layering
  3. not a particularly important concern for any use cases I'm aware of
  4. valid even though unimportant compatibility thing, will fix before merging

@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/method_parameter_count_changed.ron
Failed in:
litebox::fs::resolver::Resolver::open takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:438, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:469
litebox::fs::resolver::Resolver::chmod takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:787, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:833
litebox::fs::resolver::Resolver::chown takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:800, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:848
litebox::fs::resolver::Resolver::unlink takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:818, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:869
litebox::fs::resolver::Resolver::mkdir takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:843, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:893
litebox::fs::resolver::Resolver::rmdir takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:868, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:926
litebox::fs::resolver::Resolver::file_status takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:932, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:989

Merged via the queue into main with commit 49f7231Aug 29, 2026
14 checks passed
@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) deleted the jayb/file-system-contexts branch August 29, 2026 02:19
Jay Bosamiya (Microsoft) (jaybosamiya-ms) added a commit that referenced this pull request Sep 2, 2026
This PR switches the file system resolver to explicit `Context`s, so
that the underlying file system(s) and the context that they are used in
are separated. Essentially, this means that nothing within the file
system is itself aware of CWD (current working dir) or acting user now,
and the `Context` object explicitly carries this. This means that the
Linux shim no longer needs to maintain its own `cwd: String` field and
manipulation of it, allowing resolution + permission decisions to live
in one place.
Along with this, I also updated the in-mem backend to use the resolver
context rather than maintain its own user management, closing out yet
another place of unnecessary duplication and potential inconsistency.
Finally, as a drive-by fix: `getcwd` no longer returns a trailing `/`,
making it more consistent with Linux.
Sign up for freeto 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

@jaybosamiya-ms@wdcui
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Switch to explicit file system Contexts - #1231

Merged
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts
Aug 29, 2026
Merged

Switch to explicit file system Contexts#1231
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts

Conversation

@jaybosamiya-ms

Copy link
Copy Markdown
Member

This PR switches the file system resolver to explicit Contexts, so that the underlying file system(s) and the context that they are used in are separated. Essentially, this means that nothing within the file system is itself aware of CWD (current working dir) or acting user now, and the Context object explicitly carries this. This means that the Linux shim no longer needs to maintain its own cwd: String field and manipulation of it, allowing resolution + permission decisions to live in one place.

Along with this, I also updated the in-mem backend to use the resolver context rather than maintain its own user management, closing out yet another place of unnecessary duplication and potential inconsistency.

Finally, as a drive-by fix: getcwd no longer returns a trailing /, making it more consistent with Linux.

@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

Again, easiest to review one commit at a time :)

@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) marked this pull request as ready for review August 27, 2026 21:03
Base automatically changed from jayb/retire-filesystem-trait to mainAugust 28, 2026 13:39
@wdcui

Copy link
Copy Markdown
Member

GPT/Opus reported the following issues:

  1. Root and mount-root chmod / chown bypass ownership checks

Files: litebox/src/fs/resolver.rs:277-287,824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

This PR moves ownership authorization for chmod and chown from InMem into the resolver. However, filesystem roots and Composer mount roots are represented as PermissionCheck::ByBackend , and may_change_metadata treats that value as automatically authorized. At the same time, the ownership checks were removed from InMem , leaving neither layer responsible for authorization.

For example, a guest running as UID 1000 can call chmod("/") or chown("/") on a root directory owned by UID 0, and the operation reaches InMem without any ownership check.

There is a related TODO in resolver.rs noting that write permission on root directories is currently unchecked, but that TODO concerns permission to add or remove directory entries. It does not recognize this chmod / chown bypass. In fact, path_handle says the backend is expected to enforce metadata permissions, which is no longer true for InMem .

Suggested fix: Return resolver-checkable permission metadata for filesystem and mount roots, or preserve ownership enforcement in mutable backends for ByBackend handles.

  1. Resolver-side metadata authorization introduces a TOCTOU race

Files: litebox/src/fs/resolver.rs:824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

The resolver now authorizes chmod and chown using a copied PermissionInfo , then separately calls the backend to mutate the node. The backend acquires its metadata write lock only during the mutation, so another thread can change ownership between the authorization check and the update.

For example:

  1. UID 1000 starts chmod on a file currently owned by UID 1000 and passes the resolver check.
  2. Another thread changes the owner to UID 2000.
  3. The first thread resumes and changes the mode of a file it no longer owns.

Before this PR, InMem checked ownership while holding the same write lock used for the mutation, so this race did not exist there.

Suggested fix: Pass the acting user into the backend metadata operation and perform authorization while holding the same lock used to update the metadata. Re-reading status in the resolver would still leave a race window.

  1. Overlay copy-up can silently change ownership with a 9P upper layer

Files: litebox/src/fs/overlay.rs:271-283,474-495 , litebox/src/fs/nine_p/mod.rs:500-539 
Severity: Medium
Recognition: Partially known

Overlay previously created the upper node and then called chown to preserve the lower node’s ownership. This PR removes that chown , relying on NewNode.owner being applied atomically by every upper backend.

The 9P backend explicitly documents that Tlcreate and Tmkdir cannot set the requested owning UID: the new node is owned by the user attached to the 9P connection. Therefore, when Overlay copies up a lower file or directory owned by another user, the operation can succeed while silently changing its ownership.

The 9P protocol limitation is clearly documented in the code, so that part is known. What does not appear to be recognized is that removing Overlay’s post-create chown turns that limitation into incorrect copy-up behavior.

Suggested fix: Preserve the post-create chown and rollback for backends that cannot honor NewNode.owner , or make creation fail when the requested ownership cannot be applied rather than returning a differently owned node.

  1.  chdir("") now succeeds instead of returning ENOENT 

File: litebox_shim_linux/src/syscalls/file.rs:1745-1753 
Severity: Medium

The old implementation routed chdir through resolve_path , which explicitly rejected an empty pathname with ENOENT . The new implementation calls Context::resolve directly.

 Context::resolve("") ignores the empty path component and returns the current working directory. Because that path exists and is a directory, sys_chdir returns success without changing anything. Linux requires chdir("") to fail with ENOENT .

Suggested fix: Explicitly reject an empty pathname with ENOENT before calling Context::resolve .

@wdcuiWeidong Cui (wdcui) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment threadlitebox/src/fs/backend.rs
Comment threadlitebox/src/fs/backend.rs Outdated
@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

For the agent findings:

  1. / is a known + documented "permissions are weird here, need to fix up", nothing to be done in this PR
  2. this is again the "atomicity of operations" thing I've mentioned quite a few times before; agent's suggestion is straight up bad and breaks all the layering
  3. not a particularly important concern for any use cases I'm aware of
  4. valid even though unimportant compatibility thing, will fix before merging

@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/method_parameter_count_changed.ron
Failed in:
litebox::fs::resolver::Resolver::open takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:438, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:469
litebox::fs::resolver::Resolver::chmod takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:787, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:833
litebox::fs::resolver::Resolver::chown takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:800, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:848
litebox::fs::resolver::Resolver::unlink takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:818, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:869
litebox::fs::resolver::Resolver::mkdir takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:843, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:893
litebox::fs::resolver::Resolver::rmdir takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:868, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:926
litebox::fs::resolver::Resolver::file_status takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:932, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:989

Merged via the queue into main with commit 49f7231Aug 29, 2026
14 checks passed
@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) deleted the jayb/file-system-contexts branch August 29, 2026 02:19
Jay Bosamiya (Microsoft) (jaybosamiya-ms) added a commit that referenced this pull request Sep 2, 2026
This PR switches the file system resolver to explicit `Context`s, so
that the underlying file system(s) and the context that they are used in
are separated. Essentially, this means that nothing within the file
system is itself aware of CWD (current working dir) or acting user now,
and the `Context` object explicitly carries this. This means that the
Linux shim no longer needs to maintain its own `cwd: String` field and
manipulation of it, allowing resolution + permission decisions to live
in one place.
Along with this, I also updated the in-mem backend to use the resolver
context rather than maintain its own user management, closing out yet
another place of unnecessary duplication and potential inconsistency.
Finally, as a drive-by fix: `getcwd` no longer returns a trailing `/`,
making it more consistent with Linux.
Sign up for freeto 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

@jaybosamiya-ms@wdcui
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Switch to explicit file system Contexts - #1231

Merged
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts
Aug 29, 2026
Merged

Switch to explicit file system Contexts#1231
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts

Conversation

@jaybosamiya-ms

Copy link
Copy Markdown
Member

This PR switches the file system resolver to explicit Contexts, so that the underlying file system(s) and the context that they are used in are separated. Essentially, this means that nothing within the file system is itself aware of CWD (current working dir) or acting user now, and the Context object explicitly carries this. This means that the Linux shim no longer needs to maintain its own cwd: String field and manipulation of it, allowing resolution + permission decisions to live in one place.

Along with this, I also updated the in-mem backend to use the resolver context rather than maintain its own user management, closing out yet another place of unnecessary duplication and potential inconsistency.

Finally, as a drive-by fix: getcwd no longer returns a trailing /, making it more consistent with Linux.

@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

Again, easiest to review one commit at a time :)

@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) marked this pull request as ready for review August 27, 2026 21:03
Base automatically changed from jayb/retire-filesystem-trait to mainAugust 28, 2026 13:39
@wdcui

Copy link
Copy Markdown
Member

GPT/Opus reported the following issues:

  1. Root and mount-root chmod / chown bypass ownership checks

Files: litebox/src/fs/resolver.rs:277-287,824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

This PR moves ownership authorization for chmod and chown from InMem into the resolver. However, filesystem roots and Composer mount roots are represented as PermissionCheck::ByBackend , and may_change_metadata treats that value as automatically authorized. At the same time, the ownership checks were removed from InMem , leaving neither layer responsible for authorization.

For example, a guest running as UID 1000 can call chmod("/") or chown("/") on a root directory owned by UID 0, and the operation reaches InMem without any ownership check.

There is a related TODO in resolver.rs noting that write permission on root directories is currently unchecked, but that TODO concerns permission to add or remove directory entries. It does not recognize this chmod / chown bypass. In fact, path_handle says the backend is expected to enforce metadata permissions, which is no longer true for InMem .

Suggested fix: Return resolver-checkable permission metadata for filesystem and mount roots, or preserve ownership enforcement in mutable backends for ByBackend handles.

  1. Resolver-side metadata authorization introduces a TOCTOU race

Files: litebox/src/fs/resolver.rs:824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

The resolver now authorizes chmod and chown using a copied PermissionInfo , then separately calls the backend to mutate the node. The backend acquires its metadata write lock only during the mutation, so another thread can change ownership between the authorization check and the update.

For example:

  1. UID 1000 starts chmod on a file currently owned by UID 1000 and passes the resolver check.
  2. Another thread changes the owner to UID 2000.
  3. The first thread resumes and changes the mode of a file it no longer owns.

Before this PR, InMem checked ownership while holding the same write lock used for the mutation, so this race did not exist there.

Suggested fix: Pass the acting user into the backend metadata operation and perform authorization while holding the same lock used to update the metadata. Re-reading status in the resolver would still leave a race window.

  1. Overlay copy-up can silently change ownership with a 9P upper layer

Files: litebox/src/fs/overlay.rs:271-283,474-495 , litebox/src/fs/nine_p/mod.rs:500-539 
Severity: Medium
Recognition: Partially known

Overlay previously created the upper node and then called chown to preserve the lower node’s ownership. This PR removes that chown , relying on NewNode.owner being applied atomically by every upper backend.

The 9P backend explicitly documents that Tlcreate and Tmkdir cannot set the requested owning UID: the new node is owned by the user attached to the 9P connection. Therefore, when Overlay copies up a lower file or directory owned by another user, the operation can succeed while silently changing its ownership.

The 9P protocol limitation is clearly documented in the code, so that part is known. What does not appear to be recognized is that removing Overlay’s post-create chown turns that limitation into incorrect copy-up behavior.

Suggested fix: Preserve the post-create chown and rollback for backends that cannot honor NewNode.owner , or make creation fail when the requested ownership cannot be applied rather than returning a differently owned node.

  1.  chdir("") now succeeds instead of returning ENOENT 

File: litebox_shim_linux/src/syscalls/file.rs:1745-1753 
Severity: Medium

The old implementation routed chdir through resolve_path , which explicitly rejected an empty pathname with ENOENT . The new implementation calls Context::resolve directly.

 Context::resolve("") ignores the empty path component and returns the current working directory. Because that path exists and is a directory, sys_chdir returns success without changing anything. Linux requires chdir("") to fail with ENOENT .

Suggested fix: Explicitly reject an empty pathname with ENOENT before calling Context::resolve .

@wdcuiWeidong Cui (wdcui) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment threadlitebox/src/fs/backend.rs
Comment threadlitebox/src/fs/backend.rs Outdated
@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

For the agent findings:

  1. / is a known + documented "permissions are weird here, need to fix up", nothing to be done in this PR
  2. this is again the "atomicity of operations" thing I've mentioned quite a few times before; agent's suggestion is straight up bad and breaks all the layering
  3. not a particularly important concern for any use cases I'm aware of
  4. valid even though unimportant compatibility thing, will fix before merging

@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/method_parameter_count_changed.ron
Failed in:
litebox::fs::resolver::Resolver::open takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:438, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:469
litebox::fs::resolver::Resolver::chmod takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:787, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:833
litebox::fs::resolver::Resolver::chown takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:800, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:848
litebox::fs::resolver::Resolver::unlink takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:818, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:869
litebox::fs::resolver::Resolver::mkdir takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:843, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:893
litebox::fs::resolver::Resolver::rmdir takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:868, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:926
litebox::fs::resolver::Resolver::file_status takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:932, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:989

Merged via the queue into main with commit 49f7231Aug 29, 2026
14 checks passed
@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) deleted the jayb/file-system-contexts branch August 29, 2026 02:19
Jay Bosamiya (Microsoft) (jaybosamiya-ms) added a commit that referenced this pull request Sep 2, 2026
This PR switches the file system resolver to explicit `Context`s, so
that the underlying file system(s) and the context that they are used in
are separated. Essentially, this means that nothing within the file
system is itself aware of CWD (current working dir) or acting user now,
and the `Context` object explicitly carries this. This means that the
Linux shim no longer needs to maintain its own `cwd: String` field and
manipulation of it, allowing resolution + permission decisions to live
in one place.
Along with this, I also updated the in-mem backend to use the resolver
context rather than maintain its own user management, closing out yet
another place of unnecessary duplication and potential inconsistency.
Finally, as a drive-by fix: `getcwd` no longer returns a trailing `/`,
making it more consistent with Linux.
Sign up for freeto 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

@jaybosamiya-ms@wdcui
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Switch to explicit file system Contexts - #1231

Merged
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts
Aug 29, 2026
Merged

Switch to explicit file system Contexts#1231
Jay Bosamiya (Microsoft) (jaybosamiya-ms) merged 8 commits into
mainfrom
jayb/file-system-contexts

Conversation

@jaybosamiya-ms

Copy link
Copy Markdown
Member

This PR switches the file system resolver to explicit Contexts, so that the underlying file system(s) and the context that they are used in are separated. Essentially, this means that nothing within the file system is itself aware of CWD (current working dir) or acting user now, and the Context object explicitly carries this. This means that the Linux shim no longer needs to maintain its own cwd: String field and manipulation of it, allowing resolution + permission decisions to live in one place.

Along with this, I also updated the in-mem backend to use the resolver context rather than maintain its own user management, closing out yet another place of unnecessary duplication and potential inconsistency.

Finally, as a drive-by fix: getcwd no longer returns a trailing /, making it more consistent with Linux.

@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

Again, easiest to review one commit at a time :)

@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) marked this pull request as ready for review August 27, 2026 21:03
Base automatically changed from jayb/retire-filesystem-trait to mainAugust 28, 2026 13:39
@wdcui

Copy link
Copy Markdown
Member

GPT/Opus reported the following issues:

  1. Root and mount-root chmod / chown bypass ownership checks

Files: litebox/src/fs/resolver.rs:277-287,824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

This PR moves ownership authorization for chmod and chown from InMem into the resolver. However, filesystem roots and Composer mount roots are represented as PermissionCheck::ByBackend , and may_change_metadata treats that value as automatically authorized. At the same time, the ownership checks were removed from InMem , leaving neither layer responsible for authorization.

For example, a guest running as UID 1000 can call chmod("/") or chown("/") on a root directory owned by UID 0, and the operation reaches InMem without any ownership check.

There is a related TODO in resolver.rs noting that write permission on root directories is currently unchecked, but that TODO concerns permission to add or remove directory entries. It does not recognize this chmod / chown bypass. In fact, path_handle says the backend is expected to enforce metadata permissions, which is no longer true for InMem .

Suggested fix: Return resolver-checkable permission metadata for filesystem and mount roots, or preserve ownership enforcement in mutable backends for ByBackend handles.

  1. Resolver-side metadata authorization introduces a TOCTOU race

Files: litebox/src/fs/resolver.rs:824-865 , litebox/src/fs/in_mem.rs:574-607 
Severity: High

The resolver now authorizes chmod and chown using a copied PermissionInfo , then separately calls the backend to mutate the node. The backend acquires its metadata write lock only during the mutation, so another thread can change ownership between the authorization check and the update.

For example:

  1. UID 1000 starts chmod on a file currently owned by UID 1000 and passes the resolver check.
  2. Another thread changes the owner to UID 2000.
  3. The first thread resumes and changes the mode of a file it no longer owns.

Before this PR, InMem checked ownership while holding the same write lock used for the mutation, so this race did not exist there.

Suggested fix: Pass the acting user into the backend metadata operation and perform authorization while holding the same lock used to update the metadata. Re-reading status in the resolver would still leave a race window.

  1. Overlay copy-up can silently change ownership with a 9P upper layer

Files: litebox/src/fs/overlay.rs:271-283,474-495 , litebox/src/fs/nine_p/mod.rs:500-539 
Severity: Medium
Recognition: Partially known

Overlay previously created the upper node and then called chown to preserve the lower node’s ownership. This PR removes that chown , relying on NewNode.owner being applied atomically by every upper backend.

The 9P backend explicitly documents that Tlcreate and Tmkdir cannot set the requested owning UID: the new node is owned by the user attached to the 9P connection. Therefore, when Overlay copies up a lower file or directory owned by another user, the operation can succeed while silently changing its ownership.

The 9P protocol limitation is clearly documented in the code, so that part is known. What does not appear to be recognized is that removing Overlay’s post-create chown turns that limitation into incorrect copy-up behavior.

Suggested fix: Preserve the post-create chown and rollback for backends that cannot honor NewNode.owner , or make creation fail when the requested ownership cannot be applied rather than returning a differently owned node.

  1.  chdir("") now succeeds instead of returning ENOENT 

File: litebox_shim_linux/src/syscalls/file.rs:1745-1753 
Severity: Medium

The old implementation routed chdir through resolve_path , which explicitly rejected an empty pathname with ENOENT . The new implementation calls Context::resolve directly.

 Context::resolve("") ignores the empty path component and returns the current working directory. Because that path exists and is a directory, sys_chdir returns success without changing anything. Linux requires chdir("") to fail with ENOENT .

Suggested fix: Explicitly reject an empty pathname with ENOENT before calling Context::resolve .

@wdcuiWeidong Cui (wdcui) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment threadlitebox/src/fs/backend.rs
Comment threadlitebox/src/fs/backend.rs Outdated
@jaybosamiya-ms

Copy link
Copy Markdown
MemberAuthor

For the agent findings:

  1. / is a known + documented "permissions are weird here, need to fix up", nothing to be done in this PR
  2. this is again the "atomicity of operations" thing I've mentioned quite a few times before; agent's suggestion is straight up bad and breaks all the layering
  3. not a particularly important concern for any use cases I'm aware of
  4. valid even though unimportant compatibility thing, will fix before merging

@github-actions

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/method_parameter_count_changed.ron
Failed in:
litebox::fs::resolver::Resolver::open takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:438, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:469
litebox::fs::resolver::Resolver::chmod takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:787, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:833
litebox::fs::resolver::Resolver::chown takes 3 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:800, but now takes 4 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:848
litebox::fs::resolver::Resolver::unlink takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:818, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:869
litebox::fs::resolver::Resolver::mkdir takes 2 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:843, but now takes 3 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:893
litebox::fs::resolver::Resolver::rmdir takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:868, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:926
litebox::fs::resolver::Resolver::file_status takes 1 parameters in /home/runner/work/litebox/litebox/target/semver-checks/git-main/1377097bbab33437a9330a8580ff187730db4873/litebox/src/fs/resolver.rs:932, but now takes 2 parameters in /home/runner/work/litebox/litebox/litebox/src/fs/resolver.rs:989

Merged via the queue into main with commit 49f7231Aug 29, 2026
14 checks passed
@jaybosamiya-ms
Jay Bosamiya (Microsoft) (jaybosamiya-ms) deleted the jayb/file-system-contexts branch August 29, 2026 02:19
Jay Bosamiya (Microsoft) (jaybosamiya-ms) added a commit that referenced this pull request Sep 2, 2026
This PR switches the file system resolver to explicit `Context`s, so
that the underlying file system(s) and the context that they are used in
are separated. Essentially, this means that nothing within the file
system is itself aware of CWD (current working dir) or acting user now,
and the `Context` object explicitly carries this. This means that the
Linux shim no longer needs to maintain its own `cwd: String` field and
manipulation of it, allowing resolution + permission decisions to live
in one place.
Along with this, I also updated the in-mem backend to use the resolver
context rather than maintain its own user management, closing out yet
another place of unnecessary duplication and potential inconsistency.
Finally, as a drive-by fix: `getcwd` no longer returns a trailing `/`,
making it more consistent with Linux.
Sign up for freeto 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

@jaybosamiya-ms@wdcui