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
10 changes: 10 additions & 0 deletions dag/extdeps/access/posix.dag
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ fn file_mode_octal(mode: FileMode) -> String {
], "")
}

// The same mode as the NUMBER the kernel takes (chmod(2), open(2) mode argument): the four octal
// digits above weighted 8^3..8^0. file_mode_octal is the spelling for an argv; this is the value for
// an operation input, so neither consumer re-derives the digit layout.
fn file_mode_bits(mode: FileMode) -> Int {
file_mode_special_octal_digit(mode: mode) * 512
+ permission_bits_octal_digit(bits: mode.owner) * 64
+ permission_bits_octal_digit(bits: mode.group) * 8
+ permission_bits_octal_digit(bits: mode.other)
}

// THE SAME RELATION READ BACKWARD, which is why it lives here and not at the call site that wanted
// it. file_mode_octal above renders a FileMode as chmod's numeric spelling; this reads that
// spelling back. One grammar, two directions (DESIGN section 4) -- a parser authored beside a
Expand Down
19 changes: 19 additions & 0 deletions dag/extdeps/filesystem/filesystem_io.dag
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module extdeps.filesystem.filesystem_io

import v2.std.algebra { filter }
import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }
import std.types { Bool, FilePath, Int, List, String }
Expand Down Expand Up @@ -668,6 +669,12 @@ data filesystem_absence_establishment_adoption_standing: String = "RUNG: structu
// is declared on the two operations that have a consumer for it (Read and WriteCreateNew, both
// consumed by extdeps.realization.materialization_store_local); an operation gains it when a
// consumer needs it, since the transport answers it for every verb.
//
// WriteCreateNewWithMode: WriteCreateNew with the PUBLISHED MODE DECLARED rather than left to the process umask. The
// canonical realization (extdeps.filesystem.rust_realization) sets these bits on the staged inode
// before the hard_link publishes it, so the name never appears at any other mode. `mode` is the
// permission-bit value of a modeled extdeps.access.posix FileMode (file_mode_bits), never a
// literal; a value outside 0..=0o7777 refuses before anything is created.
service Filesystem {

operation Write {
Expand Down Expand Up @@ -704,6 +711,18 @@ service Filesystem {
transport file { path: "{path}", verb: "write_create_new" }
}

operation WriteCreateNewWithMode {
input { path: String, content: String, mode: Int }
output {
success: Bool from "write_success"
bytes_written: Int from "bytes_written"
path: String from "path"
error: String from "error"
error_kind: String from "error_kind"
}
transport file { path: "{path}", verb: "write_create_new_with_mode" }
}

operation Read {
input { path: String }
output {
Expand Down
2 changes: 1 addition & 1 deletion dag/extdeps/filesystem/filesystem_rust_realization.dag
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ fn rust_file_create_staging_candidate_attempt_limit_def() -> String {
)
}

data rust_file_write_create_new_fn_def: String = "fn gunbc_file_write_create_new(file_path: &str, content: &[u8]) -> std::io::Result<()> {\n use std::io::Write;\n static GUNBC_CREATE_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);\n let mut attempted: u32 = 0;\n let (mut staged, staging_path) = loop {\n if attempted >= GUNBC_CREATE_STAGING_CANDIDATE_ATTEMPT_LIMIT {\n return Err(std::io::Error::other(format!(\n \"gunbc create-new: StagingCandidateBudgetExhausted attempted={} limit={}\",\n attempted, GUNBC_CREATE_STAGING_CANDIDATE_ATTEMPT_LIMIT\n )));\n }\n attempted += 1;\n let seq = GUNBC_CREATE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);\n let candidate = format!(\"{}.gunbc-create-{}-{}\", file_path, std::process::id(), seq);\n match std::fs::OpenOptions::new()\n .write(true)\n .create_new(true)\n .open(&candidate)\n {\n Ok(file) => break (file, candidate),\n Err(occupied) if occupied.kind() == std::io::ErrorKind::AlreadyExists => continue,\n Err(host) => return Err(host),\n }\n };\n if let Err(staging_err) = staged.write_all(content) {\n let _ = std::fs::remove_file(&staging_path);\n return Err(staging_err);\n }\n if let Err(sync_err) = staged.sync_all() {\n let _ = std::fs::remove_file(&staging_path);\n return Err(sync_err);\n }\n drop(staged);\n let published = std::fs::hard_link(&staging_path, file_path);\n let _ = std::fs::remove_file(&staging_path);\n published\n}\n"
data rust_file_write_create_new_fn_def: String = "fn gunbc_file_write_create_new(\n file_path: &str,\n content: &[u8],\n declared_mode: Option<u32>,\n) -> std::io::Result<()> {\n use std::io::Write;\n static GUNBC_CREATE_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);\n let mut attempted: u32 = 0;\n let (mut staged, staging_path) = loop {\n if attempted >= GUNBC_CREATE_STAGING_CANDIDATE_ATTEMPT_LIMIT {\n return Err(std::io::Error::other(format!(\n \"gunbc create-new: StagingCandidateBudgetExhausted attempted={} limit={}\",\n attempted, GUNBC_CREATE_STAGING_CANDIDATE_ATTEMPT_LIMIT\n )));\n }\n attempted += 1;\n let seq = GUNBC_CREATE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);\n let candidate = format!(\"{}.gunbc-create-{}-{}\", file_path, std::process::id(), seq);\n match std::fs::OpenOptions::new()\n .write(true)\n .create_new(true)\n .open(&candidate)\n {\n Ok(file) => break (file, candidate),\n Err(occupied) if occupied.kind() == std::io::ErrorKind::AlreadyExists => continue,\n Err(host) => return Err(host),\n }\n };\n if let Some(declared) = declared_mode {\n #[cfg(unix)]\n let applied = {\n use std::os::unix::fs::PermissionsExt;\n staged.set_permissions(std::fs::Permissions::from_mode(declared))\n };\n #[cfg(not(unix))]\n let applied: std::io::Result<()> = Err(std::io::Error::new(\n std::io::ErrorKind::Unsupported,\n format!(\n \"gunbc create-new: declared mode {} is unavailable on this platform\",\n declared\n ),\n ));\n if let Err(mode_err) = applied {\n let _ = std::fs::remove_file(&staging_path);\n return Err(mode_err);\n }\n }\n if let Err(staging_err) = staged.write_all(content) {\n let _ = std::fs::remove_file(&staging_path);\n return Err(staging_err);\n }\n if let Err(sync_err) = staged.sync_all() {\n let _ = std::fs::remove_file(&staging_path);\n return Err(sync_err);\n }\n drop(staged);\n let published = std::fs::hard_link(&staging_path, file_path);\n let _ = std::fs::remove_file(&staging_path);\n published\n}\n"

// THE SEED'S COPY, AS A COMMITTED GENERATED ARTIFACT.
//
Expand Down
9 changes: 9 additions & 0 deletions dag/extdeps/tools/findutils.dag
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ fn find_symlink_target_glob_command(
)
}

// RUN ONE COMMAND OVER THE REGULAR FILES DIRECTLY IN A DIRECTORY THAT ONE USER OWNS. -maxdepth 1
// keeps it to that directory, -type f excludes symlinks (find does not follow them by default), and
// -user narrows it to entries the invoking principal can act on, so an entry another principal owns
// is never handed to a command that would refuse it. `-exec ... {} +` batches the paths into as few
// invocations as ARG_MAX admits; find exits nonzero when any invocation does.
fn find_owned_regular_files_exec_argv(dir: String, owner: String, command: List<String>) -> List<String> {
concat(concat([find_program as String, dir, "-maxdepth", "1", "-type", "f", "-user", owner, "-exec"], command), ["{}", "+"])
}

// THE REGULAR FILES UNDER A ROOT, ONE `<size> <relative path>` LINE EACH (find(1) -printf: %s is the
// size in bytes, %P the path with the starting point removed). A plain argv: no shell, and find's own
// status is the one the caller reads.
Expand Down
24 changes: 21 additions & 3 deletions dag/gunbc/durable_cas_file_store.dag
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import std.list { distinct_by_key }
import std.content_hash { ContentHash, content_hash_of_value, compare_content_hash, ContentHashEqual, ContentHashDifferent, ContentHashCrossFamilyIncomparable }
import std.durable_compare_and_set { CasAttempt, cas_attempt, CasOutcome, CasCommitted, CasPreconditionFailed, CasStoreRefused, CasExpectation, ExpectSlotAbsent, ExpectSlotGeneration, CasGeneration, CasSlotVersion, CasReadableAbsent, CasReadablePresent, CasSlotObservation, CasObservedReadable, CasObservedUnreadable, CasUnreadableMalformed, CasUnreadableReadRefused, CasStoreFailure, CasSlotObservationRefused, CasGenerationPublicationRefused, cas_generation_exhausted, cas_generation_first, cas_generation_count, cas_generation_successor, CasSuccessorGeneration, CasSuccessorExhausted }
import std.checked_arithmetic { int_inclusive_max }
import v2.std.algebra { skip }
import extdeps.access.posix { FileMode, file_mode_bits }
import extdeps.filesystem.filesystem_io {
Filesystem,
FilesystemCreateNew, FilesystemCreated, FilesystemCreateTargetOccupied, FilesystemCreateRefused, FilesystemCreateKindUnrecognized,
Expand Down Expand Up @@ -563,9 +565,16 @@ data cas_key_not_slot_addressable_detail: NonEmptyStr = "key is not slot-address
// coproduct collapses to the single admitted publication operation. It is retired by that and by
// nothing else; in particular it is NOT retired by a failure-atomic mode-carrying write, which would
// be a different and larger capability than this population needs.
// DeclaredModeCreateOnly is DefaultAccessCreateOnly's publication -- staged, synced, hard-linked,
// so equally failure-atomic -- with the published mode declared by the store's own model rather than
// left to whatever umask the writing process runs under. A store with more than one declared reader
// needs it: under a `umask 077` step every generation publishes 0600 and the other reader is locked
// out (gunbc.fabric_storage_placement). It is not a confidentiality binding; that remains
// OwnerOnlyCreateOnly's, with its declared limitation above.
type CasGenerationPublication
= DefaultAccessCreateOnly
| OwnerOnlyCreateOnly
| DeclaredModeCreateOnly { mode: FileMode }

// THE ACTUATOR TAKES AN ADMITTED PUBLICATION, NOT A PATH AND SOME BYTES.
//
Expand Down Expand Up @@ -615,6 +624,15 @@ fn cas_commit_at(admitted: AdmittedCasPublication) -> CasOutcome<NonEmptyStr> {
post: observe_cas_slot_state(root: root, key: attempt.key),
)
}
DeclaredModeCreateOnly { mode } => {
let write = Filesystem.WriteCreateNewWithMode(path: path, content: content, mode: file_mode_bits(mode: mode))
cas_outcome_from_create(
attempt: attempt,
target: target,
created: filesystem_create_new(path: write.path, success: write.success, error: write.error, error_kind: write.error_kind),
post: observe_cas_slot_state(root: root, key: attempt.key),
)
}
OwnerOnlyCreateOnly => {
let write = Filesystem.WriteOwnerOnly(path: path, content: content)
if write.success {
Expand Down Expand Up @@ -811,9 +829,9 @@ fn cas_name_is_digits(s: String) -> Bool {
fn cas_key_of_slot_name(name: String) -> NonEmptyStr? {
let parts = name.split(delimiter: ".")
if count(parts) < 2 { none } else {
let last = match parts |> get(count(parts) - 1) {
null => ""
p => p
let last = match first(skip(parts, count(parts) - 1)) {
Absent => ""
Present { value: p } => p
}
if !cas_name_is_digits(s: last) { none } else {
let key = join(parts |> take(count(parts) - 1), ".")
Expand Down
17 changes: 14 additions & 3 deletions dag/gunbc/fabric/fabric_storage_client.dag
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import gunbc.fabric_storage_wire {
FabricIdentityRead, FabricIdentityUnproxied, FabricIdentityReadRefused, fabric_storage_unwire_identity,
}
import gunbc.fabric_storage_file_store {
FabricStorageFileRoot, fabric_storage_file_root, fabric_storage_file_head, fabric_storage_file_put, fabric_storage_file_advance, fabric_storage_file_closure,
FabricStorageFileRoot, fabric_storage_file_head, fabric_storage_file_put, fabric_storage_file_advance, fabric_storage_file_closure,
}
import gunbc.fabric_storage_placement { FabricStoragePlacement, FabricStoragePlaced, FabricStorageUnplaced }
import gunbc.fabric_storage_placement { FabricStoragePlacement, FabricStoragePlaced, FabricStorageUnplaced, fabric_storage_placed_file_root, FabricStoragePlacedRootReady, FabricStoragePlacedRootRefused }
import product.placement_supply { HostIdentity, host_identity_eq }

// THE FABRIC DB AS A CALLER REACHES IT: std.fabric_storage's four operations, each bound to one of two
Expand All @@ -44,12 +44,18 @@ type FabricStorageBinding
= FabricStorageLocalFiles { root: FabricStorageFileRoot }
| FabricStorageServed { endpoint: NonEmptyStr }
| FabricStorageBindingUnplaced
| FabricStorageBindingRefused { detail: NonEmptyStr }

fn fabric_storage_binding_for(placement: FabricStoragePlacement, executor: HostIdentity) -> FabricStorageBinding {
match placement {
FabricStorageUnplaced => FabricStorageBindingUnplaced
FabricStoragePlaced { host: h, store_root: r, endpoint: e } =>
if host_identity_eq(a: h, b: executor) { FabricStorageLocalFiles { root: fabric_storage_file_root(root: r) } } else { FabricStorageServed { endpoint: e } }
if host_identity_eq(a: h, b: executor) {
match fabric_storage_placed_file_root(store_root: r) {
FabricStoragePlacedRootReady { root } => FabricStorageLocalFiles { root: root }
FabricStoragePlacedRootRefused { detail } => FabricStorageBindingRefused { detail: detail }
}
} else { FabricStorageServed { endpoint: e } }
}
}

Expand Down Expand Up @@ -108,6 +114,7 @@ fn expectation_request_words(expected: FabricHeadExpectation) -> String {
fn fabric_storage_head(binding: FabricStorageBinding, name: NonEmptyStr) -> FabricHeadRead {
match binding {
FabricStorageBindingUnplaced => FabricHeadReadRefused { fault: FabricStoreUnreachable { detail: fabric_storage_unplaced_detail } }
FabricStorageBindingRefused { detail: d } => FabricHeadReadRefused { fault: FabricStoreUnreachable { detail: d } }
FabricStorageLocalFiles { root: r } => fabric_storage_file_head(root: r, name: name)
FabricStorageServed { endpoint: e } =>
match served_post(endpoint: e, operation: "head", request: name as String) {
Expand All @@ -120,6 +127,7 @@ fn fabric_storage_head(binding: FabricStorageBinding, name: NonEmptyStr) -> Fabr
fn fabric_storage_put(binding: FabricStorageBinding, object: FabricObject) -> FabricPut {
match binding {
FabricStorageBindingUnplaced => FabricPutRefused { fault: FabricStoreUnreachable { detail: fabric_storage_unplaced_detail } }
FabricStorageBindingRefused { detail: d } => FabricPutRefused { fault: FabricStoreUnreachable { detail: d } }
FabricStorageLocalFiles { root: r } => fabric_storage_file_put(root: r, object: object)
FabricStorageServed { endpoint: e } =>
match served_post(endpoint: e, operation: "put", request: fabric_object_preimage(object: object) as String) {
Expand All @@ -132,6 +140,7 @@ fn fabric_storage_put(binding: FabricStorageBinding, object: FabricObject) -> Fa
fn fabric_storage_advance(binding: FabricStorageBinding, name: NonEmptyStr, expected: FabricHeadExpectation, target: FabricObjectRef) -> FabricHeadAdvance {
match binding {
FabricStorageBindingUnplaced => FabricHeadAdvanceRefused { fault: FabricStoreUnreachable { detail: fabric_storage_unplaced_detail } }
FabricStorageBindingRefused { detail: d } => FabricHeadAdvanceRefused { fault: FabricStoreUnreachable { detail: d } }
FabricStorageLocalFiles { root: r } => fabric_storage_file_advance(root: r, name: name, expected: expected, target: target)
FabricStorageServed { endpoint: e } =>
match served_post(endpoint: e, operation: "advance", request: join([name as String, " ", expectation_request_words(expected: expected), " ", fabric_object_ref_wire(object: target) as String], "")) {
Expand All @@ -144,6 +153,7 @@ fn fabric_storage_advance(binding: FabricStorageBinding, name: NonEmptyStr, expe
fn fabric_storage_closure(binding: FabricStorageBinding, name: NonEmptyStr, bound: Nat) -> FabricClosureRead {
match binding {
FabricStorageBindingUnplaced => FabricClosureRefused { fault: FabricStoreUnreachable { detail: fabric_storage_unplaced_detail } }
FabricStorageBindingRefused { detail: d } => FabricClosureRefused { fault: FabricStoreUnreachable { detail: d } }
FabricStorageLocalFiles { root: r } => fabric_storage_file_closure(root: r, name: name, bound: bound)
FabricStorageServed { endpoint: e } =>
match served_post(endpoint: e, operation: "closure", request: join([name as String, " ", to_string(bound)], "")) {
Expand All @@ -161,6 +171,7 @@ fn fabric_storage_closure(binding: FabricStorageBinding, name: NonEmptyStr, boun
fn fabric_storage_presented_identity(binding: FabricStorageBinding) -> FabricIdentityRead {
match binding {
FabricStorageBindingUnplaced => FabricIdentityReadRefused { fault: FabricStoreUnreachable { detail: fabric_storage_unplaced_detail } }
FabricStorageBindingRefused { detail: d } => FabricIdentityReadRefused { fault: FabricStoreUnreachable { detail: d } }
FabricStorageLocalFiles { root: _ } => FabricIdentityUnproxied
FabricStorageServed { endpoint: e } =>
match served_post(endpoint: e, operation: "identity", request: "") {
Expand Down
Loading