@push.rocks/smartnftables 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/changelog.md +27 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +14 -0
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +14 -0
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/classes.managednftables.d.ts +21 -0
  8. package/dist_ts/classes.managednftables.js +176 -0
  9. package/dist_ts/index.d.ts +2 -0
  10. package/dist_ts/index.js +2 -1
  11. package/dist_ts/managed.types.d.ts +99 -0
  12. package/dist_ts/managed.types.js +2 -0
  13. package/dist_ts/plugins.d.ts +5 -2
  14. package/dist_ts/plugins.js +6 -3
  15. package/native-notices/crates/block-buffer-0.10.4/license-apache +201 -0
  16. package/native-notices/crates/block-buffer-0.10.4/license-mit +25 -0
  17. package/native-notices/crates/cfg-if-1.0.4/license-apache +201 -0
  18. package/native-notices/crates/cfg-if-1.0.4/license-mit +25 -0
  19. package/native-notices/crates/cpufeatures-0.2.17/license-apache +201 -0
  20. package/native-notices/crates/cpufeatures-0.2.17/license-mit +25 -0
  21. package/native-notices/crates/crypto-common-0.1.7/license-apache +201 -0
  22. package/native-notices/crates/crypto-common-0.1.7/license-mit +25 -0
  23. package/native-notices/crates/digest-0.10.7/license-apache +201 -0
  24. package/native-notices/crates/digest-0.10.7/license-mit +25 -0
  25. package/native-notices/crates/generic-array-0.14.7/license +21 -0
  26. package/native-notices/crates/itoa-1.0.18/license-apache +176 -0
  27. package/native-notices/crates/itoa-1.0.18/license-mit +23 -0
  28. package/native-notices/crates/libc-0.2.189/license-apache +176 -0
  29. package/native-notices/crates/libc-0.2.189/license-mit +25 -0
  30. package/native-notices/crates/memchr-2.8.3/copying +3 -0
  31. package/native-notices/crates/memchr-2.8.3/license-mit +21 -0
  32. package/native-notices/crates/proc-macro2-1.0.107/license-apache +176 -0
  33. package/native-notices/crates/proc-macro2-1.0.107/license-mit +23 -0
  34. package/native-notices/crates/quote-1.0.47/license-apache +176 -0
  35. package/native-notices/crates/quote-1.0.47/license-mit +23 -0
  36. package/native-notices/crates/serde-1.0.229/license-apache +176 -0
  37. package/native-notices/crates/serde-1.0.229/license-mit +23 -0
  38. package/native-notices/crates/serde_core-1.0.229/license-apache +176 -0
  39. package/native-notices/crates/serde_core-1.0.229/license-mit +23 -0
  40. package/native-notices/crates/serde_derive-1.0.229/license-apache +176 -0
  41. package/native-notices/crates/serde_derive-1.0.229/license-mit +23 -0
  42. package/native-notices/crates/serde_json-1.0.151/license-apache +176 -0
  43. package/native-notices/crates/serde_json-1.0.151/license-mit +23 -0
  44. package/native-notices/crates/sha2-0.10.9/license-apache +201 -0
  45. package/native-notices/crates/sha2-0.10.9/license-mit +27 -0
  46. package/native-notices/crates/syn-3.0.5/license-apache +176 -0
  47. package/native-notices/crates/syn-3.0.5/license-mit +23 -0
  48. package/native-notices/crates/typenum-1.20.1/license +1 -0
  49. package/native-notices/crates/typenum-1.20.1/license-apache +201 -0
  50. package/native-notices/crates/typenum-1.20.1/license-mit +21 -0
  51. package/native-notices/crates/unicode-ident-1.0.24/license-apache +176 -0
  52. package/native-notices/crates/unicode-ident-1.0.24/license-mit +23 -0
  53. package/native-notices/crates/unicode-ident-1.0.24/license-unicode +39 -0
  54. package/native-notices/crates/version_check-0.9.5/license-apache +201 -0
  55. package/native-notices/crates/version_check-0.9.5/license-mit +19 -0
  56. package/native-notices/crates/zmij-1.0.23/license-mit +23 -0
  57. package/native-notices/manifest.json +520 -0
  58. package/native-notices/readme.md +36 -0
  59. package/native-notices/rust-standard-library/addr2line-0.25.1-license-apache +201 -0
  60. package/native-notices/rust-standard-library/addr2line-0.25.1-license-mit +25 -0
  61. package/native-notices/rust-standard-library/cargo.lock +522 -0
  62. package/native-notices/rust-standard-library/compiler-builtins-license.txt +275 -0
  63. package/native-notices/rust-standard-library/dependency-notices.html +9072 -0
  64. package/native-notices/rust-standard-library/license-apache +176 -0
  65. package/native-notices/rust-standard-library/license-mit +25 -0
  66. package/native-notices/rust-standard-library/llvm-libunwind-license.txt +311 -0
  67. package/native-notices/rust-standard-library/musl-1.2.5-copyright.txt +193 -0
  68. package/native-notices/rust-standard-library/unicode-license.txt +39 -0
  69. package/package.json +22 -7
  70. package/readme.md +137 -2
  71. package/rust/.cargo/config.toml +2 -0
  72. package/rust/Cargo.lock +192 -0
  73. package/rust/Cargo.toml +13 -0
  74. package/rust/src/main.rs +141 -0
  75. package/rust/src/owner.rs +604 -0
  76. package/rust/src/owner_tests.rs +795 -0
  77. package/rust/src/policy.rs +427 -0
  78. package/rust/src/tests.rs +97 -0
  79. package/rust/src/wire.rs +437 -0
  80. package/scripts/check-native-notices.mjs +27 -0
  81. package/ts/00_commitinfo_data.ts +1 -1
  82. package/ts/classes.managednftables.ts +148 -0
  83. package/ts/index.ts +2 -0
  84. package/ts/managed.types.ts +87 -0
  85. package/ts/plugins.ts +6 -0
  86. /package/{license → license.md} +0 -0
@@ -0,0 +1,604 @@
1
+ use crate::policy::{identifier, Prepared};
2
+ use crate::wire::{self, Attr, Socket};
3
+ use crate::{Error, Result};
4
+ use serde::{Deserialize, Serialize};
5
+ use std::os::unix::fs::MetadataExt;
6
+
7
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
8
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
9
+ pub struct Options {
10
+ pub owner_id: String,
11
+ pub instance_id: String,
12
+ pub table_name: String,
13
+ }
14
+
15
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
16
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
17
+ pub struct Identity {
18
+ pub owner_id: String,
19
+ pub instance_id: String,
20
+ pub table_name: String,
21
+ pub boot_id: String,
22
+ pub namespace_device: String,
23
+ pub namespace_inode: String,
24
+ }
25
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
26
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
27
+ pub struct Receipt {
28
+ pub identity: Identity,
29
+ pub table_handle: String,
30
+ pub revision: u64,
31
+ pub digest: String,
32
+ }
33
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
34
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
35
+ pub struct Applied {
36
+ pub receipt: Receipt,
37
+ pub prepared: Prepared,
38
+ }
39
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
40
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
41
+ pub struct Transition {
42
+ #[serde(deserialize_with = "crate::policy::required_nullable")]
43
+ pub previous: Option<Applied>,
44
+ pub target: Prepared,
45
+ }
46
+ #[derive(Clone, Debug, Serialize)]
47
+ #[serde(rename_all = "camelCase")]
48
+ pub struct Status {
49
+ pub identity: Identity,
50
+ pub state: String,
51
+ pub enforced: bool,
52
+ pub applied: Option<Applied>,
53
+ pub pending: Option<Transition>,
54
+ pub error: Option<String>,
55
+ }
56
+
57
+ struct Graph {
58
+ generation: u32,
59
+ table: Vec<Attr>,
60
+ chains: Vec<Vec<Attr>>,
61
+ rules: Vec<Vec<Attr>>,
62
+ }
63
+ pub struct Owner {
64
+ identity: Identity,
65
+ socket: Option<Socket>,
66
+ applied: Option<Applied>,
67
+ pending: Option<Transition>,
68
+ error: Option<Error>,
69
+ released: bool,
70
+ }
71
+
72
+ fn context() -> Result<(String, String, String)> {
73
+ let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
74
+ .map_err(|_| Error::Unavailable)?
75
+ .trim()
76
+ .to_string();
77
+ if boot.len() != 36 || !boot.bytes().all(|c| c.is_ascii_hexdigit() || c == b'-') {
78
+ return Err(Error::Unavailable);
79
+ }
80
+ let namespace =
81
+ std::fs::metadata("/proc/thread-self/ns/net").map_err(|_| Error::Unavailable)?;
82
+ Ok((
83
+ boot,
84
+ namespace.dev().to_string(),
85
+ namespace.ino().to_string(),
86
+ ))
87
+ }
88
+ fn kernel_floor() -> Result<()> {
89
+ let mut name: libc::utsname = unsafe { std::mem::zeroed() };
90
+ if unsafe { libc::uname(&mut name) } != 0 {
91
+ return Err(Error::Unavailable);
92
+ }
93
+ let release = unsafe { std::ffi::CStr::from_ptr(name.release.as_ptr()) }
94
+ .to_str()
95
+ .map_err(|_| Error::Unsupported)?;
96
+ let mut parts = release.split('.');
97
+ let major: u32 = parts
98
+ .next()
99
+ .ok_or(Error::Unsupported)?
100
+ .parse()
101
+ .map_err(|_| Error::Unsupported)?;
102
+ let minor: u32 = parts
103
+ .next()
104
+ .ok_or(Error::Unsupported)?
105
+ .parse()
106
+ .map_err(|_| Error::Unsupported)?;
107
+ if major < 6 || major == 6 && minor < 9 {
108
+ return Err(Error::Unsupported);
109
+ }
110
+ Ok(())
111
+ }
112
+
113
+ impl Owner {
114
+ pub fn new(options: Options) -> Result<Self> {
115
+ if !identifier(&options.owner_id)
116
+ || !identifier(&options.instance_id)
117
+ || options.instance_id.len() < 16
118
+ || !identifier(&options.table_name)
119
+ || !options.table_name.starts_with("snft_")
120
+ {
121
+ return Err(Error::Invalid);
122
+ }
123
+ let (boot_id, namespace_device, namespace_inode) = context()?;
124
+ Ok(Self {
125
+ identity: Identity {
126
+ owner_id: options.owner_id,
127
+ instance_id: options.instance_id,
128
+ table_name: options.table_name,
129
+ boot_id,
130
+ namespace_device,
131
+ namespace_inode,
132
+ },
133
+ socket: None,
134
+ applied: None,
135
+ pending: None,
136
+ error: None,
137
+ released: false,
138
+ })
139
+ }
140
+ fn socket(&mut self) -> Result<&mut Socket> {
141
+ kernel_floor()?;
142
+ if context()?
143
+ != (
144
+ self.identity.boot_id.clone(),
145
+ self.identity.namespace_device.clone(),
146
+ self.identity.namespace_inode.clone(),
147
+ )
148
+ {
149
+ return Err(Error::Conflict);
150
+ }
151
+ if self.socket.is_none() {
152
+ self.socket = Some(Socket::open()?);
153
+ }
154
+ Ok(self.socket.as_mut().unwrap())
155
+ }
156
+ fn userdata(&self) -> Vec<u8> {
157
+ format!(
158
+ "smartnftables1:{}:{}",
159
+ self.identity.owner_id, self.identity.instance_id
160
+ )
161
+ .into_bytes()
162
+ }
163
+ fn graph(&mut self) -> Result<Option<Graph>> {
164
+ let table_name = self.identity.table_name.clone();
165
+ let socket = self.socket()?;
166
+ for _ in 0..3 {
167
+ let generation = socket.generation()?;
168
+ let table = match socket.query(1, vec![Attr::string(1, &table_name)], false) {
169
+ Ok(mut rows) if rows.len() == 1 => rows.remove(0),
170
+ Err(Error::Absent) => return Ok(None),
171
+ Err(error) => return Err(error),
172
+ _ => return Err(Error::Protocol),
173
+ };
174
+ let chains = socket.query(4, vec![Attr::string(1, &table_name)], true)?;
175
+ let rules = socket.query(7, vec![Attr::string(1, &table_name)], true)?;
176
+ // The managed compiler never creates any of these. Foreign objects reject.
177
+ for operation in [10, 19, 23] {
178
+ if !socket
179
+ .query(operation, vec![Attr::string(1, &table_name)], true)?
180
+ .is_empty()
181
+ {
182
+ return Err(Error::Conflict);
183
+ }
184
+ }
185
+ if generation == socket.generation()? {
186
+ return Ok(Some(Graph {
187
+ generation,
188
+ table,
189
+ chains,
190
+ rules,
191
+ }));
192
+ }
193
+ }
194
+ Err(Error::Conflict)
195
+ }
196
+ fn binding(&self, graph: &Graph, expected: Option<&Applied>) -> Result<()> {
197
+ if wire::text(&graph.table, 1)? != self.identity.table_name
198
+ || wire::one(&graph.table, 6)?.value != self.userdata()
199
+ || ![4, 6].contains(&wire::number(&graph.table, 2)?)
200
+ || graph
201
+ .table
202
+ .iter()
203
+ .any(|attr| ![1, 2, 3, 4, 5, 6, 7].contains(&attr.id()))
204
+ {
205
+ return Err(Error::Conflict);
206
+ }
207
+ if let Some(expected) = expected {
208
+ self.validate_receipt(expected)?;
209
+ if expected.receipt.table_handle != wire::handle(&graph.table, 4)?.to_string() {
210
+ return Err(Error::Conflict);
211
+ }
212
+ }
213
+ Ok(())
214
+ }
215
+ fn validate_receipt(&self, expected: &Applied) -> Result<()> {
216
+ expected.prepared.validate()?;
217
+ let handle: u64 = expected
218
+ .receipt
219
+ .table_handle
220
+ .parse()
221
+ .map_err(|_| Error::Invalid)?;
222
+ if handle == 0
223
+ || handle.to_string() != expected.receipt.table_handle
224
+ || expected.receipt.identity != self.identity
225
+ || expected.receipt.revision != expected.prepared.policy.revision
226
+ || expected.receipt.digest != expected.prepared.digest
227
+ {
228
+ return Err(Error::Conflict);
229
+ }
230
+ Ok(())
231
+ }
232
+ fn matches(&self, graph: &Graph, prepared: &Prepared) -> Result<bool> {
233
+ let program = prepared.program(&self.identity.table_name)?;
234
+ let expected_chains: Vec<_> = program
235
+ .iter()
236
+ .filter(|(kind, _)| *kind == 3)
237
+ .map(|(_, attrs)| chain_identity(attrs))
238
+ .collect::<Result<_>>()?;
239
+ let actual_chains: Vec<_> = graph
240
+ .chains
241
+ .iter()
242
+ .map(|attrs| chain_identity(attrs))
243
+ .collect::<Result<_>>()?;
244
+ let mut expected_chains = expected_chains;
245
+ let mut actual_chains = actual_chains;
246
+ expected_chains.sort();
247
+ actual_chains.sort();
248
+ let expected_rules: Vec<_> = program
249
+ .iter()
250
+ .filter(|(kind, _)| *kind == 6)
251
+ .map(|(_, attrs)| rule_identity(attrs))
252
+ .collect::<Result<_>>()?;
253
+ let actual_rules: Vec<_> = graph
254
+ .rules
255
+ .iter()
256
+ .map(|attrs| rule_identity(attrs))
257
+ .collect::<Result<_>>()?;
258
+ // Dumps order chains differently; preserve order within each chain.
259
+ let regroup = |rules: Vec<(String, Vec<u8>)>| {
260
+ let mut result = std::collections::BTreeMap::<String, Vec<Vec<u8>>>::new();
261
+ for (chain, rule) in rules {
262
+ result.entry(chain).or_default().push(rule);
263
+ }
264
+ result
265
+ };
266
+ Ok(expected_chains == actual_chains && regroup(expected_rules) == regroup(actual_rules))
267
+ }
268
+ fn adopt(&mut self, graph: Graph, transition: &Transition) -> Result<Graph> {
269
+ self.binding(&graph, transition.previous.as_ref())?;
270
+ if !self.matches(&graph, &transition.target)?
271
+ && !transition
272
+ .previous
273
+ .as_ref()
274
+ .map(|prior| self.matches(&graph, &prior.prepared))
275
+ .transpose()?
276
+ .unwrap_or(false)
277
+ {
278
+ return Err(Error::Conflict);
279
+ }
280
+ let port = self.socket()?.port;
281
+ if wire::number(&graph.table, 2)? == 6 {
282
+ if wire::number(&graph.table, 7)? != port {
283
+ return Err(Error::ForeignOwner);
284
+ }
285
+ return Ok(graph);
286
+ }
287
+ let handle = wire::handle(&graph.table, 4)?;
288
+ let table_name = self.identity.table_name.clone();
289
+ self.socket()?.batch(
290
+ graph.generation,
291
+ vec![(0, 0, vec![Attr::string(1, &table_name), Attr::u32(2, 6)])],
292
+ )?;
293
+ // Postclaim proof is essential: generation IDs wrap. Now other sockets
294
+ // cannot mutate this graph between verification and the following batch.
295
+ let after = self.graph()?.ok_or(Error::Conflict)?;
296
+ self.binding(&after, transition.previous.as_ref())?;
297
+ if wire::handle(&after.table, 4)? != handle
298
+ || wire::number(&after.table, 2)? != 6
299
+ || wire::number(&after.table, 7)? != port
300
+ || !self.matches(&after, &transition.target)?
301
+ && !transition
302
+ .previous
303
+ .as_ref()
304
+ .map(|prior| self.matches(&after, &prior.prepared))
305
+ .transpose()?
306
+ .unwrap_or(false)
307
+ {
308
+ // Only drop this socket's acquired ownership, never the persistent graph.
309
+ self.socket = None;
310
+ return Err(Error::Conflict);
311
+ }
312
+ Ok(after)
313
+ }
314
+ fn applied(&self, graph: &Graph, prepared: Prepared) -> Result<Applied> {
315
+ Ok(Applied {
316
+ receipt: Receipt {
317
+ identity: self.identity.clone(),
318
+ table_handle: wire::handle(&graph.table, 4)?.to_string(),
319
+ revision: prepared.policy.revision,
320
+ digest: prepared.digest.clone(),
321
+ },
322
+ prepared,
323
+ })
324
+ }
325
+ pub fn reconcile(&mut self, transition: Transition) -> Result<Applied> {
326
+ if self.released {
327
+ return Err(Error::Conflict);
328
+ }
329
+ transition.target.validate()?;
330
+ if let Some(pending) = &self.pending {
331
+ if pending != &transition {
332
+ return Err(Error::Conflict);
333
+ }
334
+ }
335
+ if let Some(previous) = &transition.previous {
336
+ self.validate_receipt(previous)?;
337
+ if transition.target.policy.revision <= previous.prepared.policy.revision {
338
+ return Err(Error::Conflict);
339
+ }
340
+ }
341
+ // Reject known unsupported privilege/kernel states before admitting a target.
342
+ self.socket()?.generation()?;
343
+ transition.target.validate_interfaces()?;
344
+ self.pending = Some(transition.clone());
345
+ let result = self.reconcile_inner(&transition);
346
+ match &result {
347
+ Ok(applied) => {
348
+ self.applied = Some(applied.clone());
349
+ self.pending = None;
350
+ self.error = None;
351
+ }
352
+ Err(error) => self.error = Some(*error),
353
+ }
354
+ result
355
+ }
356
+ fn reconcile_inner(&mut self, transition: &Transition) -> Result<Applied> {
357
+ let table_name = self.identity.table_name.clone();
358
+ let graph = self.graph()?;
359
+ let mut operations = Vec::new();
360
+ let generation = if let Some(graph) = graph {
361
+ let graph = self.adopt(graph, transition)?;
362
+ if self.matches(&graph, &transition.target)? {
363
+ transition.target.validate_interfaces()?;
364
+ return self.applied(&graph, transition.target.clone());
365
+ }
366
+ let previous = transition.previous.as_ref().ok_or(Error::Conflict)?;
367
+ if !self.matches(&graph, &previous.prepared)? {
368
+ return Err(Error::Conflict);
369
+ }
370
+ for rule in &graph.rules {
371
+ operations.push((
372
+ 8,
373
+ 0,
374
+ vec![
375
+ Attr::string(1, &table_name),
376
+ Attr::string(2, &wire::text(rule, 2)?),
377
+ Attr::u64(3, wire::handle(rule, 3)?),
378
+ ],
379
+ ));
380
+ }
381
+ for chain in &graph.chains {
382
+ operations.push((
383
+ 5,
384
+ 0,
385
+ vec![
386
+ Attr::string(1, &table_name),
387
+ Attr::u64(2, wire::handle(chain, 2)?),
388
+ ],
389
+ ));
390
+ }
391
+ graph.generation
392
+ } else {
393
+ if transition.previous.is_some() || self.applied.is_some() {
394
+ return Err(Error::Conflict);
395
+ }
396
+ operations.push((
397
+ 0,
398
+ 0x600,
399
+ vec![
400
+ Attr::string(1, &table_name),
401
+ Attr::u32(2, 6),
402
+ Attr::bytes(6, self.userdata()),
403
+ ],
404
+ ));
405
+ self.socket()?.generation()?
406
+ };
407
+ for (operation, attributes) in transition.target.program(&table_name)? {
408
+ operations.push((
409
+ operation,
410
+ if operation == 3 { 0x600 } else { 0xc00 },
411
+ attributes,
412
+ ));
413
+ }
414
+ self.socket()?.batch(generation, operations)?;
415
+ let graph = self.graph()?.ok_or(Error::Conflict)?;
416
+ self.binding(&graph, transition.previous.as_ref())?;
417
+ if wire::number(&graph.table, 2)? != 6
418
+ || wire::number(&graph.table, 7)? != self.socket()?.port
419
+ || !self.matches(&graph, &transition.target)?
420
+ {
421
+ return Err(Error::Conflict);
422
+ }
423
+ transition.target.validate_interfaces()?;
424
+ self.applied(&graph, transition.target.clone())
425
+ }
426
+ pub fn inspect(&mut self) -> Status {
427
+ let mut enforced = false;
428
+ if let Some(applied) = self.applied.clone().filter(|_| !self.released) {
429
+ match self.graph().and_then(|graph| {
430
+ let graph = graph.ok_or(Error::Conflict)?;
431
+ self.binding(&graph, Some(&applied))?;
432
+ if wire::number(&graph.table, 2)? != 6
433
+ || wire::number(&graph.table, 7)? != self.socket()?.port
434
+ || !self.matches(&graph, &applied.prepared)?
435
+ {
436
+ return Err(Error::Conflict);
437
+ }
438
+ applied.prepared.validate_interfaces()?;
439
+ Ok(())
440
+ }) {
441
+ Ok(()) => enforced = self.pending.is_none() && self.error.is_none(),
442
+ Err(error) => self.error = Some(error),
443
+ }
444
+ }
445
+ Status {
446
+ identity: self.identity.clone(),
447
+ state: if self.released {
448
+ "released"
449
+ } else if self.error.is_some() || self.pending.is_some() {
450
+ "failed-owned"
451
+ } else if enforced {
452
+ "applied"
453
+ } else {
454
+ "idle"
455
+ }
456
+ .into(),
457
+ enforced,
458
+ applied: self.applied.clone(),
459
+ pending: self.pending.clone(),
460
+ error: self.error.map(|error| error.code().into()),
461
+ }
462
+ }
463
+ pub fn release(&mut self, expected: Applied) -> Result<()> {
464
+ self.validate_receipt(&expected)?;
465
+ if self.pending.is_some() {
466
+ return Err(Error::Conflict);
467
+ }
468
+ if self.released {
469
+ return if self.applied.as_ref() == Some(&expected) {
470
+ Ok(())
471
+ } else {
472
+ Err(Error::Conflict)
473
+ };
474
+ }
475
+ let result = (|| {
476
+ let Some(graph) = self.graph()? else {
477
+ // The exact caller-retained boot/namespace receipt is validated.
478
+ // Absence also confirms cleanup after a lost ACK and full restart.
479
+ return Ok(());
480
+ };
481
+ // Recovery on a fresh owner is explicit and validates this exact snapshot.
482
+ let transition = Transition {
483
+ previous: Some(expected.clone()),
484
+ target: expected.prepared.clone(),
485
+ };
486
+ let graph = self.adopt(graph, &transition)?;
487
+ if !self.matches(&graph, &expected.prepared)? {
488
+ return Err(Error::Conflict);
489
+ }
490
+ // Retain qualified ownership before deletion; a lost delete ACK can
491
+ // subsequently prove this exact table absent without inventing success.
492
+ self.applied = Some(expected.clone());
493
+ self.socket()?.batch(
494
+ graph.generation,
495
+ vec![(2, 0, vec![Attr::u64(4, wire::handle(&graph.table, 4)?)])],
496
+ )?;
497
+ if let Some(after) = self.graph()? {
498
+ if wire::handle(&after.table, 4)? == wire::handle(&graph.table, 4)? {
499
+ return Err(Error::Conflict);
500
+ }
501
+ }
502
+ Ok(())
503
+ })();
504
+ match result {
505
+ Ok(()) => {
506
+ self.released = true;
507
+ self.applied = Some(expected);
508
+ self.error = None;
509
+ self.socket = None;
510
+ Ok(())
511
+ }
512
+ Err(error) => {
513
+ self.error = Some(error);
514
+ Err(error)
515
+ }
516
+ }
517
+ }
518
+ pub fn close(&mut self) -> Result<()> {
519
+ if self.released {
520
+ return Ok(());
521
+ }
522
+ if let Some(applied) = self.applied.clone() {
523
+ return self.release(applied);
524
+ }
525
+ if self.socket.is_some()
526
+ && (self.pending.is_some() || self.error.is_some())
527
+ && self.graph()?.is_some()
528
+ {
529
+ return Err(Error::Conflict);
530
+ }
531
+ self.pending = None;
532
+ self.error = None;
533
+ self.socket = None;
534
+ self.released = true;
535
+ Ok(())
536
+ }
537
+ }
538
+
539
+ fn chain_identity(attributes: &[Attr]) -> Result<Vec<u8>> {
540
+ let mut identity = Vec::new();
541
+ for attribute in attributes {
542
+ match attribute.id() {
543
+ 2 | 6 | 9 => {} // Kernel handle, use count and alignment padding.
544
+ 4 => identity.push(Attr::nested(4, wire::attrs(&attribute.value)?)),
545
+ 1 | 3 | 5 | 7 | 10 => identity.push(attribute.clone()),
546
+ _ => return Err(Error::Conflict),
547
+ }
548
+ }
549
+ serde_json::to_vec(&wire::canonical(&identity, false)?).map_err(|_| Error::Protocol)
550
+ }
551
+ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
552
+ let chain = wire::text(attributes, 2)?;
553
+ if attributes
554
+ .iter()
555
+ .any(|attribute| ![1, 2, 3, 4, 6, 7, 8].contains(&attribute.id()))
556
+ {
557
+ return Err(Error::Conflict);
558
+ }
559
+ let expressions = wire::attrs(&wire::one(attributes, 4)?.value)?;
560
+ let mut result = Vec::new();
561
+ for expression in expressions {
562
+ if expression.id() != 1 {
563
+ return Err(Error::Conflict);
564
+ }
565
+ let fields = wire::attrs(&expression.value)?;
566
+ if fields.len() != 2 {
567
+ return Err(Error::Conflict);
568
+ }
569
+ let name = wire::text(&fields, 1)?;
570
+ let mut data = wire::attrs(&wire::one(&fields, 2)?.value)?;
571
+ let nested: &[u16] = match name.as_str() {
572
+ "immediate" => &[2],
573
+ "cmp" => &[3],
574
+ "bitwise" => &[4, 5],
575
+ "meta" | "payload" => &[],
576
+ _ => return Err(Error::Conflict),
577
+ };
578
+ for attribute in &mut data {
579
+ if nested.contains(&attribute.id()) {
580
+ let mut inner = wire::attrs(&attribute.value)?;
581
+ if name == "immediate" {
582
+ for entry in &mut inner {
583
+ if entry.id() == 2 {
584
+ entry.kind |= 0x8000;
585
+ }
586
+ }
587
+ }
588
+ *attribute = Attr::nested(attribute.id(), inner);
589
+ }
590
+ }
591
+ result.push((name, wire::canonical(&data, false)?));
592
+ }
593
+ let encoded = serde_json::to_vec(&(
594
+ wire::text(attributes, 1)?,
595
+ result,
596
+ &wire::one(attributes, 7)?.value,
597
+ ))
598
+ .map_err(|_| Error::Protocol)?;
599
+ Ok((chain, encoded))
600
+ }
601
+
602
+ #[cfg(test)]
603
+ #[path = "owner_tests.rs"]
604
+ mod native_tests;