Skip to main content

Module command

Module command 

Source
Expand description

The exec parser (the ACL model), the most security-critical code in this crate.

An SSH exec request carries an arbitrary byte string that a normal server hands to a shell. gunnar hands it to parse, which either produces one of the verbs in SERVICES with a validated [RepoPath], or refuses. There is no fallthrough, no shell, and no process.

§The third command landed, and it was one table row (LFS over SSH)

git-lfs 3.0’s pure-SSH transfer is here. The dispatch was written so adding it would be a row in SERVICES and a variant of GitService rather than a refactor, and that held: nothing in this module or in crate::server matches on the set of services, the required grant level travels in the table, and the dispatch compares levels rather than matching on which verb it got.

One thing did have to change, and it is the verification the LFS-over-SSH design asked for. Measured against the real git-lfs client on 2026-08-03 (git-lfs/ssh/ssh.go, GetLFSExeAndArgs), the exec line is

args = append(args, fmt.Sprintf("%s %s %s", command, meta.Path, operation))

i.e. git-lfs-transfer <path> <operation>, three tokens, and the path is not quoted at all. <operation> is upload or download, it is chosen once per connection, and it decides the grant: upload needs Write, download needs Read. A ServiceSpec { required: Access } cannot say that, so the field became Requirement, which is either a fixed level or a list of operations with a level each. The requirement still travels in the table. There is still no second authorisation path.

§What is accepted

git-upload-pack  '<path>'                needs Read
git-receive-pack '<path>'                needs Write
git-lfs-transfer <path> download         needs Read
git-lfs-transfer <path> upload           needs Write

<path> may be single-quoted in git’s sq_quote style (including the '\'' escape for an embedded quote), or bare. Everything else is refused: any other verb, a missing path, an unknown operation, a control byte or NUL anywhere, an unterminated quote, trailing bytes after the closing quote, and anything over MAX_EXEC_LEN.

§Unquoting lives HERE, and the sandbox lives in gunnar-core

git sends one argv element with the quotes inside it. There is no shell on gunnar’s side to take them off. Measured against git 2.53.0 on 2026-08-03 with a GIT_SSH_COMMAND that echoes its arguments:

ssh://git@host/ns/repo.git   →   git-upload-pack '/ns/repo.git'
git@host:ns/repo.git         →   git-upload-pack 'ns/repo.git'

Stripping that quoting is a transport concern, so [unquote] does it here and gunnar_core::resolve_repo_path, the one path sandbox, is handed an already-unquoted string and does not know SSH exists. It refuses a quote as the forbidden character it is, which is also the red half of this module: if [unquote] ever stopped working, paths would be refused rather than silently resolving somewhere.

This crate used to carry its own sandbox, gunnar_ssh::path::RepoPath, and it disagreed with gunnar-core’s: it demanded exactly two components where the other permits nested namespaces. It is deleted. Nested namespaces are legal (team/sub/repo.git is namespace team/sub), and the escalation that would otherwise imply is closed by grants not inheriting.

Structs§

GitCommand
A parsed, validated exec request.
OperationSpec
One accepted <operation> of a verb that takes one.
ServiceSpec
One row of the dispatch table.

Enums§

CommandError
Why an exec request was refused.
GitService
Which git service an exec request selects.
Operation
The <operation> argument of git-lfs-transfer.
Requirement
How a row’s grant level is arrived at.

Constants§

MAX_EXEC_LEN
Longest exec request accepted, in bytes.
NOT_SERVED
Git verbs gunnar recognises and does not serve, each with the sentence the client is told. A verb here is answered in git’s own protocol (an ERR packet and a clean exit), not with a channel failure; a verb in neither table is the bash case and gets the channel failure.
SERVICES
The whole dispatch table. Adding a command is adding a row here.

Functions§

parse
Parse an exec request. See the module documentation.