Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion dag/gunbc/fabric/fabric_storage_placement.dag
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
module gunbc.fabric_storage_placement

import std.types { String, NonEmptyStr, FilePath, Int, Bool, Port }
import std.types { String, NonEmptyStr, FilePath, Int, Bool, Port, List }
import product.placement_supply { HostIdentity }
import gunbc.fleet_intent_network { operator_host_srv1, fleet_intent_network }
import gunbc.roadmap_dashboard_instance { HostDashboardInstance, dashboard_instance_fabric_storage_root, srv1_live_dashboard_instance }
import std.effect_grant { Read, Write, Execute }
import gunbc.ownership { Ensured }
import gunbc.managed_directory { ManagedDirectory, DirectoryDependent, ManagedDirectoryAdmission, managed_directory_admit, srv1_dashboard_service_principal }
import gunbc.fleet_posix_accounts { fleet_posix_ci_runner_user }
import gunbc.fabric_storage_file_store { fabric_storage_file_root }

// WHERE THE FABRIC DB IS: one host holds the store's files and answers the endpoint. This is the
// POLICY fact of the three (DESIGN 3) -- std.fabric_storage is the interface, the file store and the
Expand Down Expand Up @@ -67,3 +72,53 @@ fn fabric_storage_placement() -> FabricStoragePlacement {
Present { value: e } => FabricStoragePlaced { host: operator_host_srv1, store_root: fabric_storage_store_root(), endpoint: e }
}
}

// ── WHO WRITES THE STORE, AND SO WHAT ITS DIRECTORIES ARE ────────────────────────────────────────
//
// TWO PRINCIPALS WRITE THIS STORE, and until this row neither was stated. The served endpoint
// (gunbc.live_deploy.emit live_deploy_fabric_storage_unit_file) runs as the dashboard service
// principal and publishes objects and advances heads for every remote writer. The CI job principal
// writes it DIRECTLY: gunbc.spark.host_commitment claim_host_effect_live resolves the executor's own
// store through host_effect_landing_live, and on the placed host that is these local files, not the
// endpoint. The deploy created the areas owned by the service principal alone at 0755, so the job
// principal's first create-new in objects/ refused permission_denied (fleet-converge run
// 36139624393) -- a writer the store's ownership never admitted.
//
// Both writes are create-only in flat directories: an object is an exclusive create, and a head
// generation is a staged sibling hard-linked into its name (gunbc.durable_cas_file_store
// DefaultAccessCreateOnly). So each writer needs Write and Execute on objects/ and heads/ and Read to
// list and fetch, and only Execute on the root to reach them; no file is ever rewritten, so no file
// needs group write, and the umask's 0644 already lets each writer read the other's entries.
//
// THE GROUP IS THE CI JOB PRINCIPAL'S OWN PRIMARY GROUP, not a new shared group, and that is derived
// rather than preferred: with one owner and exactly one other writer, the smallest group that admits
// the writer is the one it already has. That needs no group creation, no membership edit and no new
// sudo grant, and grants nothing to anyone but that principal. setgid follows from the group
// differing from the owner's (gunbc.managed_directory managed_directory_permissions). A third writer
// with its own gid would land in the other class and REFUSE at managed_directory_admit, which is the
// point at which a genuinely shared group is modeled -- not before, and never as o+w.
fn fabric_storage_directory(member: NonEmptyStr, path: NonEmptyStr, writes: Bool) -> ManagedDirectory {
let service = srv1_dashboard_service_principal()
let needs = if writes { [Read, Write, Execute] } else { [Execute] }
ManagedDirectory {
member: member,
path: path as String as FilePath,
owner: service,
group_principal: fleet_posix_ci_runner_user,
dependents: [
DirectoryDependent { who: service, needs: [Read, Write, Execute] },
DirectoryDependent { who: fleet_posix_ci_runner_user, needs: needs },
],
ownership: Ensured,
}
}

fn fabric_storage_store_directories() -> List<ManagedDirectoryAdmission> {
let root = fabric_storage_store_root()
let areas = fabric_storage_file_root(root: root)
[
managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-root", path: root, writes: false)),
managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-objects", path: areas.objects, writes: true)),
managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-heads", path: areas.heads, writes: true)),
]
}
22 changes: 19 additions & 3 deletions dag/gunbc/live_deploy/emit.dag
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import std.evaluation_budget { EvaluationLimit, LimitSet, LimitUnset }
import std.types { String, List, Bool, NonEmptyStr, Int, FilePath, CommitSha, Port }
import gunbc.managed_directory {
ManagedDirectory,
ManagedDirectoryAdmission, ManagedDirectoryAdmitted, ManagedDirectoryRefused,
managed_directory_mode_octal,
attempt_state_directory_at,
}
Expand Down Expand Up @@ -110,7 +111,7 @@ import gunbc.live_deploy.spec {
DeploymentSpec,
DeploymentStep, ArtifactStep, Dependency,
DeploymentDependencyStep,
EnsuredSubject, EnsuredPackage, EnsuredHostDirectory, ensured_subject_identity,
EnsuredSubject, EnsuredPackage, EnsuredHostDirectory, EnsuredManagedHostDirectory, ensured_subject_identity,
deployment_provider_state_step,
DeploymentArtifactStep,
ServeBinary, GunbcSourceTree, DispatchWorktreeRoot, AttemptStateRoot, ComputeRoot, SystemdUnit, BeltTimerUnit, FleetConvergeTimerUnit, TailscaleServeMapping, DeployedTreeRemoteUnit, PublicationHelperTimerUnit, FabricStorageServeUnit, FabricStorageServeMapping,
Expand Down Expand Up @@ -1374,10 +1375,25 @@ fn ensure_dependency_steps(dep: DeploymentDependencyStep, privileged: Bool) -> L
EnsuredHostDirectory { path, owner } => [
deploy_raw(command: install_d_owned_for_user(user: owner, path: path as String)),
]
EnsuredManagedHostDirectory { admission } => [deploy_raw(command: ensure_managed_directory_command(admission: admission))]
}
}

// `-g` is spelled with the OWNER'S USER NAME, not a modeled group name, and that is inherited from
// A REFUSED ADMISSION LOWERS TO A MARKER THAT IS NOT SHELL, so the apply fails at this step naming
// the writer the directory's owner and group do not cover -- it never emits an install with a wider
// mode, and never a directory the writer then cannot write.
data managed_directory_writer_uncovered_poison: String = "__GUNBC_DEPLOY_REFUSED__ managed host directory NOT ENSURED: a declared writer is neither its owner nor in its group, and gunbc.managed_directory managed_directory_admit refuses rather than deriving a world-writable mode. member="

fn ensure_managed_directory_command(admission: ManagedDirectoryAdmission) -> String {
match admission {
ManagedDirectoryAdmitted { dir } => install_d_managed_command(dir: dir)
ManagedDirectoryRefused { member, path, writer } =>
join([managed_directory_writer_uncovered_poison, member as String, " path=", path as String, " writer=", writer as String], "")
}
}

// `-g` is spelled with the GROUP PRINCIPAL'S USER NAME (the owner's, unless the directory declares
// another writer's group), not a modeled group name, and that is inherited from
// install_d_owned_command rather than introduced here: PosixUser carries a gid but no group name,
// and srv1's per-user groups happen to share their user's name so the emitted argv is
// byte-identical either way. It is written down because the coincidence is load-bearing and silent
Expand All @@ -1389,7 +1405,7 @@ fn install_d_managed_command(dir: ManagedDirectory) -> String {
install_directory_owned_command(
mode: managed_directory_mode_octal(d: dir) as NonEmptyStr,
owner: dir.owner.name,
group: dir.owner.name,
group: dir.group_principal.name,
path: dir.path as String,
)
}
Expand Down
44 changes: 20 additions & 24 deletions dag/gunbc/live_deploy/spec.dag
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import gunbc.host_layout {
srv1_dispatch_attempt_state_root,
srv1_devboot_artifact_store_root,
}
import gunbc.fabric_storage_file_store { fabric_storage_file_root }
import gunbc.fabric_storage_placement { fabric_storage_store_root, fabric_storage_placed_on, fabric_storage_https_port, fabric_storage_listen_port }
import gunbc.fabric_storage_placement { fabric_storage_store_directories, fabric_storage_placed_on, fabric_storage_https_port, fabric_storage_listen_port }
import gunbc.managed_directory { ManagedDirectoryAdmission, managed_directory_admission_path }
import gunbc.roadmap_dashboard_instance {
HostDashboardInstance,
dashboard_instance_dispatch_worktree_root,
Expand Down Expand Up @@ -127,14 +127,20 @@ type EnsuredDependencyKind =
// reading a deployment spec, so an ensured host resource stays a fact about the HOST and needs no
// instance to be stated.

// A MANAGED host directory is the arm whose owner, group and mode are DERIVED from its declared
// dependents (gunbc.managed_directory) rather than asserted as one owner at the shared 0755. It
// carries the ADMISSION, not the directory, so a directory with a writer the owner and group do not
// cover reaches the emitter as a refusal and can never be lowered to an install at all.
type EnsuredSubject
= EnsuredPackage { package: NonEmptyStr }
| EnsuredHostDirectory { path: NonEmptyStr, owner: NonEmptyStr }
| EnsuredManagedHostDirectory { admission: ManagedDirectoryAdmission }

fn ensured_subject_identity(subject: EnsuredSubject) -> NonEmptyStr {
match subject {
EnsuredPackage { package } => package
EnsuredHostDirectory { path, owner } => path
EnsuredManagedHostDirectory { admission } => managed_directory_admission_path(a: admission) as String as NonEmptyStr
}
}

Expand Down Expand Up @@ -1067,44 +1073,34 @@ fn deployment_ensured_packages() -> List<DeploymentDependencyStep> {
// absent area refuses not_found -- fleet-converge run 35818578751's host-effect claim did exactly
// that. The areas are derived from the store's own root function, so the ensured paths and the
// written paths cannot disagree, and this deployment is the store's one production creator.
//
// AND AN AREA THE WRITER CANNOT WRITE IS NOT A STORE EITHER. The three directories are managed:
// their owner, group and mode come from the writers gunbc.fabric_storage_placement
// fabric_storage_store_directories declares (the service principal and the CI job principal), not
// from gunbc_service_user at the shared 0755 -- which is what refused fleet-converge run
// 36139624393's claim permission_denied. install -d re-applies owner, group and mode to a directory
// that already exists, so the next apply repairs the areas #12126 created.
fn deployment_ensured_srv1_host_directories() -> List<DeploymentDependencyStep> {
[
concat([
DeploymentDependencyStep {
kind: DevbootArtifactStoreDirectory,
subject: EnsuredHostDirectory {
path: srv1_devboot_artifact_store_root as String as NonEmptyStr,
owner: gunbc_service_user,
},
},
DeploymentDependencyStep {
], concat(map(fabric_storage_store_directories(), a => DeploymentDependencyStep {
kind: FabricStorageStoreDirectory,
subject: EnsuredHostDirectory {
path: fabric_storage_store_root(),
owner: gunbc_service_user,
},
},
DeploymentDependencyStep {
kind: FabricStorageStoreDirectory,
subject: EnsuredHostDirectory {
path: fabric_storage_file_root(root: fabric_storage_store_root()).objects,
owner: gunbc_service_user,
},
},
DeploymentDependencyStep {
kind: FabricStorageStoreDirectory,
subject: EnsuredHostDirectory {
path: fabric_storage_file_root(root: fabric_storage_store_root()).heads,
owner: gunbc_service_user,
},
},
subject: EnsuredManagedHostDirectory { admission: a },
}), [
DeploymentDependencyStep {
kind: CodexRuntimeReleaseDirectory,
subject: EnsuredHostDirectory {
path: srv1_codex_runtime_release_root as String as NonEmptyStr,
owner: gunbc_service_user,
},
},
]
]))
}

fn deployment_ensured_steps(target: DeploymentHostTarget) -> List<DeploymentDependencyStep> {
Expand Down
70 changes: 61 additions & 9 deletions dag/gunbc/managed_directory.dag
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,17 @@ type DirectoryDependent {
needs: List<Verb>
}

// THE DIRECTORY'S GROUP IS A PRINCIPAL'S PRIMARY GROUP, NAMED BY THAT PRINCIPAL. It is the capability
// the frontier note above names as missing -- a group distinct from the owner's -- and it is carried
// as a PosixUser rather than a group name because PosixUser is where the roster already states a gid,
// and srv1's primary groups share their user's name (the coincidence install_d_managed_command
// records). For every directory whose group is its owner's, group_principal IS the owner and the
// derivation is unchanged byte for byte.
type ManagedDirectory {
member: NonEmptyStr
path: FilePath
owner: PosixUser
group_principal: PosixUser
dependents: List<DirectoryDependent>
ownership: Ownership
}
Expand All @@ -132,10 +139,10 @@ fn dependent_class_eq(a: DependentClass, b: DependentClass) -> Bool {
}
}

fn dependent_class_of(owner: PosixUser, who: PosixUser) -> DependentClass {
fn dependent_class_of(owner: PosixUser, group_gid: Int, who: PosixUser) -> DependentClass {
if who.uid == owner.uid {
OwnerClass
} else if who.gid == owner.gid {
} else if who.gid == group_gid {
GroupClass
} else {
OtherClass
Expand All @@ -148,14 +155,15 @@ fn needs_verb(needs: List<Verb>, v: Verb) -> Bool {

fn class_needs(
owner: PosixUser,
group_gid: Int,
dependents: List<DirectoryDependent>,
cls: DependentClass,
) -> List<Verb> {
flat_map(
filter(
xs: dependents,
predicate: d => dependent_class_eq(
a: dependent_class_of(owner: owner, who: d.who),
a: dependent_class_of(owner: owner, group_gid: group_gid, who: d.who),
b: cls,
),
),
Expand All @@ -165,31 +173,74 @@ fn class_needs(

fn class_bits(
owner: PosixUser,
group_gid: Int,
dependents: List<DirectoryDependent>,
cls: DependentClass,
) -> PermissionBits {
let needs = class_needs(owner: owner, dependents: dependents, cls: cls)
let needs = class_needs(owner: owner, group_gid: group_gid, dependents: dependents, cls: cls)
PermissionBits {
read: needs_verb(needs: needs, v: Read),
write: needs_verb(needs: needs, v: Write),
execute: needs_verb(needs: needs, v: Execute),
}
}

// SETGID IS DERIVED FROM THE GROUP, NOT CHOSEN: it is set exactly when the directory's group is not
// its owner's. Such a directory exists because a second principal writes into it, and without setgid
// every entry the OWNER creates would land in the owner's group while the other writer's entries land
// in theirs -- two groups inside one directory, so any later tightening of the other-class bits would
// silently cut one writer off from the other's entries. With setgid every entry carries the
// directory's group, and the group is what both writers share.
fn managed_directory_permissions(d: ManagedDirectory) -> FilePermissions {
let g = d.group_principal.gid
FilePermissions {
ownership: FileOwnership { uid: d.owner.uid, gid: d.owner.gid },
ownership: FileOwnership { uid: d.owner.uid, gid: g },
mode: FileMode {
owner: class_bits(owner: d.owner, dependents: d.dependents, cls: OwnerClass),
group: class_bits(owner: d.owner, dependents: d.dependents, cls: GroupClass),
other: class_bits(owner: d.owner, dependents: d.dependents, cls: OtherClass),
owner: class_bits(owner: d.owner, group_gid: g, dependents: d.dependents, cls: OwnerClass),
group: class_bits(owner: d.owner, group_gid: g, dependents: d.dependents, cls: GroupClass),
other: class_bits(owner: d.owner, group_gid: g, dependents: d.dependents, cls: OtherClass),
setuid: false,
setgid: false,
setgid: g != d.owner.gid,
sticky: false,
},
}
}

// ── ADMISSION: A WRITER THE OWNER AND GROUP DO NOT COVER REFUSES ─────────────────────────────────
//
// The derivation above is total: a dependent in the other class that needs Write gets o+w, because
// that is what it asked for. For a directory the fleet ensures that is the wrong answer -- a
// world-writable store is not a way to admit one more writer -- so a consumer that emits an ensure
// takes the directory through this admission, and a writer outside the owner and the group REFUSES
// with its name rather than widening the mode. The remedy the refusal points at is the model:
// make that writer the owner, or the directory's group principal, or (for a third writer with its
// own gid) a shared group, which this module does not model yet and so refuses rather than fakes.
type ManagedDirectoryAdmission
= ManagedDirectoryAdmitted { dir: ManagedDirectory }
| ManagedDirectoryRefused { member: NonEmptyStr, path: FilePath, writer: NonEmptyStr }

fn managed_directory_admit(d: ManagedDirectory) -> ManagedDirectoryAdmission {
let g = d.group_principal.gid
let stray = filter(
xs: d.dependents,
predicate: dep => needs_verb(needs: dep.needs, v: Write) && dependent_class_eq(
a: dependent_class_of(owner: d.owner, group_gid: g, who: dep.who),
b: OtherClass,
),
)
match first(stray) {
Absent => ManagedDirectoryAdmitted { dir: d }
Present { value: dep } => ManagedDirectoryRefused { member: d.member, path: d.path, writer: dep.who.name }
}
}

fn managed_directory_admission_path(a: ManagedDirectoryAdmission) -> FilePath {
match a {
ManagedDirectoryAdmitted { dir } => dir.path
ManagedDirectoryRefused { member: _, path, writer: _ } => path
}
}

fn managed_directory_mode_octal(d: ManagedDirectory) -> String {
file_mode_octal(mode: managed_directory_permissions(d: d).mode)
}
Expand Down Expand Up @@ -232,6 +283,7 @@ fn attempt_state_directory_at(path: FilePath) -> ManagedDirectory {
member: "attempt-state-root" as NonEmptyStr,
path: path,
owner: service,
group_principal: service,
dependents: [
DirectoryDependent { who: service, needs: [Read, Write, Execute] },
],
Expand Down
2 changes: 2 additions & 0 deletions dag/test/claim/devboot_subject_identity_witness_test.dag
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import gunbc.live_deploy.spec {
ArtifactStep,
DeploymentDependencyStep,
EnsuredHostDirectory,
EnsuredManagedHostDirectory,
EnsuredPackage,
ensured_subject_identity,
deployment_ensured_steps,
Expand Down Expand Up @@ -727,6 +728,7 @@ fn devboot_ensured_step_projects_layout(acc: Int, step: DeploymentDependencyStep
EnsuredHostDirectory { path, owner } =>
if (path as String) == (srv1_devboot_artifact_store_root as String) { acc + 1 } else { acc }
EnsuredPackage { package } => acc
EnsuredManagedHostDirectory { admission: _ } => acc
}
}

Expand Down
Loading