milestone 16: behavioral fixes for silent failures, locks, counters and the query log
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
//! the next start runs — before a single row is written, so a settings PUT
|
||||
//! cannot leave a configuration the server would refuse to boot from.
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -283,6 +284,25 @@ pub fn applyPut(
|
||||
.fail => |failure| return .{ .fail = failure },
|
||||
};
|
||||
|
||||
// Ruling 18 of milestone 16: argon2id at m=19 MiB is the longest thing this
|
||||
// handler does, and its input is the parsed patch alone — nothing under the
|
||||
// lock. Hashing inside the lock stalled every settings read and every other
|
||||
// mutation for its duration. The login path already hashes unlocked
|
||||
// (auth.zig), and `LiveHash`'s generation check closes the install race.
|
||||
const password = newPassword(patch);
|
||||
var new_hash: []const u8 = "";
|
||||
if (password) |plain| {
|
||||
if (plain.len > auth.max_password_len) {
|
||||
return .{ .fail = .{ .invalid = "web.password is too long" } };
|
||||
}
|
||||
const buf = try arena.alloc(u8, hash_buf_len);
|
||||
new_hash = hashPassword(io, arena, plain, buf) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Canceled => return .{ .fail = .{ .unavailable = "shutting down" } },
|
||||
else => return .{ .fail = .{ .internal = error.Unexpected } },
|
||||
};
|
||||
}
|
||||
|
||||
state.config_lock.lockUncancelable(io);
|
||||
defer state.config_lock.unlock(io);
|
||||
|
||||
@@ -300,21 +320,10 @@ pub fn applyPut(
|
||||
) } };
|
||||
}
|
||||
|
||||
// The password never becomes a row. It is hashed here and the hash is what
|
||||
// the merged configuration — and therefore the settings table — carries.
|
||||
const password = newPassword(patch);
|
||||
// The password never becomes a row: the hash made above is what the merged
|
||||
// configuration — and therefore the settings table — carries.
|
||||
const previous_hash = cfg.web.password_hash;
|
||||
if (password) |plain| {
|
||||
if (plain.len > auth.max_password_len) {
|
||||
return .{ .fail = .{ .invalid = "web.password is too long" } };
|
||||
}
|
||||
const buf = try arena.alloc(u8, hash_buf_len);
|
||||
cfg.web.password_hash = hashPassword(io, arena, plain, buf) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Canceled => return .{ .fail = .{ .unavailable = "shutting down" } },
|
||||
else => return .{ .fail = .{ .internal = error.Unexpected } },
|
||||
};
|
||||
}
|
||||
if (password != null) cfg.web.password_hash = new_hash;
|
||||
cfg.web.password = "";
|
||||
|
||||
if (try problem(arena, cfg)) |text| return .{ .fail = .{ .invalid = text } };
|
||||
@@ -374,6 +383,7 @@ fn problem(arena: Allocator, cfg: model.Config) error{OutOfMemory}!?[]const u8 {
|
||||
/// password set through the API and one set through a config import produce the
|
||||
/// same kind of hash.
|
||||
fn hashPassword(io: std.Io, gpa: Allocator, password: []const u8, buf: []u8) ![]const u8 {
|
||||
hash_stall.park(io);
|
||||
return std.crypto.pwhash.argon2.strHash(password, .{
|
||||
.allocator = gpa,
|
||||
.params = .owasp_2id,
|
||||
@@ -390,6 +400,48 @@ fn hashPassword(io: std.Io, gpa: Allocator, password: []const u8, buf: []u8) ![]
|
||||
};
|
||||
}
|
||||
|
||||
/// Holds a hash still so a test can prove another request runs beside it. The
|
||||
/// hash finishing on its own would prove nothing: before ruling 18 a settings
|
||||
/// GET also completed, it merely waited out the hash first. The storage exists
|
||||
/// in a test build only, and `park` reduces to nothing everywhere else — the
|
||||
/// rotation seam's shape (logging.zig).
|
||||
const hash_stall = if (builtin.is_test) struct {
|
||||
var armed: bool = false;
|
||||
var parked: std.Io.Event = .unset;
|
||||
var release: std.Io.Event = .unset;
|
||||
|
||||
fn park(io: std.Io) void {
|
||||
if (!armed) return;
|
||||
parked.set(io);
|
||||
release.waitUncancelable(io);
|
||||
}
|
||||
} else struct {
|
||||
fn park(io: std.Io) void {
|
||||
_ = io;
|
||||
}
|
||||
};
|
||||
|
||||
/// The seam's controls, for the test that proves a settings read runs beside a
|
||||
/// hash in flight. Present in a test build only.
|
||||
pub const hash_stall_control = if (builtin.is_test) struct {
|
||||
pub fn arm() void {
|
||||
hash_stall.parked = .unset;
|
||||
hash_stall.release = .unset;
|
||||
hash_stall.armed = true;
|
||||
}
|
||||
|
||||
/// Returns once a hash is parked on the seam.
|
||||
pub fn waitParked(io: std.Io) void {
|
||||
hash_stall.parked.waitUncancelable(io);
|
||||
}
|
||||
|
||||
/// Lets the parked hash finish and disarms the seam for the next test.
|
||||
pub fn release(io: std.Io) void {
|
||||
hash_stall.armed = false;
|
||||
hash_stall.release.set(io);
|
||||
}
|
||||
} else struct {};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// routes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user