forked from rosa/hakurei
Compare commits
611 Commits
389844b1ea
...
v0.4.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
3a5f4af114
|
|||
|
6195260480
|
|||
|
21044d5a60
|
|||
|
025810bf0f
|
|||
|
20354c0411
|
|||
|
9fbcd0daf2
|
|||
|
248f44a5a7
|
|||
|
401dd57cbc
|
|||
|
854bcc998b
|
|||
|
358247be5b
|
|||
|
f517a8ef07
|
|||
|
973218f91f
|
|||
|
0ea195837b
|
|||
|
1348991634
|
|||
|
14b445fde5
|
|||
|
218f7fa345
|
|||
|
cd493fd95f
|
|||
|
9db70c83e3
|
|||
|
4e09241e5f
|
|||
|
bd4b300ea6
|
|||
|
6dc8214a1a
|
|||
|
cada5a46ad
|
|||
|
e747942829
|
|||
|
33b855123e
|
|||
|
58ce134718
|
|||
|
2066093343
|
|||
|
07509b3ba2
|
|||
|
a7485d587a
|
|||
|
4892beefc1
|
|||
|
7ab54b8c94
|
|||
|
a4fab67811
|
|||
|
ed5cdd38a4
|
|||
|
f6318304ee
|
|||
|
cb618093d5
|
|||
|
b0b2471c0c
|
|||
|
344d2b8207
|
|||
|
3938e8bce5
|
|||
|
aee15b4f2a
|
|||
|
18b1103fdc
|
|||
|
c5a02da0f0
|
|||
|
c0c2f3233a
|
|||
|
bda00ac90e
|
|||
|
79342e3053
|
|||
|
825bf24731
|
|||
|
08112f0b90
|
|||
|
e1a1e1e399
|
|||
|
92b61889a6
|
|||
|
28e133c298
|
|||
|
323dcb2820
|
|||
|
f46a0370a7
|
|||
|
5ee66a86ee
|
|||
|
d75b8b6e32
|
|||
|
7c9481d38d
|
|||
|
983ab4378f
|
|||
|
5b5c096d31
|
|||
|
5f49127120
|
|||
|
164f8c873d
|
|||
|
9274b642ec
|
|||
|
5992be3dd3
|
|||
|
c661a3b63a
|
|||
|
df0bb877db
|
|||
|
f333b8fbd6
|
|||
|
928a9f61e9
|
|||
|
ce06539eca
|
|||
|
69908e5a41
|
|||
|
b5445573a8
|
|||
|
f869ff95a1
|
|||
|
725f2e0ef3
|
|||
|
4ffa20cd3f
|
|||
|
38450db74a
|
|||
|
9344f694c7
|
|||
|
e8bb5a622d
|
|||
|
22e508fe17
|
|||
|
b18ecf5832
|
|||
|
ec29e755fb
|
|||
|
9aaf160ff4
|
|||
|
e192fca762
|
|||
|
7eafc7b1e4
|
|||
|
282462c2f0
|
|||
|
d9f522d648
|
|||
|
a0b0c7ecc9
|
|||
|
2b8809da7a
|
|||
|
84cda19d63
|
|||
|
154ab953c1
|
|||
|
34e11dd312
|
|||
|
140cb3cc47
|
|||
|
82c974d656
|
|||
|
8c17f201e5
|
|||
|
63e0457538
|
|||
|
ca93264c1f
|
|||
|
94173eafff
|
|||
|
e28c4aa3c0
|
|||
|
8e8410ce38
|
|||
|
8fb6fdaa80
|
|||
|
db69dcf0be
|
|||
|
76c1fb84c8
|
|||
|
729be19af3
|
|||
|
4d04f86f4a
|
|||
|
42cea1e7c6
|
|||
|
83498b5a8a
|
|||
|
9e824452bd
|
|||
|
56937ac396
|
|||
|
6c2f7089b6
|
|||
|
74c18390b4
|
|||
|
1490b32387
|
|||
|
fbd2329d50
|
|||
|
f398f71fa9
|
|||
|
4d017b1309
|
|||
|
cd1d447664
|
|||
|
8b87e0eb76
|
|||
|
f4215ddda5
|
|||
|
652b4c2ba0
|
|||
|
75715f4590
|
|||
|
87c3b3663d
|
|||
|
6d792023b2
|
|||
|
3e62cf379f
|
|||
|
6d991f2644
|
|||
|
a2cc28f53c
|
|||
|
598c7aa30f
|
|||
|
b18f40d974
|
|||
|
12a6061051
|
|||
|
02c3823ed7
|
|||
|
cfbd8bd6f1
|
|||
|
fc7a339ed2
|
|||
|
97fdd5db8b
|
|||
|
a5d9f76f50
|
|||
|
b313dfefb0
|
|||
|
5b32297178
|
|||
|
32cc72821e
|
|||
|
958473e6f4
|
|||
|
cc52bb3035
|
|||
|
7816f8b523
|
|||
|
91e4229e32
|
|||
|
c346cb4c57
|
|||
|
0e0d5e8def
|
|||
|
ea875416d4
|
|||
|
8c7109a2d5
|
|||
|
caf3e0db4b
|
|||
|
12b9f51128
|
|||
|
d15f965d0c
|
|||
|
fadd1b14f7
|
|||
|
3458806685
|
|||
|
8ca70550ab
|
|||
|
7eebf49b98
|
|||
|
8c84883af6
|
|||
|
4e1359aa95
|
|||
|
a3867ad65f
|
|||
|
689f972976
|
|||
|
3f33b62dfd
|
|||
|
ac5488eef6
|
|||
|
77a15130c7
|
|||
|
4c1e823908
|
|||
|
5f5a398a5b
|
|||
|
d5e4a2e6a7
|
|||
|
57c6b84b60
|
|||
|
4269627b4b
|
|||
|
d50e3c3d5b
|
|||
|
eae2890d98
|
|||
|
f8ebfd71a7
|
|||
|
dce1a05f6c
|
|||
|
eff265837c
|
|||
|
7d809eb15f
|
|||
|
9accc2f961
|
|||
|
e5a4094298
|
|||
|
410c4f8bb0
|
|||
|
2e23c6d367
|
|||
|
f5e9a0c04e
|
|||
|
3bd4ef616c
|
|||
|
41402fd578
|
|||
|
b47fa1a214
|
|||
|
9e363cb2c9
|
|||
|
1389c77022
|
|||
|
e231341e48
|
|||
|
70f977627d
|
|||
|
f3a6f7ddf9
|
|||
|
2a9aa3b400
|
|||
|
68a91523b9
|
|||
|
5647321622
|
|||
|
cdd31dd27b
|
|||
|
0615899e56
|
|||
|
0914569e62
|
|||
|
25d9edfc64
|
|||
|
af4c3bbff2
|
|||
|
54aae9d72a
|
|||
|
58646f8ea5
|
|||
|
9d7a27d8ac
|
|||
|
6546ddc64b
|
|||
|
cbf18b302d
|
|||
|
1acb5b0105
|
|||
|
40b33f9fc7
|
|||
|
443a7a30f6
|
|||
|
497e4a5642
|
|||
|
c0e3841ddb
|
|||
|
9ce2c325db
|
|||
|
9836030c59
|
|||
|
b482fd4abf
|
|||
|
2e502ede6c
|
|||
|
4bec0b890c
|
|||
|
7770ccf0aa
|
|||
|
656059278d
|
|||
|
1a9974ffdc
|
|||
|
1a2699b486
|
|||
|
1d3d621e2f
|
|||
|
47f4e287fc
|
|||
|
2e710328a4
|
|||
|
2e7b52d701
|
|||
|
d728607505
|
|||
|
ef414ab01a
|
|||
|
96abf266dd
|
|||
|
fcba32e9c4
|
|||
|
a7f5a5802d
|
|||
|
bb230378e0
|
|||
|
f638c73933
|
|||
|
98d915af3d
|
|||
|
c0593e8325
|
|||
|
608d8303ec
|
|||
|
1c6f30379e
|
|||
|
009a4e0d58
|
|||
|
e7c8656691
|
|||
|
d6be116ff8
|
|||
|
962b02cf25
|
|||
|
6fd6d971ed
|
|||
|
548c96c7ec
|
|||
|
6e8bfa6c4c
|
|||
|
a770d62b9b
|
|||
|
ff44060763
|
|||
|
3010a209b5
|
|||
|
e65a3b435c
|
|||
|
23515f67c8
|
|||
|
4389df60ae
|
|||
|
8092492018
|
|||
|
a7877844bf
|
|||
|
1ed027846d
|
|||
|
2f376d4813
|
|||
|
dc3810b530
|
|||
|
6e9e8c74f3
|
|||
|
4d60fa5632
|
|||
|
8807cbc730
|
|||
|
0e95573f18
|
|||
|
eb2b53307a
|
|||
|
682b3a2ce5
|
|||
|
594221eb78
|
|||
|
34822925e1
|
|||
|
37df040d85
|
|||
|
0360e779f3
|
|||
|
3e236333a7
|
|||
|
f24ae21af1
|
|||
|
99b324fb17
|
|||
|
6f50811dc9
|
|||
|
6b87bac401
|
|||
|
a967aa3b6e
|
|||
|
38bc2c7508
|
|||
|
30eb0d6a61
|
|||
|
c2ff9c9fa5
|
|||
|
d38d306147
|
|||
|
c32c06b2e8
|
|||
|
61199f734c
|
|||
|
87cf0d4e6b
|
|||
|
cf0dffa0f5
|
|||
|
686d7ec63a
|
|||
|
4c653b1151
|
|||
|
0b0a63d151
|
|||
|
6231cfe2aa
|
|||
|
712e80890b
|
|||
|
3fe7d48014
|
|||
|
16f9d39427
|
|||
|
c1cd5ba07b
|
|||
|
7b0cd2e472
|
|||
|
e580307528
|
|||
|
ee1dffb676
|
|||
|
f095fcf181
|
|||
|
ca8a130130
|
|||
|
0ad6b00e41
|
|||
|
ad0f1cf36b
|
|||
|
b12d924fa2
|
|||
|
c31d8ae41a
|
|||
|
6dbbf15c0e
|
|||
|
be7de68a42
|
|||
|
a759cf3666
|
|||
|
8c2dd3e984
|
|||
|
67038d5af4
|
|||
|
53d8d12e7f
|
|||
|
7997d79e56
|
|||
|
f2f1726190
|
|||
|
f63203cb0a
|
|||
|
19555c7670
|
|||
|
a3beab8959
|
|||
|
2ea786d6a9
|
|||
|
747d4ec4b0
|
|||
|
b76e6f6519
|
|||
|
840d8f68bf
|
|||
|
4bede7ecdd
|
|||
|
487a03b5a3
|
|||
|
8f3c22896a
|
|||
|
a167c1aba5
|
|||
|
a6008ef68b
|
|||
|
5228b27362
|
|||
|
f00d3a07ad
|
|||
|
f9538bc21b
|
|||
|
6ae5efec56
|
|||
|
14f4c59c8c
|
|||
|
688d43417b
|
|||
|
9f8fafa39b
|
|||
|
6643cfbeee
|
|||
|
dcde38f2e9
|
|||
|
deebbf6b1a
|
|||
|
0c557798bc
|
|||
|
327e6ed5a2
|
|||
|
76c7a423a9
|
|||
|
6e113b8836
|
|||
|
ce9f4b5f71
|
|||
|
8f727273ef
|
|||
|
d0a63b942e
|
|||
|
7f2126df32
|
|||
|
0cf0e18e35
|
|||
|
ee5c0dd135
|
|||
|
92c48d82e2
|
|||
|
c79a4fe7f8
|
|||
|
0aeb2bccfb
|
|||
|
50e079b99f
|
|||
|
fb2cb5005a
|
|||
|
6e73c28a92
|
|||
|
2c08aa3674
|
|||
|
1af73ae7b4
|
|||
|
c9aa5e04b1
|
|||
|
70a38bd3b0
|
|||
|
533b15da89
|
|||
|
a890e1d0e5
|
|||
|
e3520835bb
|
|||
|
0e56847754
|
|||
|
145d03b366
|
|||
|
2886228d40
|
|||
|
e1e499b79e
|
|||
|
65b7dd8b37
|
|||
|
8d72b9e5bd
|
|||
|
8a3c3d145a
|
|||
|
575ef307ad
|
|||
|
d4144fcf7f
|
|||
|
bad66facbc
|
|||
|
4aba014eac
|
|||
|
779ba994ce
|
|||
|
917be2de93
|
|||
|
9aad98d409
|
|||
|
b0d06b67dc
|
|||
|
089100f29d
|
|||
|
dfd26abf6c
|
|||
|
617ee21647
|
|||
|
15cdb37ec2
|
|||
|
1f0bdc7aca
|
|||
|
e3ffe85670
|
|||
|
f4403ba5cd
|
|||
|
5a26895a22
|
|||
|
09d9f766a9
|
|||
|
6558169666
|
|||
|
cccf970c57
|
|||
|
57ffb21690
|
|||
|
9c560b455a
|
|||
|
4c7c0fbfc6
|
|||
|
18b3b7904e
|
|||
|
fefefdf734
|
|||
|
b84bb09a80
|
|||
|
337bf20f50
|
|||
|
1cb792cf6e
|
|||
|
b2b40b07e8
|
|||
|
da11b26ec1
|
|||
|
024489e800
|
|||
|
0f795712b0
|
|||
|
7e2210ff71
|
|||
|
a71a008f3c
|
|||
|
162265b47e
|
|||
|
3fa7ac04e4
|
|||
|
bf2867d653
|
|||
|
ec0f0f6507
|
|||
|
a77a802955
|
|||
|
4407e14dfc
|
|||
|
e024d3184a
|
|||
|
8e1bf00c2d
|
|||
|
b111e22050
|
|||
|
1fa458c0be
|
|||
|
2c7ae67a67
|
|||
|
3826621b21
|
|||
|
041b505c2e
|
|||
|
e6debce649
|
|||
|
aa26b86fce
|
|||
|
a57a8fd5d8
|
|||
|
1d5d063d6a
|
|||
|
e61628a34e
|
|||
|
5a18f14929
|
|||
|
f12880688d
|
|||
|
bb5bbfe16a
|
|||
|
427e1ca37c
|
|||
|
96fdd9ecc5
|
|||
|
02771b655b
|
|||
|
d1c8d2c39b
|
|||
|
0efd742e8a
|
|||
|
ae1fe638d5
|
|||
|
445d95023b
|
|||
|
fc66f0bb47
|
|||
|
2cd6b35bee
|
|||
|
09a216c6ec
|
|||
|
44d17325c2
|
|||
|
544ce77cbc
|
|||
|
63c3c30b23
|
|||
|
d23c4ecc7c
|
|||
|
a46656dff8
|
|||
|
77db153ff5
|
|||
|
520d95bc07
|
|||
|
451df3f4e7
|
|||
|
011fac15ed
|
|||
|
347682ad0b
|
|||
|
1a2b979add
|
|||
|
b1c90cc380
|
|||
|
3a66b8143a
|
|||
|
64bbd3aabd
|
|||
|
08799a13d0
|
|||
|
1aef9c3bbb
|
|||
|
1f38303747
|
|||
|
640777b00c
|
|||
|
1d657193cf
|
|||
|
bab5406295
|
|||
|
725ae7d64d
|
|||
|
37a0c3967e
|
|||
|
ea0692548f
|
|||
|
48ea23e648
|
|||
|
40320e4920
|
|||
|
3ca0f61632
|
|||
|
6ffaac96e3
|
|||
|
13c7713d0c
|
|||
|
42389f7ec5
|
|||
|
30f130c691
|
|||
|
ceb4d26087
|
|||
|
852f3a9b3d
|
|||
|
5e02dbdb0d
|
|||
|
6a3248d472
|
|||
|
67404c98d9
|
|||
|
b9bf69cfce
|
|||
|
4648f98272
|
|||
|
11d99439ac
|
|||
|
39e4c5b8ac
|
|||
|
e8f6db38b6
|
|||
|
20d5b71575
|
|||
|
e903e7f542
|
|||
|
1caa051f4d
|
|||
|
dcdc6f7f6d
|
|||
|
5ad6f26b46
|
|||
|
7ba75a79f4
|
|||
|
9ef84d3904
|
|||
|
3b7b6e51fb
|
|||
|
b1b4debb82
|
|||
|
021cbbc2a8
|
|||
|
a4a54a4a4d
|
|||
|
04a344aac6
|
|||
|
6b98156a3d
|
|||
|
753432cf09
|
|||
|
f8902e3679
|
|||
|
8ee53a5164
|
|||
|
3981d44757
|
|||
|
9fd67e47b4
|
|||
|
4dcec40156
|
|||
|
9a274c78a3
|
|||
|
5647c3a91f
|
|||
|
992139c75d
|
|||
|
57c69b533e
|
|||
|
6f0c2a80f2
|
|||
|
08dfefb28d
|
|||
|
b081629662
|
|||
|
fba541f301
|
|||
|
5f0da3d5c2
|
|||
|
4d5841dd62
|
|||
|
9e752b588a
|
|||
|
27b1aaae38
|
|||
|
9e18de1dc2
|
|||
|
b80ea91a42
|
|||
|
30a9dfa4b8
|
|||
|
8d657b6fdf
|
|||
|
ae9b9adfd2
|
|||
|
dd6a480a21
|
|||
|
3942272c30
|
|||
|
9036986156
|
|||
|
a394971dd7
|
|||
|
9daba60809
|
|||
|
bcd79a22ff
|
|||
|
0ff7ab915b
|
|||
|
823575acac
|
|||
|
136bc0917b
|
|||
|
d6b082dd0b
|
|||
|
89d6d9576b
|
|||
|
fafce04a5d
|
|||
|
5d760a1db9
|
|||
|
d197e40b2a
|
|||
|
2008902247
|
|||
|
30ac985fd2
|
|||
|
e9fec368f8
|
|||
|
46add42f58
|
|||
|
377b61e342
|
|||
|
520c36db6d
|
|||
|
3352bb975b
|
|||
|
f7f48d57e9
|
|||
|
5c2345128e
|
|||
|
78f9676b1f
|
|||
|
5b5b676132
|
|||
|
78383fb6e8
|
|||
|
e97f6a393f
|
|||
|
eeffefd22b
|
|||
|
ac825640ab
|
|||
|
a7f7ce1795
|
|||
|
38c639e35c
|
|||
|
b2cb13e94c
|
|||
|
46f98d12d6
|
|||
|
503c7f953c
|
|||
|
15c9f6545d
|
|||
|
83b0e32c55
|
|||
|
eeaf26e7a2
|
|||
|
b587caf2e8
|
|||
|
f1c2ca4928
|
|||
|
0ca301219f
|
|||
|
e2199e1276
|
|||
|
86eacb3208
|
|||
|
8541bdd858
|
|||
|
46be0b0dc8
|
|||
|
cbe37e87e7
|
|||
|
66d741fb07
|
|||
|
0d449011f6
|
|||
|
46428ed85d
|
|||
|
081d6b463c
|
|||
|
11b3171180
|
|||
|
adbb84c3dd
|
|||
|
1084e31d95
|
|||
|
27a1b8fe0a
|
|||
|
b2141a41d7
|
|||
|
c0dff5bc87
|
|||
|
04513c0510
|
|||
|
28ebf973d6
|
|||
|
41aeb404ec
|
|||
|
0b1009786f
|
|||
|
b390640376
|
|||
|
ad2c9f36cd
|
|||
|
67db3fbb8d
|
|||
|
560cb626a1
|
|||
|
c33a6a5b7e
|
|||
|
952082bd9b
|
|||
|
24a9b24823
|
|||
|
c2e61e7987
|
|||
|
86787b3bc5
|
|||
|
cdfcfe6ce0
|
|||
|
68a2f0c240
|
|||
|
7319c7adf9
|
|||
|
e9c890cbb2
|
|||
|
6f924336fc
|
|||
|
bd88f10524
|
|||
|
e34e3b917e
|
|||
|
b0ba165107
|
|||
|
351d6c5a35
|
|||
|
f23f73701c
|
|||
|
876917229a
|
|||
|
0558032c2d
|
|||
|
c61cdc505f
|
|||
|
062edb3487
|
|||
|
e4355279a1
|
|||
|
289fdebead
|
|||
|
9c9e190db9
|
|||
|
d7d42c69a1
|
|||
|
c758e762bd
|
|||
|
10f8b1c221
|
|||
|
6907700d67
|
|||
|
0243f3ffbd
|
|||
|
cd0beeaf8e
|
|||
|
a69273ab2a
|
|||
|
4cd0f57e48
|
|||
|
33a0e6c01b
|
|||
|
d58f5c7590
|
|||
|
1da992e342
|
|||
|
9641805ec2
|
|||
|
0738f4889a
|
|||
|
7de3cfe221
|
|||
|
8b0648dd5d
|
|||
|
4667fac76c
|
|||
|
52e5443b0e
|
|||
|
130e470b60
|
|||
|
ba5ee8e3ee
|
|||
|
d1cef30877
|
|||
|
0188a3f0c7
|
|||
|
04fe3b24ce
|
|||
|
93ad551054
|
|||
|
3d54d1f176
|
|||
|
9feac7738f
|
|||
|
591a60bac9
|
|||
|
5093a06026
|
|||
|
50c1d7f880
|
|||
|
9e63633fbc
|
|||
|
61f981a34a
|
|||
|
d717c41bbe
|
|||
|
b896eec9b7
|
|||
|
8ab99e5e40
|
|||
|
2b6160ef7d
|
|||
|
4dcac7f133
|
|||
|
966fd4df9e
|
|||
|
a2cf59b989
|
|||
|
e87f59c4e4
|
|||
|
3b221c3e77
|
|||
|
ff3b385b12
|
|||
|
c6920e6ab7
|
|||
|
59b25d45fe
|
|||
|
9b99650eb1
|
|||
|
15bff9e1a6
|
|||
|
b948525c07
|
|||
|
9acbd16e9a
|
|||
|
64e5a1068b
|
|||
|
b6cbd49d8c
|
|||
|
6913b9224a
|
|||
|
9584958ecc
|
+8
-27
@@ -1,37 +1,18 @@
|
|||||||
# Binaries for programs and plugins
|
# produced by tools and text editors
|
||||||
*.exe
|
*.qcow2
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
*.pkg
|
|
||||||
/hakurei
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
|
||||||
# vendor/
|
|
||||||
|
|
||||||
# Go workspace file
|
|
||||||
go.work
|
|
||||||
go.work.sum
|
|
||||||
|
|
||||||
# env file
|
|
||||||
.env
|
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# go generate
|
# go generate
|
||||||
/cmd/hakurei/LICENSE
|
/cmd/hakurei/LICENSE
|
||||||
/internal/pkg/testdata/testtool
|
/cmd/mbf/internal/pkgserver/ui/static
|
||||||
|
/internal/pkg/internal/testtool/testtool
|
||||||
/internal/rosa/hakurei_current.tar.gz
|
/internal/rosa/hakurei_current.tar.gz
|
||||||
|
|
||||||
# release
|
# cmd/dist default destination
|
||||||
/dist/hakurei-*
|
/dist
|
||||||
|
|
||||||
# interactive nixos vm
|
# local packages
|
||||||
nixos.qcow2
|
/internal/rosa/package/local
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
HAKUREI_DIST_MAKE='' exec "$(dirname -- "$0")/cmd/dist/dist.sh"
|
||||||
+30
-25
@@ -2,7 +2,7 @@
|
|||||||
package check
|
package check
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -20,8 +20,8 @@ func (e AbsoluteError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e AbsoluteError) Is(target error) bool {
|
func (e AbsoluteError) Is(target error) bool {
|
||||||
var ce AbsoluteError
|
ce, ok := errors.AsType[AbsoluteError](target)
|
||||||
if !errors.As(target, &ce) {
|
if !ok {
|
||||||
return errors.Is(target, syscall.EINVAL)
|
return errors.Is(target, syscall.EINVAL)
|
||||||
}
|
}
|
||||||
return e == ce
|
return e == ce
|
||||||
@@ -30,6 +30,22 @@ func (e AbsoluteError) Is(target error) bool {
|
|||||||
// Absolute holds a pathname checked to be absolute.
|
// Absolute holds a pathname checked to be absolute.
|
||||||
type Absolute struct{ pathname unique.Handle[string] }
|
type Absolute struct{ pathname unique.Handle[string] }
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ fmt.GoStringer = new(Absolute)
|
||||||
|
|
||||||
|
_ encoding.TextAppender = new(Absolute)
|
||||||
|
_ encoding.TextMarshaler = new(Absolute)
|
||||||
|
_ encoding.TextUnmarshaler = new(Absolute)
|
||||||
|
|
||||||
|
_ encoding.BinaryAppender = new(Absolute)
|
||||||
|
_ encoding.BinaryMarshaler = new(Absolute)
|
||||||
|
_ encoding.BinaryUnmarshaler = new(Absolute)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *Absolute) GoString() string {
|
||||||
|
return fmt.Sprintf("check.MustAbs(%q)", a.String())
|
||||||
|
}
|
||||||
|
|
||||||
// ok returns whether [Absolute] is not the zero value.
|
// ok returns whether [Absolute] is not the zero value.
|
||||||
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
|
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
|
||||||
|
|
||||||
@@ -84,13 +100,16 @@ func (a *Absolute) Append(elem ...string) *Absolute {
|
|||||||
// Dir calls [filepath.Dir] with [Absolute] as its argument.
|
// Dir calls [filepath.Dir] with [Absolute] as its argument.
|
||||||
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
|
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
|
||||||
|
|
||||||
// GobEncode returns the checked pathname.
|
// AppendText appends the checked pathname.
|
||||||
func (a *Absolute) GobEncode() ([]byte, error) {
|
func (a *Absolute) AppendText(data []byte) ([]byte, error) {
|
||||||
return []byte(a.String()), nil
|
return append(data, a.String()...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GobDecode stores data if it represents an absolute pathname.
|
// MarshalText returns the checked pathname.
|
||||||
func (a *Absolute) GobDecode(data []byte) error {
|
func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) }
|
||||||
|
|
||||||
|
// UnmarshalText stores data if it represents an absolute pathname.
|
||||||
|
func (a *Absolute) UnmarshalText(data []byte) error {
|
||||||
pathname := string(data)
|
pathname := string(data)
|
||||||
if !filepath.IsAbs(pathname) {
|
if !filepath.IsAbs(pathname) {
|
||||||
return AbsoluteError(pathname)
|
return AbsoluteError(pathname)
|
||||||
@@ -99,23 +118,9 @@ func (a *Absolute) GobDecode(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON returns a JSON representation of the checked pathname.
|
func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) }
|
||||||
func (a *Absolute) MarshalJSON() ([]byte, error) {
|
func (a *Absolute) MarshalBinary() ([]byte, error) { return a.MarshalText() }
|
||||||
return json.Marshal(a.String())
|
func (a *Absolute) UnmarshalBinary(data []byte) error { return a.UnmarshalText(data) }
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON stores data if it represents an absolute pathname.
|
|
||||||
func (a *Absolute) UnmarshalJSON(data []byte) error {
|
|
||||||
var pathname string
|
|
||||||
if err := json.Unmarshal(data, &pathname); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(pathname) {
|
|
||||||
return AbsoluteError(pathname)
|
|
||||||
}
|
|
||||||
a.pathname = unique.Make(pathname)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||||
func SortAbs(x []*Absolute) {
|
func SortAbs(x []*Absolute) {
|
||||||
|
|||||||
+6
-15
@@ -170,20 +170,20 @@ func TestCodecAbsolute(t *testing.T) {
|
|||||||
|
|
||||||
{"good", MustAbs("/etc"),
|
{"good", MustAbs("/etc"),
|
||||||
nil,
|
nil,
|
||||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
||||||
|
|
||||||
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
||||||
{"not absolute", nil,
|
{"not absolute", nil,
|
||||||
AbsoluteError("etc"),
|
AbsoluteError("etc"),
|
||||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||||
|
|
||||||
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
||||||
{"zero", nil,
|
{"zero", nil,
|
||||||
new(AbsoluteError),
|
new(AbsoluteError),
|
||||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||||
`""`, `{"val":"","magic":3236757504}`},
|
`""`, `{"val":"","magic":3236757504}`},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,15 +347,6 @@ func TestCodecAbsolute(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("json passthrough", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
wantErr := "invalid character ':' looking for beginning of value"
|
|
||||||
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
|
|
||||||
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAbsoluteWrap(t *testing.T) {
|
func TestAbsoluteWrap(t *testing.T) {
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ import "strings"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// SpecialOverlayEscape is the escape string for overlay mount options.
|
// SpecialOverlayEscape is the escape string for overlay mount options.
|
||||||
|
//
|
||||||
|
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||||
SpecialOverlayEscape = `\`
|
SpecialOverlayEscape = `\`
|
||||||
// SpecialOverlayOption is the separator string between overlay mount options.
|
// SpecialOverlayOption is the separator string between overlay mount options.
|
||||||
|
//
|
||||||
|
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||||
SpecialOverlayOption = ","
|
SpecialOverlayOption = ","
|
||||||
// SpecialOverlayPath is the separator string between overlay paths.
|
// SpecialOverlayPath is the separator string between overlay paths.
|
||||||
|
//
|
||||||
|
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||||
SpecialOverlayPath = ":"
|
SpecialOverlayPath = ":"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EscapeOverlayDataSegment escapes a string for formatting into the data
|
// EscapeOverlayDataSegment escapes a string for formatting into the data
|
||||||
// argument of an overlay mount system call.
|
// argument of an overlay mount system call.
|
||||||
|
//
|
||||||
|
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||||
func EscapeOverlayDataSegment(s string) string {
|
func EscapeOverlayDataSegment(s string) string {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
+323
@@ -0,0 +1,323 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/ext"
|
||||||
|
"hakurei.app/fhs"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parsePair parses a NUL-delimited quoted paths pair.
|
||||||
|
func parsePair(s string) (source, target *check.Absolute, err error) {
|
||||||
|
var p string
|
||||||
|
if p, err = strconv.Unquote(s); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_source, _target, ok := strings.Cut(p, "\x00")
|
||||||
|
if source, err = check.NewAbs(_source); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target, err = check.NewAbs(_target)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse decodes a high-level configuration stream and returns its
|
||||||
|
// corresponding [hst.Config].
|
||||||
|
func parse(
|
||||||
|
id string,
|
||||||
|
base *check.Absolute,
|
||||||
|
r io.Reader,
|
||||||
|
templateP *string,
|
||||||
|
) (*hst.Config, error) {
|
||||||
|
shell := fhs.AbsRoot.Append("bin", "zsh")
|
||||||
|
home := hst.AbsPrivateTmp.Append("home")
|
||||||
|
|
||||||
|
root := hst.FSOverlay{
|
||||||
|
Target: fhs.AbsRoot,
|
||||||
|
Lower: []*check.Absolute{base.Append("initial")},
|
||||||
|
}
|
||||||
|
c := hst.Config{
|
||||||
|
ID: id,
|
||||||
|
Enablements: new(hst.Enablements),
|
||||||
|
|
||||||
|
SessionBus: &hst.BusConfig{
|
||||||
|
Own: []string{
|
||||||
|
id + ".*",
|
||||||
|
"org.mpris.MediaPlayer2." + id + ".*",
|
||||||
|
},
|
||||||
|
Filter: true,
|
||||||
|
},
|
||||||
|
SystemBus: &hst.BusConfig{Filter: true},
|
||||||
|
|
||||||
|
Container: &hst.ContainerConfig{
|
||||||
|
Env: make(map[string]string),
|
||||||
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
|
{FilesystemConfig: &root},
|
||||||
|
{FilesystemConfig: &hst.FSBind{
|
||||||
|
Target: home,
|
||||||
|
Source: base.Append("state", id),
|
||||||
|
Write: true,
|
||||||
|
Ensure: true,
|
||||||
|
}},
|
||||||
|
|
||||||
|
{FilesystemConfig: &hst.FSEphemeral{
|
||||||
|
Target: fhs.AbsVar.Append("tmp"),
|
||||||
|
Write: true,
|
||||||
|
Perm: 01777,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
|
||||||
|
Username: "chronos",
|
||||||
|
Shell: shell,
|
||||||
|
Home: home,
|
||||||
|
Path: shell,
|
||||||
|
Args: []string{"zsh", "-c"},
|
||||||
|
|
||||||
|
Flags: hst.FCoverRun,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := bufio.NewScanner(r)
|
||||||
|
scanOnce := func() error {
|
||||||
|
if s.Scan() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanOnce(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if template, identity, ok := strings.Cut(s.Text(), ":"); !ok {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
} else if v, err := strconv.Atoi(identity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
if templateP != nil {
|
||||||
|
*templateP = template
|
||||||
|
}
|
||||||
|
c.Identity = v
|
||||||
|
root.Upper = base.Append("template", template)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanOnce(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Container.Args = append(c.Container.Args, s.Text(), "")
|
||||||
|
|
||||||
|
var flagInteractive, flagGPU, flagSystemBus bool
|
||||||
|
flags := map[string]*bool{
|
||||||
|
"interactive": &flagInteractive,
|
||||||
|
"gpu": &flagGPU,
|
||||||
|
"system_bus": &flagSystemBus,
|
||||||
|
}
|
||||||
|
|
||||||
|
for s.Scan() {
|
||||||
|
key, value, ok := strings.Cut(s.Text(), " ")
|
||||||
|
if key != "" && key[0] == ';' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var p *bool
|
||||||
|
if p, ok = flags[key]; ok {
|
||||||
|
*p = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "wayland":
|
||||||
|
*c.Enablements |= hst.EWayland
|
||||||
|
case "x11":
|
||||||
|
*c.Enablements |= hst.EX11
|
||||||
|
case "dbus":
|
||||||
|
*c.Enablements |= hst.EDBus
|
||||||
|
case "pipewire":
|
||||||
|
*c.Enablements |= hst.EPipeWire
|
||||||
|
|
||||||
|
case "multiarch":
|
||||||
|
c.Container.Flags |= hst.FMultiarch
|
||||||
|
case "devel":
|
||||||
|
c.Container.Flags |= hst.FDevel
|
||||||
|
case "userns":
|
||||||
|
c.Container.Flags |= hst.FUserns
|
||||||
|
case "net":
|
||||||
|
c.Container.Flags |= hst.FHostNet
|
||||||
|
case "abstract":
|
||||||
|
c.Container.Flags |= hst.FHostAbstract
|
||||||
|
case "tty":
|
||||||
|
c.Container.Flags |= hst.FTty
|
||||||
|
case "mapuid":
|
||||||
|
c.Container.Flags |= hst.FMapRealUID
|
||||||
|
case "device":
|
||||||
|
c.Container.Flags |= hst.FDevice
|
||||||
|
|
||||||
|
case "share_runtime":
|
||||||
|
c.Container.Flags |= hst.FShareRuntime
|
||||||
|
case "share_tmpdir":
|
||||||
|
c.Container.Flags |= hst.FShareTmpdir
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid flag %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "username":
|
||||||
|
c.Container.Username = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "hostname":
|
||||||
|
c.Container.Hostname = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "group":
|
||||||
|
c.Groups = append(c.Groups, value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "sched_policy":
|
||||||
|
if err := c.SchedPolicy.UnmarshalText([]byte(value)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "sched_priority":
|
||||||
|
v, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.SchedPriority = ext.Int(v)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "env":
|
||||||
|
if key, value, ok = strings.Cut(value, "="); !ok {
|
||||||
|
return nil, fmt.Errorf("invalid environment %q", key)
|
||||||
|
}
|
||||||
|
c.Container.Env[key] = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "ro":
|
||||||
|
source, target, err := parsePair(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Container.Filesystem = append(c.Container.Filesystem,
|
||||||
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||||
|
Target: target,
|
||||||
|
Source: source,
|
||||||
|
}},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "ro+":
|
||||||
|
source, target, err := parsePair(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Container.Filesystem = append(c.Container.Filesystem,
|
||||||
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||||
|
Target: target,
|
||||||
|
Source: source,
|
||||||
|
Optional: true,
|
||||||
|
}},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "rw":
|
||||||
|
source, target, err := parsePair(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Container.Filesystem = append(c.Container.Filesystem,
|
||||||
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||||
|
Target: target,
|
||||||
|
Source: source,
|
||||||
|
Write: true,
|
||||||
|
}},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "dev":
|
||||||
|
source, target, err := parsePair(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Container.Filesystem = append(c.Container.Filesystem,
|
||||||
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||||
|
Target: target,
|
||||||
|
Source: source,
|
||||||
|
Device: true,
|
||||||
|
}},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "own":
|
||||||
|
c.SessionBus.Own = append(c.SessionBus.Own, value)
|
||||||
|
continue
|
||||||
|
case "own_system":
|
||||||
|
c.SystemBus.Own = append(c.SystemBus.Own, value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "talk":
|
||||||
|
c.SessionBus.Talk = append(c.SessionBus.Talk, value)
|
||||||
|
continue
|
||||||
|
case "talk_system":
|
||||||
|
c.SystemBus.Talk = append(c.SystemBus.Talk, value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid key %q", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagInteractive {
|
||||||
|
c.Container.Args[1] += "i"
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagGPU {
|
||||||
|
c.Container.Filesystem = append(c.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||||
|
{FilesystemConfig: &hst.FSBind{
|
||||||
|
Source: fhs.AbsDev.Append("dri"),
|
||||||
|
Device: true,
|
||||||
|
Optional: true,
|
||||||
|
}},
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !flagSystemBus {
|
||||||
|
c.SystemBus = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Container.Flags&hst.FShareTmpdir == 0 {
|
||||||
|
c.Container.Filesystem = append(c.Container.Filesystem,
|
||||||
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{
|
||||||
|
Target: fhs.AbsTmp,
|
||||||
|
Write: true,
|
||||||
|
Perm: 01777,
|
||||||
|
}},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/fhs"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
base := fhs.AbsProc.Append("nonexistent")
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
want *hst.Config
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{"com.discordapp.Discord", `nonfree:8
|
||||||
|
exec Discord --ozone-platform-hint=wayland
|
||||||
|
|
||||||
|
gpu
|
||||||
|
wayland
|
||||||
|
dbus
|
||||||
|
system_bus
|
||||||
|
pipewire
|
||||||
|
userns
|
||||||
|
net
|
||||||
|
mapuid
|
||||||
|
|
||||||
|
share_runtime
|
||||||
|
share_tmpdir
|
||||||
|
|
||||||
|
group media_rw
|
||||||
|
env ELECTRON_TRASH=gio
|
||||||
|
rw "/sdcard"
|
||||||
|
; remove before reusing
|
||||||
|
ro "/bin\x00/.hakurei/bin"
|
||||||
|
|
||||||
|
talk org.kde.StatusNotifierWatcher
|
||||||
|
talk com.canonical.AppMenu.Registrar
|
||||||
|
talk com.canonical.indicator.application
|
||||||
|
talk com.canonical.Unity
|
||||||
|
`, &hst.Config{
|
||||||
|
Identity: 8,
|
||||||
|
ID: "com.discordapp.Discord",
|
||||||
|
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire),
|
||||||
|
Groups: []string{"media_rw"},
|
||||||
|
|
||||||
|
SessionBus: &hst.BusConfig{
|
||||||
|
Talk: []string{
|
||||||
|
"org.kde.StatusNotifierWatcher",
|
||||||
|
"com.canonical.AppMenu.Registrar",
|
||||||
|
"com.canonical.indicator.application",
|
||||||
|
"com.canonical.Unity",
|
||||||
|
},
|
||||||
|
Own: []string{
|
||||||
|
"com.discordapp.Discord.*",
|
||||||
|
"org.mpris.MediaPlayer2.com.discordapp.Discord.*",
|
||||||
|
},
|
||||||
|
Filter: true,
|
||||||
|
},
|
||||||
|
SystemBus: &hst.BusConfig{Filter: true},
|
||||||
|
|
||||||
|
Container: &hst.ContainerConfig{
|
||||||
|
Env: map[string]string{
|
||||||
|
"ELECTRON_TRASH": "gio",
|
||||||
|
},
|
||||||
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
|
{FilesystemConfig: &hst.FSOverlay{
|
||||||
|
Target: fhs.AbsRoot,
|
||||||
|
Lower: []*check.Absolute{
|
||||||
|
base.Append("initial"),
|
||||||
|
},
|
||||||
|
Upper: base.Append("template", "nonfree"),
|
||||||
|
}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{
|
||||||
|
Target: hst.AbsPrivateTmp.Append("home"),
|
||||||
|
Source: base.Append("state", "com.discordapp.Discord"),
|
||||||
|
Write: true,
|
||||||
|
Ensure: true,
|
||||||
|
}},
|
||||||
|
|
||||||
|
{FilesystemConfig: &hst.FSEphemeral{
|
||||||
|
Target: fhs.AbsVar.Append("tmp"),
|
||||||
|
Write: true,
|
||||||
|
Perm: 01777,
|
||||||
|
}},
|
||||||
|
|
||||||
|
{FilesystemConfig: &hst.FSBind{
|
||||||
|
Source: check.MustAbs("/sdcard"),
|
||||||
|
Write: true,
|
||||||
|
}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{
|
||||||
|
Target: check.MustAbs("/.hakurei/bin"),
|
||||||
|
Source: check.MustAbs("/bin"),
|
||||||
|
}},
|
||||||
|
|
||||||
|
{FilesystemConfig: &hst.FSBind{
|
||||||
|
Source: fhs.AbsDev.Append("dri"),
|
||||||
|
Device: true,
|
||||||
|
Optional: true,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
|
||||||
|
Username: "chronos",
|
||||||
|
Shell: fhs.AbsRoot.Append("bin", "zsh"),
|
||||||
|
Home: hst.AbsPrivateTmp.Append("home"),
|
||||||
|
Path: fhs.AbsRoot.Append("bin", "zsh"),
|
||||||
|
Args: []string{
|
||||||
|
"zsh", "-c",
|
||||||
|
"exec Discord --ozone-platform-hint=wayland",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
|
||||||
|
Flags: hst.FCoverRun | hst.FUserns | hst.FHostNet | hst.FMapRealUID |
|
||||||
|
hst.FShareRuntime | hst.FShareTmpdir,
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got, err := parse(
|
||||||
|
tc.name,
|
||||||
|
base,
|
||||||
|
strings.NewReader(tc.data),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(err, tc.err) {
|
||||||
|
t.Errorf("parse: error = %v, want %v", err, tc.err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(got, tc.want) {
|
||||||
|
t.Errorf("parse: %#v, want %#v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/fhs"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/env"
|
||||||
|
"hakurei.app/internal/lockedfile"
|
||||||
|
"hakurei.app/internal/outcome"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MutationConflictError describes an active mutable instance.
|
||||||
|
type MutationConflictError string
|
||||||
|
|
||||||
|
func (e MutationConflictError) Error() string {
|
||||||
|
return "mutable instance active at " + string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// informTemplate guards intention of a template or its derivatives.
|
||||||
|
func informTemplate(base *check.Absolute, name string, mutable bool) (func() error, error) {
|
||||||
|
mu := lockedfile.MutexAt(base.Append("lock", name).String())
|
||||||
|
if unlock, err := mu.Lock(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
marker := base.Append("lock", "."+name)
|
||||||
|
if p, err := os.ReadFile(marker.String()); err == nil {
|
||||||
|
if _, err = os.Stat(fhs.AbsProc.Append(string(p)).String()); err == nil {
|
||||||
|
return nil, MutationConflictError(p)
|
||||||
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Printf("removing stale marker by %s", string(p))
|
||||||
|
if err = os.Remove(marker.String()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mutable {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var active []hst.ID
|
||||||
|
var sc hst.Paths
|
||||||
|
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
|
||||||
|
entries, copyError := outcome.NewStore(&sc).All()
|
||||||
|
var s hst.State
|
||||||
|
for eh := range entries {
|
||||||
|
s = hst.State{}
|
||||||
|
if _, err := eh.Load(&s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Validate(0) != nil || len(s.Container.Filesystem) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
root, ok := s.Container.Filesystem[0].FilesystemConfig.(*hst.FSOverlay)
|
||||||
|
if !ok || root == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !root.Target.Is(fhs.AbsRoot) ||
|
||||||
|
len(root.Lower) != 1 ||
|
||||||
|
!root.Lower[0].Is(base.Append("initial")) ||
|
||||||
|
!root.Upper.Is(base.Append("template", name)) ||
|
||||||
|
root.Work != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
active = append(active, s.ID)
|
||||||
|
}
|
||||||
|
if err := copyError(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(active) != 0 {
|
||||||
|
var buf strings.Builder
|
||||||
|
buf.WriteString("derivative instances still active:")
|
||||||
|
for _, id := range active {
|
||||||
|
buf.WriteString("\n\t")
|
||||||
|
buf.WriteString(id.String())
|
||||||
|
}
|
||||||
|
return nil, errors.New(buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() error { return os.RemoveAll(marker.String()) }, os.WriteFile(
|
||||||
|
marker.String(),
|
||||||
|
[]byte(strconv.Itoa(os.Getpid())),
|
||||||
|
0400,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// acquireTemplate obtains exclusivity of a template.
|
||||||
|
func acquireTemplate(base *check.Absolute, name string) (remove func() error, err error) {
|
||||||
|
return informTemplate(base, name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enterTemplate checks against exclusivity of a template.
|
||||||
|
func enterTemplate(base *check.Absolute, name string) error {
|
||||||
|
_, err := informTemplate(base, name, false)
|
||||||
|
return err
|
||||||
|
}
|
||||||
+242
@@ -0,0 +1,242 @@
|
|||||||
|
// The app program is a proof-of-concept frontend for cmd/hakurei.
|
||||||
|
//
|
||||||
|
// This program is not covered by the compatibility promise. The command line
|
||||||
|
// interface and configuration syntax may change at any time.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/command"
|
||||||
|
"hakurei.app/fhs"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix("app: ")
|
||||||
|
msg := message.New(log.Default())
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(),
|
||||||
|
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagVerbose bool
|
||||||
|
flagBase string
|
||||||
|
|
||||||
|
base, template, initial *check.Absolute
|
||||||
|
)
|
||||||
|
c := command.New(os.Stderr, log.Printf, "app", func([]string) (err error) {
|
||||||
|
msg.SwapVerbose(flagVerbose)
|
||||||
|
flagBase = os.ExpandEnv(flagBase)
|
||||||
|
if flagBase == "" {
|
||||||
|
flagBase = "state"
|
||||||
|
}
|
||||||
|
if flagBase, err = filepath.Abs(flagBase); err != nil {
|
||||||
|
return
|
||||||
|
} else if base, err = check.NewAbs(flagBase); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
template = base.Append("template")
|
||||||
|
initial = base.Append("initial")
|
||||||
|
return
|
||||||
|
}).Flag(
|
||||||
|
&flagVerbose,
|
||||||
|
"v", command.BoolFlag(false),
|
||||||
|
"Increase log verbosity",
|
||||||
|
).Flag(
|
||||||
|
&flagBase,
|
||||||
|
"d", command.StringFlag("$ROSA_APP_PATH"),
|
||||||
|
"Configuration and state directory",
|
||||||
|
)
|
||||||
|
|
||||||
|
{
|
||||||
|
var (
|
||||||
|
flagShell string
|
||||||
|
flagHome string
|
||||||
|
)
|
||||||
|
c.NewCommand(
|
||||||
|
"enter", "Enter mutable state template",
|
||||||
|
func(args []string) error {
|
||||||
|
if len(args) != 1 {
|
||||||
|
dents, err := os.ReadDir(template.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, dent := range dents {
|
||||||
|
if !dent.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println(dent.Name())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config := hst.Config{
|
||||||
|
ID: "app.hakurei.mutable." + args[0],
|
||||||
|
Container: &hst.ContainerConfig{
|
||||||
|
Hostname: args[0] + "-mutable",
|
||||||
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
|
{FilesystemConfig: &hst.FSOverlay{
|
||||||
|
Target: fhs.AbsRoot,
|
||||||
|
Lower: []*check.Absolute{initial},
|
||||||
|
Upper: template.Append(args[0]),
|
||||||
|
Work: base.Append("work", args[0]),
|
||||||
|
}},
|
||||||
|
{FilesystemConfig: &hst.FSEphemeral{
|
||||||
|
Target: fhs.AbsTmp,
|
||||||
|
Write: true,
|
||||||
|
Perm: 0755,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Username: "chronos",
|
||||||
|
Flags: hst.FNoPlace |
|
||||||
|
hst.FMultiarch |
|
||||||
|
hst.FDevel |
|
||||||
|
hst.FUserns |
|
||||||
|
hst.FHostNet |
|
||||||
|
hst.FTty,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, err := check.NewAbs(flagShell); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
config.Container.Shell = a
|
||||||
|
config.Container.Path = a
|
||||||
|
config.Container.Args = []string{
|
||||||
|
"-" + filepath.Base(flagShell),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, err := check.NewAbs(flagHome); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
config.Container.Home = a
|
||||||
|
}
|
||||||
|
|
||||||
|
remove, err := acquireTemplate(base, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = run(ctx, msg, &config)
|
||||||
|
return errors.Join(err, remove())
|
||||||
|
},
|
||||||
|
).Flag(
|
||||||
|
&flagShell,
|
||||||
|
"shell", command.StringFlag("/bin/zsh"),
|
||||||
|
"Shell program within container",
|
||||||
|
).Flag(
|
||||||
|
&flagHome,
|
||||||
|
"home", command.StringFlag("/home/chronos"),
|
||||||
|
"Home directory within container",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var (
|
||||||
|
flagCommand string
|
||||||
|
)
|
||||||
|
c.NewCommand(
|
||||||
|
"run", "Start the named application",
|
||||||
|
func(args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
dents, err := os.ReadDir(base.Append("app").String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, dent := range dents {
|
||||||
|
if dent.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println(dent.Name())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var config *hst.Config
|
||||||
|
var r io.Reader
|
||||||
|
f, err := os.Open(base.Append("app", args[0]).String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r = f
|
||||||
|
|
||||||
|
var common *os.File
|
||||||
|
if common, err = os.Open(base.Append("common").String()); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
_ = f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r = io.MultiReader(f, common)
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
config, err = parse(args[0], base, r, &name)
|
||||||
|
if closeErr := f.Close(); err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
if common != nil {
|
||||||
|
if closeErr := common.Close(); err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagCommand != "" {
|
||||||
|
config.Container.Args[2] = flagCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = enterTemplate(base, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return run(ctx, msg, config, args[1:]...)
|
||||||
|
},
|
||||||
|
).
|
||||||
|
Flag(
|
||||||
|
&flagCommand,
|
||||||
|
"command", command.StringFlag(""),
|
||||||
|
"Override configured command",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.MustParse(os.Args[1:], func(err error) {
|
||||||
|
if e, ok := errors.AsType[*exec.ExitError](err); ok && e != nil {
|
||||||
|
os.Exit(e.ExitCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
if w, ok := err.(interface{ Unwrap() []error }); !ok {
|
||||||
|
var m string
|
||||||
|
m, ok = message.GetMessage(err)
|
||||||
|
if !ok {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Fatal(m)
|
||||||
|
} else {
|
||||||
|
errs := w.Unwrap()
|
||||||
|
for i, e := range errs {
|
||||||
|
if i == len(errs)-1 {
|
||||||
|
log.Fatal(e)
|
||||||
|
}
|
||||||
|
log.Println(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
// run starts a container via cmd/hakurei and returns after it terminates.
|
||||||
|
func run(
|
||||||
|
ctx context.Context,
|
||||||
|
msg message.Msg,
|
||||||
|
config *hst.Config,
|
||||||
|
args ...string,
|
||||||
|
) error {
|
||||||
|
c, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(c, "hakurei")
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
cmd.Cancel = func() error {
|
||||||
|
return cmd.Process.Signal(syscall.SIGINT)
|
||||||
|
}
|
||||||
|
if msg.IsVerbose() {
|
||||||
|
cmd.Args = append(cmd.Args, "-v")
|
||||||
|
}
|
||||||
|
cmd.Args = append(cmd.Args, "run", "3")
|
||||||
|
cmd.Args = append(cmd.Args, args...)
|
||||||
|
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
|
||||||
|
|
||||||
|
if err = cmd.Start(); err != nil {
|
||||||
|
_, _ = r.Close(), w.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.Close(); err != nil {
|
||||||
|
_ = w.Close()
|
||||||
|
return err
|
||||||
|
} else if err = json.NewEncoder(w).Encode(&config); err != nil {
|
||||||
|
_ = w.Close()
|
||||||
|
return err
|
||||||
|
} else if err = w.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
v0.4.5
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
TOOLCHAIN_VERSION="$(go version)"
|
||||||
|
cd "$(dirname -- "$0")/../.."
|
||||||
|
echo "Building cmd/dist using ${TOOLCHAIN_VERSION}."
|
||||||
|
FLAGS=''
|
||||||
|
if test -n "$VERBOSE"; then
|
||||||
|
FLAGS="$FLAGS -v"
|
||||||
|
fi
|
||||||
|
go run $FLAGS --tags=dist ./cmd/dist
|
||||||
Vendored
+269
@@ -0,0 +1,269 @@
|
|||||||
|
//go:build dist
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"crypto/sha512"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate sh -c "git describe --tags > VERSION"
|
||||||
|
//go:embed VERSION
|
||||||
|
var version string
|
||||||
|
|
||||||
|
// getenv looks up an environment variable, and returns fallback if it is unset.
|
||||||
|
func getenv(key, fallback string) string {
|
||||||
|
if v, ok := os.LookupEnv(key); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustRun runs a command with the current process's environment and panics
|
||||||
|
// on error or non-zero exit code.
|
||||||
|
func mustRun(ctx context.Context, env []string, name string, arg ...string) {
|
||||||
|
cmd := exec.CommandContext(ctx, name, arg...)
|
||||||
|
if env != nil {
|
||||||
|
cmd.Env = append(cmd.Environ(), env...)
|
||||||
|
}
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed comp/_hakurei
|
||||||
|
var comp []byte
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix("")
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
|
verbose := os.Getenv("VERBOSE") != ""
|
||||||
|
runTests := os.Getenv("HAKUREI_DIST_MAKE") == ""
|
||||||
|
version = getenv("HAKUREI_VERSION", strings.TrimSpace(version))
|
||||||
|
prefix := getenv("PREFIX", "/usr")
|
||||||
|
destdir := getenv("DESTDIR", "dist")
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
log.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(destdir, 0755); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
s, err := os.MkdirTemp(destdir, ".dist.*")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
var code int
|
||||||
|
|
||||||
|
if err = os.RemoveAll(s); err != nil {
|
||||||
|
code = 1
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
code = 1
|
||||||
|
log.Println(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(code)
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
verboseFlag := "-v"
|
||||||
|
if !verbose {
|
||||||
|
verboseFlag = "-buildvcs=false"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Building hakurei %s for %s/%s.", version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
mustRun(ctx, nil, "go", "generate", "./...")
|
||||||
|
mustRun(
|
||||||
|
ctx, nil, "go", "build",
|
||||||
|
"-trimpath",
|
||||||
|
verboseFlag, "-o", s,
|
||||||
|
"-ldflags=-s -w "+
|
||||||
|
"-buildid= -linkmode external -extldflags=-static "+
|
||||||
|
"-X hakurei.app/internal/info.buildVersion="+version+" "+
|
||||||
|
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
|
||||||
|
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu",
|
||||||
|
"./cmd/hakurei",
|
||||||
|
"./cmd/sharefs",
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Printf("Building cmd/hsu for %s/%s.", runtime.GOOS, runtime.GOARCH)
|
||||||
|
mustRun(
|
||||||
|
ctx, []string{"CGO_ENABLED=0"}, "go", "build",
|
||||||
|
"-trimpath",
|
||||||
|
verboseFlag, "-o", s,
|
||||||
|
"-ldflags=-s -w "+
|
||||||
|
"-buildid= "+
|
||||||
|
"-X main.hakureiPath="+prefix+"/bin/hakurei",
|
||||||
|
"./cmd/hsu",
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Println()
|
||||||
|
if runTests {
|
||||||
|
log.Println("##### Testing Hakurei.")
|
||||||
|
mustRun(
|
||||||
|
ctx, nil, "go", "test",
|
||||||
|
"-ldflags=-buildid= -linkmode external -extldflags=-static",
|
||||||
|
"./...",
|
||||||
|
)
|
||||||
|
log.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("##### Creating distribution.")
|
||||||
|
const suffix = ".tar.gz"
|
||||||
|
distName := "hakurei-" + version + "-" + runtime.GOARCH
|
||||||
|
var f *os.File
|
||||||
|
if f, err = os.OpenFile(
|
||||||
|
filepath.Join(s, distName+suffix),
|
||||||
|
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
|
||||||
|
0644,
|
||||||
|
); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = f.Close(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
h := sha512.New()
|
||||||
|
gw, _ := gzip.NewWriterLevel(io.MultiWriter(f, h), gzip.BestCompression)
|
||||||
|
tw := tar.NewWriter(gw)
|
||||||
|
|
||||||
|
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
|
||||||
|
header := tar.Header{
|
||||||
|
Name: filepath.Join(distName, name),
|
||||||
|
Size: size,
|
||||||
|
Mode: int64(mode),
|
||||||
|
Uname: "root",
|
||||||
|
Gname: "root",
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode&os.ModeDir != 0 {
|
||||||
|
header.Typeflag = tar.TypeDir
|
||||||
|
fmt.Printf("%s %s\n", mode, name)
|
||||||
|
} else {
|
||||||
|
header.Typeflag = tar.TypeReg
|
||||||
|
fmt.Printf("%s %s (%d bytes)\n", mode, name, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tw.WriteHeader(&header); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mustWriteFile := func(name string, data []byte, mode os.FileMode) {
|
||||||
|
mustWriteHeader(name, int64(len(data)), mode)
|
||||||
|
if mode&os.ModeDir != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = tw.Write(data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mustWriteFromPath := func(dst, src string, mode os.FileMode) {
|
||||||
|
var r *os.File
|
||||||
|
if r, err = os.Open(src); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fi os.FileInfo
|
||||||
|
if fi, err = r.Stat(); err != nil {
|
||||||
|
_ = r.Close()
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == 0 {
|
||||||
|
mode = fi.Mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
mustWriteHeader(dst, fi.Size(), mode)
|
||||||
|
if _, err = io.Copy(tw, r); err != nil {
|
||||||
|
_ = r.Close()
|
||||||
|
panic(err)
|
||||||
|
} else if err = r.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mustWriteFile(".", nil, fs.ModeDir|0755)
|
||||||
|
mustWriteFile("comp/", nil, os.ModeDir|0755)
|
||||||
|
mustWriteFile("comp/_hakurei", comp, 0644)
|
||||||
|
mustWriteFile("install.sh", []byte(`#!/bin/sh -e
|
||||||
|
cd "$(dirname -- "$0")" || exit 1
|
||||||
|
|
||||||
|
install -vDm0755 "bin/hakurei" "${DESTDIR}`+prefix+`/bin/hakurei"
|
||||||
|
install -vDm0755 "bin/sharefs" "${DESTDIR}`+prefix+`/bin/sharefs"
|
||||||
|
|
||||||
|
install -vDm4511 "bin/hsu" "${DESTDIR}`+prefix+`/bin/hsu"
|
||||||
|
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
||||||
|
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -vDm0644 "comp/_hakurei" "${DESTDIR}`+prefix+`/share/zsh/site-functions/_hakurei"
|
||||||
|
`), 0755)
|
||||||
|
|
||||||
|
mustWriteFromPath("README.md", "README.md", 0)
|
||||||
|
mustWriteFile("hsurc.default", []byte("1000 0"), 0400)
|
||||||
|
mustWriteFromPath("bin/hsu", filepath.Join(s, "hsu"), 04511)
|
||||||
|
for _, name := range []string{
|
||||||
|
"hakurei",
|
||||||
|
"sharefs",
|
||||||
|
} {
|
||||||
|
mustWriteFromPath(
|
||||||
|
filepath.Join("bin", name),
|
||||||
|
filepath.Join(s, name),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tw.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else if err = gw.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else if err = f.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
f = nil
|
||||||
|
|
||||||
|
if err = os.WriteFile(
|
||||||
|
filepath.Join(destdir, distName+suffix+".sha512"),
|
||||||
|
append(hex.AppendEncode(nil, h.Sum(nil)), " "+distName+suffix+"\n"...),
|
||||||
|
0644,
|
||||||
|
); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err = os.Rename(
|
||||||
|
filepath.Join(s, distName+suffix),
|
||||||
|
filepath.Join(destdir, distName+suffix),
|
||||||
|
); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+153
-20
@@ -5,17 +5,91 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
. "syscall"
|
. "syscall"
|
||||||
|
|
||||||
|
"hakurei.app/internal/kobject"
|
||||||
|
"hakurei.app/internal/report"
|
||||||
|
"hakurei.app/internal/uevent"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
var r report.Reporter
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix("earlyinit: ")
|
||||||
|
r.SetOutput(log.Default())
|
||||||
|
|
||||||
|
// this handles SIGQUIT to provide useful debugging information without
|
||||||
|
// terminating, and prevents the runtime from throwing on the must family
|
||||||
|
// of early error reporting functions, DO NOT REMOVE
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, SIGQUIT)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
<-c
|
||||||
|
if p := pprof.Lookup("goroutine"); p == nil {
|
||||||
|
log.Println("initial built-in goroutine profile does not exist")
|
||||||
|
} else if err := p.WriteTo(os.Stderr, 2); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fatal calls [log.Println] with v and blocks forever. Must be called from
|
||||||
|
// main. Must not be used after error reporting is set up.
|
||||||
|
func fatal(v ...any) {
|
||||||
|
log.Println(v...)
|
||||||
|
log.Println("unable to continue, please reboot and resolve the problem manually")
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// must calls fatal with err if it is non-nil.
|
||||||
|
func must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustSyscall is like must, but with an additional action name.
|
||||||
|
func mustSyscall(action string, err error) {
|
||||||
|
if err != nil {
|
||||||
|
fatal("cannot "+action+":", err)
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// must1 is like must, but with an additional passed through value.
|
||||||
|
func must1[T any](v T, err error) T {
|
||||||
|
must(err)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// optionSystem specifies devpath of the system device.
|
||||||
|
optionSystem = "system"
|
||||||
|
|
||||||
|
// flagVerbose increases output verbosity.
|
||||||
|
flagVerbose = "verbose"
|
||||||
|
// flagStrict sets [report.DStrict] on r.
|
||||||
|
flagStrict = "strict"
|
||||||
|
// flagNoRecover sets [report.DNoRecover] on r.
|
||||||
|
flagNoRecover = "no_recover"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetPrefix("earlyinit: ")
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
option map[string]string
|
option map[string]string
|
||||||
@@ -33,15 +107,44 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Mount(
|
{
|
||||||
|
var flag uint64
|
||||||
|
if slices.Contains(flags, flagStrict) {
|
||||||
|
flag |= report.DStrict
|
||||||
|
}
|
||||||
|
if slices.Contains(flags, flagNoRecover) {
|
||||||
|
flag |= report.DNoRecover
|
||||||
|
}
|
||||||
|
log.Printf("reporting flags %x", flag)
|
||||||
|
r.SetFlags(flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := message.New(log.Default())
|
||||||
|
msg.SwapVerbose(slices.Contains(flags, flagVerbose))
|
||||||
|
|
||||||
|
mustSyscall("mount devtmpfs", Mount(
|
||||||
"devtmpfs",
|
"devtmpfs",
|
||||||
"/dev/",
|
"/dev/",
|
||||||
"devtmpfs",
|
"devtmpfs",
|
||||||
MS_NOSUID|MS_NOEXEC,
|
MS_NOSUID|MS_NOEXEC,
|
||||||
"",
|
"",
|
||||||
); err != nil {
|
))
|
||||||
log.Fatalf("cannot mount devtmpfs: %v", err)
|
must(os.Mkdir("/dev/pts/", 0))
|
||||||
}
|
mustSyscall("mount devpts", Mount(
|
||||||
|
"devpts",
|
||||||
|
"/dev/pts/",
|
||||||
|
"devpts",
|
||||||
|
MS_NOSUID|MS_NOEXEC,
|
||||||
|
"mode=620,ptmxmode=666",
|
||||||
|
))
|
||||||
|
must(os.Mkdir("/dev/shm/", 0))
|
||||||
|
mustSyscall("mount shm", Mount(
|
||||||
|
"shm",
|
||||||
|
"/dev/shm/",
|
||||||
|
"tmpfs",
|
||||||
|
MS_NOSUID|MS_NODEV,
|
||||||
|
"",
|
||||||
|
))
|
||||||
|
|
||||||
// The kernel might be unable to set up the console. When that happens,
|
// The kernel might be unable to set up the console. When that happens,
|
||||||
// printk is called with "Warning: unable to open an initial console."
|
// printk is called with "Warning: unable to open an initial console."
|
||||||
@@ -98,6 +201,49 @@ func main() {
|
|||||||
"",
|
"",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
conn := must1(uevent.Dial(-128 * 1024 * 1024))
|
||||||
|
events := make(chan *uevent.Message, 1<<10)
|
||||||
|
var uuid uevent.UUID
|
||||||
|
must1(rand.Read(uuid[:]))
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
go consume(ctx, msg, &r, conn, uuid, events)
|
||||||
|
s := kobject.New(uuid, func(o *kobject.Object, env map[string]string) {
|
||||||
|
p := make([]string, 0, len(env))
|
||||||
|
for k, v := range env {
|
||||||
|
p = append(p, k+"="+v)
|
||||||
|
}
|
||||||
|
slices.Sort(p)
|
||||||
|
log.Printf("change %s: %s", o.DevPath, strings.Join(p, ", "))
|
||||||
|
}, func(err error) {
|
||||||
|
severity := report.Inconsistent
|
||||||
|
if e, ok := err.(kobject.EventError); ok && e.Kind == kobject.EBadTarget {
|
||||||
|
severity = report.Trivial
|
||||||
|
}
|
||||||
|
r.Dispatch(
|
||||||
|
severity,
|
||||||
|
"processed inconsistent uevent",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
s.Consume(ctx, events)
|
||||||
|
|
||||||
|
log.Println("closing NETLINK_KOBJECT_UEVENT socket")
|
||||||
|
cancel()
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Fatal(err) // not reached
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
must(os.Mkdir("/system", 0))
|
||||||
|
if devpath := option[optionSystem]; devpath == "" {
|
||||||
|
fatal("system must be nonempty")
|
||||||
|
} else {
|
||||||
|
log.Printf("waiting for devpath pattern %q", devpath)
|
||||||
|
mustMountSystem(ctx, s, devpath)
|
||||||
|
}
|
||||||
|
|
||||||
// after top level has been set up
|
// after top level has been set up
|
||||||
mustSyscall("remount root", Mount(
|
mustSyscall("remount root", Mount(
|
||||||
"",
|
"",
|
||||||
@@ -113,19 +259,6 @@ func main() {
|
|||||||
[]byte("/system/lib/firmware"),
|
[]byte("/system/lib/firmware"),
|
||||||
0,
|
0,
|
||||||
))
|
))
|
||||||
|
go dispatchModprobe(ctx, s)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustSyscall calls [log.Fatalln] if err is non-nil.
|
|
||||||
func mustSyscall(action string, err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("cannot "+action+":", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// must calls [log.Fatal] with err if it is non-nil.
|
|
||||||
func must(err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hakurei.app/internal/kobject"
|
||||||
|
"hakurei.app/internal/report"
|
||||||
|
"hakurei.app/internal/uevent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModprobeError describes an unsuccessful modprobe invocation.
|
||||||
|
type ModprobeError struct {
|
||||||
|
ModAlias string `json:"modalias"`
|
||||||
|
Stdout string `json:"stdout"`
|
||||||
|
Stderr string `json:"stderr"`
|
||||||
|
ExitCode int `json:"exit_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ report.RepresentableError = ModprobeError{}
|
||||||
|
|
||||||
|
func (ModprobeError) Representable() {}
|
||||||
|
func (e ModprobeError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"modprobe exit status %d: %s",
|
||||||
|
e.ExitCode, strings.TrimSpace(e.Stderr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchModprobe invokes modprobe for [uevent.KOBJ_ADD] events raising new
|
||||||
|
// MODALIAS strings.
|
||||||
|
func dispatchModprobe(
|
||||||
|
ctx context.Context,
|
||||||
|
s *kobject.State,
|
||||||
|
) {
|
||||||
|
aliases := make(chan string, 1<<8)
|
||||||
|
go func() {
|
||||||
|
defer close(aliases)
|
||||||
|
s.Range(ctx, func(o *kobject.Object, act uevent.KobjectAction) bool {
|
||||||
|
if act == uevent.KOBJ_ADD && o.Driver == "" && o.ModAlias != "" {
|
||||||
|
aliases <- o.ModAlias
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
for alias := range aliases {
|
||||||
|
stdout, err := exec.Command("/system/sbin/modprobe", alias).Output()
|
||||||
|
if err == nil {
|
||||||
|
if len(stdout) > 0 {
|
||||||
|
log.Println(string(stdout))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
exitError, ok := errors.AsType[*exec.ExitError](err)
|
||||||
|
if !ok || exitError == nil {
|
||||||
|
r.Dispatch(report.Degraded, "invoke modprobe", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Dispatch(report.Trivial, "load device driver", ModprobeError{
|
||||||
|
ModAlias: alias,
|
||||||
|
Stdout: string(stdout),
|
||||||
|
Stderr: string(exitError.Stderr),
|
||||||
|
ExitCode: exitError.ExitCode(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/fhs"
|
||||||
|
"hakurei.app/internal/kobject"
|
||||||
|
"hakurei.app/internal/uevent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mustMountSystem waits for and mounts a system device matching pattern.
|
||||||
|
func mustMountSystem(
|
||||||
|
ctx context.Context,
|
||||||
|
s *kobject.State,
|
||||||
|
pattern string,
|
||||||
|
) {
|
||||||
|
c, stop := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var matchErr error
|
||||||
|
var systemPath *check.Absolute
|
||||||
|
s.Range(c, func(o *kobject.Object, act uevent.KobjectAction) bool {
|
||||||
|
if (act != uevent.KOBJ_ADD && act != uevent.KOBJ_CHANGE) ||
|
||||||
|
o.Subsystem != "block" ||
|
||||||
|
o.Env["DEVTYPE"] != "disk" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := filepath.Match(pattern, o.DevPath); err != nil {
|
||||||
|
matchErr = err
|
||||||
|
return false
|
||||||
|
} else if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := o.Env["DEVNAME"]
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
systemPath = fhs.AbsDev.Append(name)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if c.Err() != nil {
|
||||||
|
fatal("devpath", strconv.Quote(pattern), "never appeared")
|
||||||
|
}
|
||||||
|
if matchErr != nil {
|
||||||
|
fatal("cannot match system devpath:", matchErr)
|
||||||
|
}
|
||||||
|
err := syscall.Mount(
|
||||||
|
systemPath.String(),
|
||||||
|
"/system/",
|
||||||
|
"squashfs",
|
||||||
|
0,
|
||||||
|
"threads=multi",
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
fatal("cannot mount system:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/fhs"
|
||||||
|
"hakurei.app/internal/report"
|
||||||
|
"hakurei.app/internal/uevent"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newRejectColdboot returns a function to be called on every subsequent pending
|
||||||
|
// coldboot, and returns whether coldboot should proceed. Rejection is sticky.
|
||||||
|
func newRejectColdboot() func() bool {
|
||||||
|
// one coldboot per five minutes, two consecutive coldboot
|
||||||
|
const (
|
||||||
|
coldbootInterval = 5 * time.Minute
|
||||||
|
coldbootBurst = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
s := make(chan struct{}, coldbootBurst)
|
||||||
|
s <- struct{}{} // for early fault before reporting is ready
|
||||||
|
go func() {
|
||||||
|
t := time.NewTicker(coldbootInterval)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-t.C:
|
||||||
|
select {
|
||||||
|
case s <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return func() bool {
|
||||||
|
select {
|
||||||
|
case <-s:
|
||||||
|
return true
|
||||||
|
|
||||||
|
case <-done:
|
||||||
|
return false
|
||||||
|
|
||||||
|
default:
|
||||||
|
close(done)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume continuously consumes events from conn with retries.
|
||||||
|
func consume(
|
||||||
|
ctx context.Context,
|
||||||
|
msg message.Msg,
|
||||||
|
r *report.Reporter,
|
||||||
|
conn *uevent.Conn,
|
||||||
|
uuid uevent.UUID,
|
||||||
|
events chan<- *uevent.Message,
|
||||||
|
) {
|
||||||
|
defer close(events)
|
||||||
|
|
||||||
|
nextColdboot := newRejectColdboot()
|
||||||
|
coldboot := true
|
||||||
|
retry:
|
||||||
|
if dispatchErr := conn.Consume(ctx, fhs.Sys, &uuid, events, coldboot, func(path string) {
|
||||||
|
msg.Verbose("coldboot visited", path)
|
||||||
|
}, func(err error) bool {
|
||||||
|
if _, ok := err.(uevent.NeedsColdboot); ok && !nextColdboot() {
|
||||||
|
r.Dispatch(
|
||||||
|
report.Degraded,
|
||||||
|
"rejecting coldboot loop",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r.Dispatch(
|
||||||
|
report.Inconsistent,
|
||||||
|
"consumed invalid message",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}, nil); dispatchErr != nil {
|
||||||
|
if _, ok := dispatchErr.(uevent.Recoverable); !ok {
|
||||||
|
r.Dispatch(
|
||||||
|
report.Fatal,
|
||||||
|
"discontinuing uevent processing due to nonrecoverable error",
|
||||||
|
dispatchErr,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := dispatchErr.(uevent.NeedsColdboot); ok {
|
||||||
|
// coldboot loop rejected by handler
|
||||||
|
coldboot = false
|
||||||
|
}
|
||||||
|
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRejectColdboot(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
nextColdboot := newRejectColdboot()
|
||||||
|
want := func(want bool) {
|
||||||
|
if got := nextColdboot(); got != want {
|
||||||
|
t.Fatalf("nextColdboot: %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synctest.Wait()
|
||||||
|
want(true)
|
||||||
|
time.Sleep(time.Hour)
|
||||||
|
synctest.Wait()
|
||||||
|
want(true)
|
||||||
|
want(true)
|
||||||
|
time.Sleep(5 * time.Minute)
|
||||||
|
synctest.Wait()
|
||||||
|
want(true)
|
||||||
|
want(false)
|
||||||
|
time.Sleep(time.Hour)
|
||||||
|
synctest.Wait()
|
||||||
|
want(false)
|
||||||
|
want(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
+13
-6
@@ -38,8 +38,9 @@ var errSuccess = errors.New("success")
|
|||||||
|
|
||||||
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
||||||
var (
|
var (
|
||||||
flagVerbose bool
|
flagVerbose bool
|
||||||
flagJSON bool
|
flagInsecure bool
|
||||||
|
flagJSON bool
|
||||||
)
|
)
|
||||||
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
||||||
msg.SwapVerbose(flagVerbose)
|
msg.SwapVerbose(flagVerbose)
|
||||||
@@ -57,6 +58,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
return nil
|
return nil
|
||||||
}).
|
}).
|
||||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||||
|
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
|
||||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||||
|
|
||||||
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
|
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
|
||||||
@@ -75,7 +77,12 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
config.Container.Args = append(config.Container.Args, args[1:]...)
|
config.Container.Args = append(config.Container.Args, args[1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome.Main(ctx, msg, config, flagIdentifierFile)
|
var flags int
|
||||||
|
if flagInsecure {
|
||||||
|
flags |= hst.VAllowInsecure
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}).
|
}).
|
||||||
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
|
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
|
||||||
@@ -145,7 +152,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var et hst.Enablement
|
var et hst.Enablements
|
||||||
if flagWayland {
|
if flagWayland {
|
||||||
et |= hst.EWayland
|
et |= hst.EWayland
|
||||||
}
|
}
|
||||||
@@ -163,7 +170,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
ID: flagID,
|
ID: flagID,
|
||||||
Identity: flagIdentity,
|
Identity: flagIdentity,
|
||||||
Groups: flagGroups,
|
Groups: flagGroups,
|
||||||
Enablements: hst.NewEnablements(et),
|
Enablements: &et,
|
||||||
|
|
||||||
Container: &hst.ContainerConfig{
|
Container: &hst.ContainerConfig{
|
||||||
Filesystem: []hst.FilesystemConfigJSON{
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
@@ -282,7 +289,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome.Main(ctx, msg, &config, -1)
|
outcome.Main(ctx, msg, &config, 0, -1)
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}).
|
}).
|
||||||
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestHelp(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"main", []string{}, `
|
"main", []string{}, `
|
||||||
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
run Load and start container from configuration file
|
run Load and start container from configuration file
|
||||||
|
|||||||
+6
-5
@@ -7,7 +7,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// decodeJSON decodes json from r and stores it in v. A non-nil error results in a call to fatal.
|
// decodeJSON decodes json from r and stores it in v. A non-nil error results in
|
||||||
|
// a call to fatal.
|
||||||
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
|
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
|
||||||
err := json.NewDecoder(r).Decode(v)
|
err := json.NewDecoder(r).Decode(v)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -47,14 +48,14 @@ func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := encoder.Encode(v); err != nil {
|
if err := encoder.Encode(v); err != nil {
|
||||||
var marshalerError *json.MarshalerError
|
if e, ok := errors.AsType[*json.MarshalerError](err); ok && e != nil {
|
||||||
if errors.As(err, &marshalerError) && marshalerError != nil {
|
|
||||||
// this likely indicates an implementation error in hst
|
// this likely indicates an implementation error in hst
|
||||||
fatal("cannot encode json for " + marshalerError.Type.String() + ": " + marshalerError.Err.Error())
|
fatal("cannot encode json for " + e.Type.String() + ": " + e.Err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does not need to be handled
|
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does
|
||||||
|
// not need to be handled
|
||||||
fatal("cannot write json: " + err.Error())
|
fatal("cannot write json: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func printShowInstance(
|
|||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
|
||||||
if err := config.Validate(); err != nil {
|
if err := config.Validate(hst.VAllowInsecure); err != nil {
|
||||||
valid = false
|
valid = false
|
||||||
if m, ok := message.GetMessage(err); ok {
|
if m, ok := message.GetMessage(err); ok {
|
||||||
mustPrint(output, "Error: "+m+"!\n\n")
|
mustPrint(output, "Error: "+m+"!\n\n")
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var (
|
|||||||
PID: 0xbeef,
|
PID: 0xbeef,
|
||||||
ShimPID: 0xcafe,
|
ShimPID: 0xcafe,
|
||||||
Config: &hst.Config{
|
Config: &hst.Config{
|
||||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
|
Enablements: new(hst.EWayland | hst.EPipeWire),
|
||||||
Identity: 1,
|
Identity: 1,
|
||||||
Container: &hst.ContainerConfig{
|
Container: &hst.ContainerConfig{
|
||||||
Shell: check.MustAbs("/bin/sh"),
|
Shell: check.MustAbs("/bin/sh"),
|
||||||
@@ -64,7 +64,7 @@ func TestPrintShowInstance(t *testing.T) {
|
|||||||
Identity: 9 (org.chromium.Chromium)
|
Identity: 9 (org.chromium.Chromium)
|
||||||
Enablements: wayland, dbus, pipewire
|
Enablements: wayland, dbus, pipewire
|
||||||
Groups: video, dialout, plugdev
|
Groups: video, dialout, plugdev
|
||||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, noplace, device, cover_run, runtime, tmpdir
|
||||||
Home: /data/data/org.chromium.Chromium
|
Home: /data/data/org.chromium.Chromium
|
||||||
Hostname: localhost
|
Hostname: localhost
|
||||||
Path: /run/current-system/sw/bin/chromium
|
Path: /run/current-system/sw/bin/chromium
|
||||||
@@ -161,7 +161,7 @@ App
|
|||||||
Identity: 9 (org.chromium.Chromium)
|
Identity: 9 (org.chromium.Chromium)
|
||||||
Enablements: wayland, dbus, pipewire
|
Enablements: wayland, dbus, pipewire
|
||||||
Groups: video, dialout, plugdev
|
Groups: video, dialout, plugdev
|
||||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, noplace, device, cover_run, runtime, tmpdir
|
||||||
Home: /data/data/org.chromium.Chromium
|
Home: /data/data/org.chromium.Chromium
|
||||||
Hostname: localhost
|
Hostname: localhost
|
||||||
Path: /run/current-system/sw/bin/chromium
|
Path: /run/current-system/sw/bin/chromium
|
||||||
@@ -354,7 +354,9 @@ App
|
|||||||
"tty": true,
|
"tty": true,
|
||||||
"multiarch": true,
|
"multiarch": true,
|
||||||
"map_real_uid": true,
|
"map_real_uid": true,
|
||||||
|
"noplace": true,
|
||||||
"device": true,
|
"device": true,
|
||||||
|
"cover_run": true,
|
||||||
"share_runtime": true,
|
"share_runtime": true,
|
||||||
"share_tmpdir": true
|
"share_tmpdir": true
|
||||||
},
|
},
|
||||||
@@ -505,7 +507,9 @@ App
|
|||||||
"tty": true,
|
"tty": true,
|
||||||
"multiarch": true,
|
"multiarch": true,
|
||||||
"map_real_uid": true,
|
"map_real_uid": true,
|
||||||
|
"noplace": true,
|
||||||
"device": true,
|
"device": true,
|
||||||
|
"cover_run": true,
|
||||||
"share_runtime": true,
|
"share_runtime": true,
|
||||||
"share_tmpdir": true
|
"share_tmpdir": true
|
||||||
}
|
}
|
||||||
@@ -703,7 +707,9 @@ func TestPrintPs(t *testing.T) {
|
|||||||
"tty": true,
|
"tty": true,
|
||||||
"multiarch": true,
|
"multiarch": true,
|
||||||
"map_real_uid": true,
|
"map_real_uid": true,
|
||||||
|
"noplace": true,
|
||||||
"device": true,
|
"device": true,
|
||||||
|
"cover_run": true,
|
||||||
"share_runtime": true,
|
"share_runtime": true,
|
||||||
"share_tmpdir": true
|
"share_tmpdir": true
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-23
@@ -21,15 +21,6 @@
|
|||||||
// following paragraphs are considered an internal detail and not covered by the
|
// following paragraphs are considered an internal detail and not covered by the
|
||||||
// compatibility promise.
|
// compatibility promise.
|
||||||
//
|
//
|
||||||
// After checking credentials, hsu checks via /proc/ the absolute pathname of
|
|
||||||
// its parent process, and fails if it does not match the hakurei pathname set
|
|
||||||
// at link time. This is not a security feature: the priv-side is considered
|
|
||||||
// trusted, and this feature makes no attempt to address the racy nature of
|
|
||||||
// querying /proc/, or debuggers attached to the parent process. Instead, this
|
|
||||||
// aims to discourage misuse and reduce confusion if the user accidentally
|
|
||||||
// stumbles upon this program. It also prevents accidental use of the incorrect
|
|
||||||
// installation of hsu in some environments.
|
|
||||||
//
|
|
||||||
// Since target container environment variables are set up in shim via the
|
// Since target container environment variables are set up in shim via the
|
||||||
// [container] infrastructure, the environment is used for parameters from the
|
// [container] infrastructure, the environment is used for parameters from the
|
||||||
// parent process.
|
// parent process.
|
||||||
@@ -62,7 +53,6 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,18 +97,6 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var toolPath string
|
|
||||||
pexe := filepath.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
|
||||||
if p, err := os.Readlink(pexe); err != nil {
|
|
||||||
log.Fatalf("cannot read parent executable path: %v", err)
|
|
||||||
} else if strings.HasSuffix(p, " (deleted)") {
|
|
||||||
log.Fatal("hakurei executable has been deleted")
|
|
||||||
} else if p != hakureiPath {
|
|
||||||
log.Fatal("this program must be started by hakurei")
|
|
||||||
} else {
|
|
||||||
toolPath = p
|
|
||||||
}
|
|
||||||
|
|
||||||
// refuse to run if hsurc is not protected correctly
|
// refuse to run if hsurc is not protected correctly
|
||||||
if s, err := os.Stat(hsuConfPath); err != nil {
|
if s, err := os.Stat(hsuConfPath); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -205,7 +183,7 @@ func main() {
|
|||||||
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syscall.Exec(toolPath, []string{
|
if err := syscall.Exec(hakureiPath, []string{
|
||||||
"hakurei",
|
"hakurei",
|
||||||
"shim",
|
"shim",
|
||||||
}, []string{
|
}, []string{
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
"hakurei.app/internal/rosa"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cache refers to an instance of [pkg.Cache] that might be open.
|
||||||
|
type cache struct {
|
||||||
|
ctx context.Context
|
||||||
|
msg message.Msg
|
||||||
|
|
||||||
|
// Should generally not be used directly.
|
||||||
|
c *pkg.Cache
|
||||||
|
|
||||||
|
cures, jobs int
|
||||||
|
// Primarily to work around missing landlock LSM.
|
||||||
|
hostAbstract bool
|
||||||
|
// Set SCHED_IDLE.
|
||||||
|
idle bool
|
||||||
|
// Unset [pkg.CSuppressInit].
|
||||||
|
verboseInit bool
|
||||||
|
// Loaded artifact of [rosa.QEMU].
|
||||||
|
qemu pkg.Artifact
|
||||||
|
|
||||||
|
base, mirror string
|
||||||
|
}
|
||||||
|
|
||||||
|
// open opens the underlying [pkg.Cache].
|
||||||
|
func (cache *cache) open() (err error) {
|
||||||
|
if cache.c != nil {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
var base *check.Absolute
|
||||||
|
if cache.base, err = filepath.Abs(cache.base); err != nil {
|
||||||
|
return
|
||||||
|
} else if base, err = check.NewAbs(cache.base); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags int
|
||||||
|
if cache.idle {
|
||||||
|
flags |= pkg.CSchedIdle
|
||||||
|
}
|
||||||
|
if cache.hostAbstract {
|
||||||
|
flags |= pkg.CHostAbstract
|
||||||
|
}
|
||||||
|
if !cache.verboseInit {
|
||||||
|
flags |= pkg.CSuppressInit
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-cache.ctx.Done():
|
||||||
|
if testing.Testing() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Exit(2)
|
||||||
|
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cache.msg.Verbosef("opening cache at %s", base)
|
||||||
|
cache.c, err = pkg.Open(
|
||||||
|
cache.ctx,
|
||||||
|
cache.msg,
|
||||||
|
flags,
|
||||||
|
cache.cures,
|
||||||
|
cache.jobs,
|
||||||
|
base,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done <- struct{}{}
|
||||||
|
|
||||||
|
if cache.mirror != "" {
|
||||||
|
var pub []byte
|
||||||
|
pub, err = os.ReadFile(base.Append("ed25519.pub").String())
|
||||||
|
if err != nil {
|
||||||
|
cache.c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var r rosa.Remote
|
||||||
|
if r, err = rosa.NewRemote(cache.mirror, pub, http.DefaultClient); err != nil {
|
||||||
|
cache.c.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cache.c.SetExternal(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cache.qemu != nil {
|
||||||
|
var pathname *check.Absolute
|
||||||
|
pathname, _, err = cache.c.Cure(cache.qemu)
|
||||||
|
if err != nil {
|
||||||
|
cache.c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for arch, entry := range rosa.Arches(pathname) {
|
||||||
|
pkg.RegisterArch(arch, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying [pkg.Cache] if it is open.
|
||||||
|
func (cache *cache) Close() {
|
||||||
|
if cache.c != nil {
|
||||||
|
cache.c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do calls f on the underlying cache and returns its error value.
|
||||||
|
func (cache *cache) Do(f func(cache *pkg.Cache) error) error {
|
||||||
|
if cache.c == nil {
|
||||||
|
if err := cache.open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f(cache.c)
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCache(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cm := cache{
|
||||||
|
ctx: t.Context(),
|
||||||
|
msg: message.New(log.New(os.Stderr, "check: ", 0)),
|
||||||
|
base: t.TempDir(),
|
||||||
|
|
||||||
|
hostAbstract: true, idle: true,
|
||||||
|
}
|
||||||
|
defer cm.Close()
|
||||||
|
cm.Close()
|
||||||
|
|
||||||
|
if err := cm.open(); err != nil {
|
||||||
|
t.Fatalf("open: error = %v", err)
|
||||||
|
}
|
||||||
|
if err := cm.open(); err != os.ErrInvalid {
|
||||||
|
t.Errorf("(duplicate) open: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||||
|
return cache.Scrub(0)
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Scrub: error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"unique"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// daemonTimeout is the maximum amount of time cureFromIR will wait on I/O.
|
||||||
|
const daemonTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
// daemonDeadline returns the deadline corresponding to daemonTimeout, or the
|
||||||
|
// zero value when running in a test.
|
||||||
|
func daemonDeadline() time.Time {
|
||||||
|
if testing.Testing() {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return time.Now().Add(daemonTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// remoteNoReply notifies that the client will not receive a cure reply.
|
||||||
|
remoteNoReply = 1 << iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// cureFromIR services an IR curing request.
|
||||||
|
func cureFromIR(
|
||||||
|
cache *pkg.Cache,
|
||||||
|
conn net.Conn,
|
||||||
|
flags uint64,
|
||||||
|
) (pkg.Artifact, error) {
|
||||||
|
a, decodeErr := cache.NewDecoder(conn).Decode()
|
||||||
|
if decodeErr != nil {
|
||||||
|
_, err := conn.Write([]byte("\x00" + decodeErr.Error()))
|
||||||
|
return nil, errors.Join(decodeErr, err, conn.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
pathname, _, cureErr := cache.Cure(a)
|
||||||
|
if flags&remoteNoReply != 0 {
|
||||||
|
return a, errors.Join(cureErr, conn.Close())
|
||||||
|
}
|
||||||
|
if err := conn.SetWriteDeadline(daemonDeadline()); err != nil {
|
||||||
|
return a, errors.Join(cureErr, err, conn.Close())
|
||||||
|
}
|
||||||
|
if cureErr != nil {
|
||||||
|
_, err := conn.Write([]byte("\x00" + cureErr.Error()))
|
||||||
|
return a, errors.Join(cureErr, err, conn.Close())
|
||||||
|
}
|
||||||
|
_, err := conn.Write([]byte(pathname.String()))
|
||||||
|
if testing.Testing() && errors.Is(err, io.ErrClosedPipe) {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
return a, errors.Join(err, conn.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// specialCancel is a message consisting of a single identifier referring
|
||||||
|
// to a curing artifact to be cancelled.
|
||||||
|
specialCancel = iota
|
||||||
|
// specialAbort requests for all pending cures to be aborted. It has no
|
||||||
|
// message body.
|
||||||
|
specialAbort
|
||||||
|
|
||||||
|
// remoteSpecial denotes a special message with custom layout.
|
||||||
|
remoteSpecial = math.MaxUint64
|
||||||
|
)
|
||||||
|
|
||||||
|
// writeSpecialHeader writes the header of a remoteSpecial message.
|
||||||
|
func writeSpecialHeader(conn net.Conn, kind uint64) error {
|
||||||
|
var sh [16]byte
|
||||||
|
binary.LittleEndian.PutUint64(sh[:], remoteSpecial)
|
||||||
|
binary.LittleEndian.PutUint64(sh[8:], kind)
|
||||||
|
if n, err := conn.Write(sh[:]); err != nil {
|
||||||
|
return err
|
||||||
|
} else if n != len(sh) {
|
||||||
|
return io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancelIdent reads an identifier from conn and cancels the corresponding cure.
|
||||||
|
func cancelIdent(
|
||||||
|
cache *pkg.Cache,
|
||||||
|
conn net.Conn,
|
||||||
|
) (*pkg.ID, bool, error) {
|
||||||
|
var ident pkg.ID
|
||||||
|
if _, err := io.ReadFull(conn, ident[:]); err != nil {
|
||||||
|
return nil, false, errors.Join(err, conn.Close())
|
||||||
|
}
|
||||||
|
ok := cache.Cancel(unique.Make(ident))
|
||||||
|
return &ident, ok, conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve services connections from a [net.UnixListener].
|
||||||
|
func serve(
|
||||||
|
ctx context.Context,
|
||||||
|
log *log.Logger,
|
||||||
|
cm *cache,
|
||||||
|
ul *net.UnixListener,
|
||||||
|
) error {
|
||||||
|
ul.SetUnlinkOnClose(true)
|
||||||
|
if cm.c == nil {
|
||||||
|
if err := cm.open(); err != nil {
|
||||||
|
return errors.Join(err, ul.Close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := ul.AcceptUnix()
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wg.Go(func() {
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
_ = conn.SetDeadline(time.Now())
|
||||||
|
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _err := conn.SetReadDeadline(daemonDeadline()); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
if _err = conn.Close(); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var word [8]byte
|
||||||
|
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
if _err = conn.Close(); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flags := binary.LittleEndian.Uint64(word[:])
|
||||||
|
|
||||||
|
if flags == remoteSpecial {
|
||||||
|
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
if _err = conn.Close(); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch special := binary.LittleEndian.Uint64(word[:]); special {
|
||||||
|
default:
|
||||||
|
log.Printf("invalid special %d", special)
|
||||||
|
|
||||||
|
case specialCancel:
|
||||||
|
if id, ok, _err := cancelIdent(cm.c, conn); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
} else if !ok {
|
||||||
|
log.Println(
|
||||||
|
"attempting to cancel invalid artifact",
|
||||||
|
pkg.Encode(*id),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.Println(
|
||||||
|
"cancelled artifact",
|
||||||
|
pkg.Encode(*id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case specialAbort:
|
||||||
|
log.Println("aborting all pending cures")
|
||||||
|
cm.c.Abort()
|
||||||
|
if _err := conn.Close(); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, _err := cureFromIR(cm.c, conn, flags); _err != nil {
|
||||||
|
log.Println(_err)
|
||||||
|
} else {
|
||||||
|
log.Printf(
|
||||||
|
"fulfilled artifact %s",
|
||||||
|
pkg.Encode(cm.c.Ident(a).Value()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
if err := ul.SetDeadline(time.Now()); err != nil {
|
||||||
|
return errors.Join(err, ul.Close())
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return ul.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// dial wraps [net.DialUnix] with a context.
|
||||||
|
func dial(ctx context.Context, addr *net.UnixAddr) (
|
||||||
|
done chan<- struct{},
|
||||||
|
conn *net.UnixConn,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
conn, err = net.DialUnix("unix", nil, addr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d := make(chan struct{})
|
||||||
|
done = d
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
_ = conn.SetDeadline(time.Now())
|
||||||
|
|
||||||
|
case <-d:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// cureRemote cures a [pkg.Artifact] on a daemon.
|
||||||
|
func cureRemote(
|
||||||
|
ctx context.Context,
|
||||||
|
addr *net.UnixAddr,
|
||||||
|
a pkg.Artifact,
|
||||||
|
flags uint64,
|
||||||
|
) (*check.Absolute, error) {
|
||||||
|
if flags == remoteSpecial {
|
||||||
|
return nil, syscall.EINVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
done, conn, err := dial(ctx, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
if n, flagErr := conn.Write(binary.LittleEndian.AppendUint64(nil, flags)); flagErr != nil {
|
||||||
|
return nil, errors.Join(flagErr, conn.Close())
|
||||||
|
} else if n != 8 {
|
||||||
|
return nil, errors.Join(io.ErrShortWrite, conn.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = pkg.NewIR().EncodeAll(conn, a); err != nil {
|
||||||
|
return nil, errors.Join(err, conn.Close())
|
||||||
|
} else if err = conn.CloseWrite(); err != nil {
|
||||||
|
return nil, errors.Join(err, conn.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags&remoteNoReply != 0 {
|
||||||
|
return nil, conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, recvErr := io.ReadAll(conn)
|
||||||
|
if err = errors.Join(recvErr, conn.Close()); err != nil {
|
||||||
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
|
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||||
|
err = cancelErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(payload) > 0 && payload[0] == 0 {
|
||||||
|
return nil, errors.New(string(payload[1:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
var p *check.Absolute
|
||||||
|
p, err = check.NewAbs(string(payload))
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancelRemote cancels a [pkg.Artifact] curing on a daemon.
|
||||||
|
func cancelRemote(
|
||||||
|
ctx context.Context,
|
||||||
|
addr *net.UnixAddr,
|
||||||
|
a pkg.Artifact,
|
||||||
|
wait bool,
|
||||||
|
) error {
|
||||||
|
done, conn, err := dial(ctx, addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
if err = writeSpecialHeader(conn, specialCancel); err != nil {
|
||||||
|
return errors.Join(err, conn.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
id := pkg.NewIR().Ident(a).Value()
|
||||||
|
if n, err = conn.Write(id[:]); err != nil {
|
||||||
|
return errors.Join(err, conn.Close())
|
||||||
|
} else if n != len(id) {
|
||||||
|
return errors.Join(io.ErrShortWrite, conn.Close())
|
||||||
|
}
|
||||||
|
if wait {
|
||||||
|
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(err, conn.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
// abortRemote aborts all [pkg.Artifact] curing on a daemon.
|
||||||
|
func abortRemote(
|
||||||
|
ctx context.Context,
|
||||||
|
addr *net.UnixAddr,
|
||||||
|
wait bool,
|
||||||
|
) error {
|
||||||
|
done, conn, err := dial(ctx, addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
err = writeSpecialHeader(conn, specialAbort)
|
||||||
|
if wait && err == nil {
|
||||||
|
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(err, conn.Close())
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNoReply(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if !daemonDeadline().IsZero() {
|
||||||
|
t.Fatal("daemonDeadline did not return the zero value")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := pkg.Open(
|
||||||
|
t.Context(),
|
||||||
|
message.New(log.New(os.Stderr, "cir: ", 0)),
|
||||||
|
0, 0, 0,
|
||||||
|
check.MustAbs(t.TempDir()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: error = %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
client, server := net.Pipe()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
go func() {
|
||||||
|
<-t.Context().Done()
|
||||||
|
if _err := client.SetDeadline(time.Now()); _err != nil && !errors.Is(_err, io.ErrClosedPipe) {
|
||||||
|
panic(_err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _err := c.EncodeAll(
|
||||||
|
client,
|
||||||
|
pkg.NewFile("check", []byte{0}),
|
||||||
|
); _err != nil {
|
||||||
|
panic(_err)
|
||||||
|
} else if _err = client.Close(); _err != nil {
|
||||||
|
panic(_err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
a, cureErr := cureFromIR(c, server, remoteNoReply)
|
||||||
|
if cureErr != nil {
|
||||||
|
t.Fatalf("cureFromIR: error = %v", cureErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-done
|
||||||
|
wantIdent := pkg.MustDecode("fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG")
|
||||||
|
if gotIdent := c.Ident(a).Value(); gotIdent != wantIdent {
|
||||||
|
t.Errorf(
|
||||||
|
"cureFromIR: %s, want %s",
|
||||||
|
pkg.Encode(gotIdent), pkg.Encode(wantIdent),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaemon(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := log.New(&buf, "daemon: ", 0)
|
||||||
|
|
||||||
|
addr := net.UnixAddr{
|
||||||
|
Name: filepath.Join(t.TempDir(), "daemon"),
|
||||||
|
Net: "unix",
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cm := cache{
|
||||||
|
ctx: ctx,
|
||||||
|
msg: message.New(logger),
|
||||||
|
base: t.TempDir(),
|
||||||
|
}
|
||||||
|
defer cm.Close()
|
||||||
|
|
||||||
|
ul, err := net.ListenUnix("unix", &addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListenUnix: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
if _err := serve(ctx, logger, &cm, ul); _err != nil {
|
||||||
|
panic(_err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = cancelRemote(ctx, &addr, pkg.NewFile("nonexistent", nil), true); err != nil {
|
||||||
|
t.Fatalf("cancelRemote: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = abortRemote(ctx, &addr, true); err != nil {
|
||||||
|
t.Fatalf("abortRemote: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep this last for synchronisation
|
||||||
|
var p *check.Absolute
|
||||||
|
p, err = cureRemote(ctx, &addr, pkg.NewFile("check", []byte{0}), 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cureRemote: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
<-done
|
||||||
|
|
||||||
|
const want = "fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG"
|
||||||
|
if got := filepath.Base(p.String()); got != want {
|
||||||
|
t.Errorf("cureRemote: %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantLog := []string{
|
||||||
|
"",
|
||||||
|
"daemon: aborting all pending cures",
|
||||||
|
"daemon: attempting to cancel invalid artifact kQm9fmnCmXST1-MMmxzcau2oKZCXXrlZydo4PkeV5hO_2PKfeC8t98hrbV_ZZx_j",
|
||||||
|
"daemon: fulfilled artifact fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG",
|
||||||
|
}
|
||||||
|
gotLog := strings.Split(buf.String(), "\n")
|
||||||
|
slices.Sort(gotLog)
|
||||||
|
if !slices.Equal(gotLog, wantLog) {
|
||||||
|
t.Errorf(
|
||||||
|
"serve: logged\n%s\nwant\n%s",
|
||||||
|
strings.Join(gotLog, "\n"), strings.Join(wantLog, "\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"unique"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
"hakurei.app/internal/rosa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// commandInfo implements the info subcommand.
|
||||||
|
func commandInfo(
|
||||||
|
cm *cache,
|
||||||
|
args []string,
|
||||||
|
w io.Writer,
|
||||||
|
writeStatus bool,
|
||||||
|
r *rosa.Report,
|
||||||
|
) (err error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("info requires at least 1 argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
// recovered by HandleAccess
|
||||||
|
mustPrintln := func(a ...any) {
|
||||||
|
if _, _err := fmt.Fprintln(w, a...); _err != nil {
|
||||||
|
panic(_err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mustPrint := func(a ...any) {
|
||||||
|
if _, _err := fmt.Fprint(w, a...); _err != nil {
|
||||||
|
panic(_err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t := rosa.Native().Std()
|
||||||
|
for i, name := range args {
|
||||||
|
handle := rosa.ArtifactH(unique.Make(name))
|
||||||
|
if meta, a := t.Load(handle); meta == nil {
|
||||||
|
return fmt.Errorf("unknown artifact %q", name)
|
||||||
|
} else {
|
||||||
|
var suffix string
|
||||||
|
|
||||||
|
if meta.Version != rosa.Unversioned {
|
||||||
|
suffix += "-" + meta.Version
|
||||||
|
}
|
||||||
|
mustPrintln("name : " + name + suffix)
|
||||||
|
|
||||||
|
mustPrintln("description : " + meta.Description)
|
||||||
|
if meta.Website != "" {
|
||||||
|
mustPrintln("website : " +
|
||||||
|
strings.TrimSuffix(meta.Website, "/"))
|
||||||
|
}
|
||||||
|
if len(meta.Dependencies) > 0 {
|
||||||
|
mustPrint("depends on :")
|
||||||
|
for _, d := range meta.Dependencies {
|
||||||
|
_meta, _ := rosa.Native().Std().MustLoad(d)
|
||||||
|
s := _meta.Name
|
||||||
|
if _meta.Version != rosa.Unversioned {
|
||||||
|
s += "-" + _meta.Version
|
||||||
|
}
|
||||||
|
mustPrint(" " + s)
|
||||||
|
}
|
||||||
|
mustPrintln()
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusPrefix = "status : "
|
||||||
|
if writeStatus {
|
||||||
|
if r == nil {
|
||||||
|
var f io.ReadSeekCloser
|
||||||
|
err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||||
|
f, err = cache.OpenStatus(a)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
mustPrintln(
|
||||||
|
statusPrefix + "not yet cured",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mustPrint(statusPrefix)
|
||||||
|
_, err = io.Copy(w, f)
|
||||||
|
if err = errors.Join(err, f.Close()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||||
|
status, n := r.ArtifactOf(cache.Ident(a))
|
||||||
|
if status == nil {
|
||||||
|
mustPrintln(
|
||||||
|
statusPrefix + "not in report",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mustPrintln("size :", n)
|
||||||
|
mustPrint(statusPrefix)
|
||||||
|
if _, err = w.Write(status); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != len(args)-1 {
|
||||||
|
mustPrintln()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"unique"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
"hakurei.app/internal/rosa"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInfo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_t := rosa.Native().Std()
|
||||||
|
qemuMeta, _ := _t.Load(rosa.H("qemu"))
|
||||||
|
glibMeta, _ := _t.Load(rosa.H("glib"))
|
||||||
|
zlibMeta, zlib := _t.Load(rosa.H("zlib"))
|
||||||
|
zstdMeta, _ := _t.Load(rosa.H("zstd"))
|
||||||
|
hakureiMeta, _ := _t.Load(rosa.H("hakurei"))
|
||||||
|
hakureiDistMeta, _ := _t.Load(rosa.H("hakurei-dist"))
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
status map[string]string
|
||||||
|
report string
|
||||||
|
want string
|
||||||
|
wantErr any
|
||||||
|
}{
|
||||||
|
{"qemu", []string{"qemu"}, nil, "", `
|
||||||
|
name : qemu-` + qemuMeta.Version + `
|
||||||
|
description : a generic and open source machine emulator and virtualizer
|
||||||
|
website : https://www.qemu.org
|
||||||
|
depends on : glib-` + glibMeta.Version + ` zstd-` + zstdMeta.Version + `
|
||||||
|
`, nil},
|
||||||
|
|
||||||
|
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
|
||||||
|
name : hakurei-` + hakureiMeta.Version + `
|
||||||
|
description : low-level userspace tooling for Rosa OS
|
||||||
|
website : https://hakurei.app
|
||||||
|
|
||||||
|
name : hakurei-dist-` + hakureiDistMeta.Version + `
|
||||||
|
description : low-level userspace tooling for Rosa OS (distribution tarball)
|
||||||
|
website : https://hakurei.app
|
||||||
|
`, nil},
|
||||||
|
|
||||||
|
{"nonexistent", []string{"zlib", "\x00"}, nil, "", `
|
||||||
|
name : zlib-` + zlibMeta.Version + `
|
||||||
|
description : lossless data-compression library
|
||||||
|
website : https://zlib.net
|
||||||
|
|
||||||
|
`, fmt.Errorf("unknown artifact %q", "\x00")},
|
||||||
|
|
||||||
|
{"status cache", []string{"zlib", "zstd"}, map[string]string{
|
||||||
|
"zstd": "internal/pkg (amd64) on satori\n",
|
||||||
|
"hakurei": "internal/pkg (amd64) on satori\n\n",
|
||||||
|
}, "", `
|
||||||
|
name : zlib-` + zlibMeta.Version + `
|
||||||
|
description : lossless data-compression library
|
||||||
|
website : https://zlib.net
|
||||||
|
status : not yet cured
|
||||||
|
|
||||||
|
name : zstd-` + zstdMeta.Version + `
|
||||||
|
description : a fast compression algorithm
|
||||||
|
website : https://facebook.github.io/zstd
|
||||||
|
status : internal/pkg (amd64) on satori
|
||||||
|
`, nil},
|
||||||
|
|
||||||
|
{"status cache perm", []string{"zlib"}, map[string]string{
|
||||||
|
"zlib": "\x00",
|
||||||
|
}, "", `
|
||||||
|
name : zlib-` + zlibMeta.Version + `
|
||||||
|
description : lossless data-compression library
|
||||||
|
website : https://zlib.net
|
||||||
|
`, func(cm *cache) error {
|
||||||
|
return &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(zlib).Value())),
|
||||||
|
Err: syscall.EACCES,
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
|
||||||
|
{"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
|
||||||
|
name : zlib-` + zlibMeta.Version + `
|
||||||
|
description : lossless data-compression library
|
||||||
|
website : https://zlib.net
|
||||||
|
status : not in report
|
||||||
|
`, nil},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
cm *cache
|
||||||
|
buf strings.Builder
|
||||||
|
r *rosa.Report
|
||||||
|
)
|
||||||
|
|
||||||
|
if tc.status != nil || tc.report != "" {
|
||||||
|
cm = &cache{
|
||||||
|
ctx: context.Background(),
|
||||||
|
msg: message.New(log.New(os.Stderr, "info: ", 0)),
|
||||||
|
base: t.TempDir(),
|
||||||
|
}
|
||||||
|
defer cm.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.report != "" {
|
||||||
|
pathname := filepath.Join(t.TempDir(), "report")
|
||||||
|
err := os.WriteFile(
|
||||||
|
pathname,
|
||||||
|
unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)),
|
||||||
|
0400,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err = rosa.OpenReport(pathname)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = r.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.status != nil {
|
||||||
|
for name, status := range tc.status {
|
||||||
|
_, a := _t.Load(rosa.ArtifactH(unique.Make(name)))
|
||||||
|
if a == nil {
|
||||||
|
t.Fatalf("invalid name %q", name)
|
||||||
|
}
|
||||||
|
perm := os.FileMode(0400)
|
||||||
|
if status == "\x00" {
|
||||||
|
perm = 0
|
||||||
|
}
|
||||||
|
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||||
|
return os.WriteFile(filepath.Join(
|
||||||
|
cm.base,
|
||||||
|
"status",
|
||||||
|
pkg.Encode(cache.Ident(a).Value()),
|
||||||
|
), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Do: error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wantErr error
|
||||||
|
switch c := tc.wantErr.(type) {
|
||||||
|
case error:
|
||||||
|
wantErr = c
|
||||||
|
case func(cm *cache) error:
|
||||||
|
wantErr = c(cm)
|
||||||
|
default:
|
||||||
|
if tc.wantErr != nil {
|
||||||
|
t.Fatalf("invalid wantErr %#v", tc.wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commandInfo(
|
||||||
|
cm,
|
||||||
|
tc.args,
|
||||||
|
&buf,
|
||||||
|
cm != nil,
|
||||||
|
r,
|
||||||
|
); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Fatalf("commandInfo: error = %v, want %v", err, wantErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := buf.String(); got != strings.TrimPrefix(tc.want, "\n") {
|
||||||
|
t.Errorf("commandInfo:\n%s\nwant\n%s", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
// Package pkgserver implements the package metadata service backend.
|
||||||
|
package pkgserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/internal/info"
|
||||||
|
"hakurei.app/internal/rosa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// for lazy initialisation of serveInfo
|
||||||
|
var (
|
||||||
|
infoPayload struct {
|
||||||
|
// Current package count.
|
||||||
|
Count int `json:"count"`
|
||||||
|
// Hakurei version, set at link time.
|
||||||
|
HakureiVersion string `json:"hakurei_version"`
|
||||||
|
}
|
||||||
|
infoPayloadOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleInfo writes constant system information.
|
||||||
|
func handleInfo(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
infoPayloadOnce.Do(func() {
|
||||||
|
infoPayload.Count = len(rosa.Native().Collect())
|
||||||
|
infoPayload.HakureiVersion = info.Version()
|
||||||
|
})
|
||||||
|
// TODO(mae): cache entire response if no additional fields are planned
|
||||||
|
writeAPIPayload(w, infoPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStatusHandler returns a [http.HandlerFunc] that offers status files for
|
||||||
|
// viewing or download, if available.
|
||||||
|
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m, ok := index.names[path.Base(r.URL.Path)]
|
||||||
|
if !ok || !m.HasReport {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := "text/plain; charset=utf-8"
|
||||||
|
if disposition {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
|
||||||
|
// quoting like this is unsound, but okay, because metadata is hardcoded
|
||||||
|
contentDisposition := `attachment; filename="`
|
||||||
|
contentDisposition += m.Name + "-"
|
||||||
|
if m.Version != "" {
|
||||||
|
contentDisposition += m.Version + "-"
|
||||||
|
}
|
||||||
|
contentDisposition += m.ids + `.log"`
|
||||||
|
w.Header().Set("Content-Disposition", contentDisposition)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
if err := func() (err error) {
|
||||||
|
defer index.handleAccess(&err)()
|
||||||
|
_, err = w.Write(m.status)
|
||||||
|
return
|
||||||
|
}(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(
|
||||||
|
w, "cannot deliver status, contact maintainers",
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGet writes a slice of metadata with specified order.
|
||||||
|
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
limit, err := strconv.Atoi(q.Get("limit"))
|
||||||
|
if err != nil || limit > 100 || limit < 1 {
|
||||||
|
http.Error(
|
||||||
|
w, "limit must be an integer between 1 and 100",
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(q.Get("index"))
|
||||||
|
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
||||||
|
http.Error(
|
||||||
|
w, "index must be an integer between 0 and "+
|
||||||
|
strconv.Itoa(len(index.sorts[0])-1),
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort, err := strconv.Atoi(q.Get("sort"))
|
||||||
|
if err != nil || sort >= len(index.sorts) || sort < 0 {
|
||||||
|
http.Error(
|
||||||
|
w, "sort must be an integer between 0 and "+
|
||||||
|
strconv.Itoa(sortOrderEnd),
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
||||||
|
writeAPIPayload(w, &struct {
|
||||||
|
Values []*metadata `json:"values"`
|
||||||
|
}{values})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
limit, err := strconv.Atoi(q.Get("limit"))
|
||||||
|
if err != nil || limit > 100 || limit < 1 {
|
||||||
|
http.Error(
|
||||||
|
w, "limit must be an integer between 1 and 100",
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(q.Get("index"))
|
||||||
|
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
||||||
|
http.Error(
|
||||||
|
w, "index must be an integer between 0 and "+
|
||||||
|
strconv.Itoa(len(index.sorts[0])-1),
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
search, err := url.QueryUnescape(q.Get("search"))
|
||||||
|
if len(search) > 100 || err != nil {
|
||||||
|
http.Error(
|
||||||
|
w, "search must be a string between 0 and 100 characters long",
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
desc := q.Get("desc") == "true"
|
||||||
|
n, res, err := index.performSearchQuery(limit, i, search, desc)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
writeAPIPayload(w, &struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Values []searchResult `json:"values"`
|
||||||
|
}{n, res})
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiVersion is the name of the current API revision, as part of the pattern.
|
||||||
|
const apiVersion = "v1"
|
||||||
|
|
||||||
|
// registerAPI registers API handler functions.
|
||||||
|
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
|
||||||
|
mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
|
||||||
|
mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
|
||||||
|
mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
|
||||||
|
mux.HandleFunc("GET /status/", index.newStatusHandler(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register arranges for mux to service API requests.
|
||||||
|
func Register(ctx context.Context, mux *http.ServeMux, report *rosa.Report) error {
|
||||||
|
var index packageIndex
|
||||||
|
index.search = make(searchCache)
|
||||||
|
if err := index.populate(report); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
index.search.clean()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
index.registerAPI(mux)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeAPIPayload sets headers common to API responses and encodes payload as
|
||||||
|
// JSON for the response body.
|
||||||
|
func writeAPIPayload(w http.ResponseWriter, payload any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Pragma", "no-cache")
|
||||||
|
w.Header().Set("Expires", "0")
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(
|
||||||
|
w, "cannot encode payload, contact maintainers",
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package pkgserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/info"
|
||||||
|
"hakurei.app/internal/rosa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// prefix is prepended to every API path.
|
||||||
|
const prefix = "/api/" + apiVersion + "/"
|
||||||
|
|
||||||
|
func TestAPIInfo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handleInfo(w, httptest.NewRequestWithContext(
|
||||||
|
t.Context(),
|
||||||
|
http.MethodGet,
|
||||||
|
prefix+"info",
|
||||||
|
nil,
|
||||||
|
))
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
checkStatus(t, resp, http.StatusOK)
|
||||||
|
checkAPIHeader(t, w.Header())
|
||||||
|
|
||||||
|
checkPayload(t, resp, struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
HakureiVersion string `json:"hakurei_version"`
|
||||||
|
}{len(rosa.Native().Collect()), info.Version()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIGet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const target = prefix + "get"
|
||||||
|
|
||||||
|
index := newIndex(t)
|
||||||
|
newRequest := func(suffix string) *httptest.ResponseRecorder {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
index.handleGet(w, httptest.NewRequestWithContext(
|
||||||
|
t.Context(),
|
||||||
|
http.MethodGet,
|
||||||
|
target+suffix,
|
||||||
|
nil,
|
||||||
|
))
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := newRequest("?" + suffix + "=invalid")
|
||||||
|
resp := w.Result()
|
||||||
|
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("min", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
|
||||||
|
resp := w.Result()
|
||||||
|
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||||
|
|
||||||
|
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
|
||||||
|
resp = w.Result()
|
||||||
|
checkStatus(t, resp, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("max", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
|
||||||
|
resp := w.Result()
|
||||||
|
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||||
|
|
||||||
|
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
|
||||||
|
resp = w.Result()
|
||||||
|
checkStatus(t, resp, http.StatusOK)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("limit", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checkValidate(
|
||||||
|
t, "index=0&sort=0&limit", 1, 100,
|
||||||
|
"limit must be an integer between 1 and 100",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
count := len(rosa.Native().Collect())
|
||||||
|
t.Run("index", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checkValidate(
|
||||||
|
t, "limit=1&sort=0&index", 0, count-1,
|
||||||
|
"index must be an integer between 0 and "+strconv.Itoa(count-1),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sort", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
checkValidate(
|
||||||
|
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
|
||||||
|
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package pkgserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
"hakurei.app/internal/rosa"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
declarationAscending = iota
|
||||||
|
declarationDescending
|
||||||
|
nameAscending
|
||||||
|
nameDescending
|
||||||
|
sizeAscending
|
||||||
|
sizeDescending
|
||||||
|
|
||||||
|
sortOrderEnd = iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// packageIndex refers to metadata by name and various sort orders.
|
||||||
|
type packageIndex struct {
|
||||||
|
sorts [sortOrderEnd + 1][]*metadata
|
||||||
|
names map[string]*metadata
|
||||||
|
search searchCache
|
||||||
|
// Taken from [rosa.Report] if available.
|
||||||
|
handleAccess func(*error) func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// metadata holds [rosa.Metadata] extended with additional information.
|
||||||
|
type metadata struct {
|
||||||
|
handle rosa.ArtifactH
|
||||||
|
*rosa.Metadata
|
||||||
|
|
||||||
|
// Copied from [rosa.Metadata], [rosa.Unversioned] is equivalent to the zero
|
||||||
|
// value. Otherwise, the zero value is invalid.
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
// Output data size, available if present in report.
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
// Whether the underlying [pkg.Artifact] is present in the report.
|
||||||
|
HasReport bool `json:"report"`
|
||||||
|
|
||||||
|
// Ident string encoded ahead of time.
|
||||||
|
ids string
|
||||||
|
// Backed by [rosa.Report], access must be prepared by HandleAccess.
|
||||||
|
status []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate deterministically populates packageIndex, optionally with a report.
|
||||||
|
func (index *packageIndex) populate(report *rosa.Report) (err error) {
|
||||||
|
if report != nil {
|
||||||
|
defer report.HandleAccess(&err)()
|
||||||
|
index.handleAccess = report.HandleAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
handles := rosa.Native().Collect()
|
||||||
|
work := make([]*metadata, len(handles))
|
||||||
|
index.names = make(map[string]*metadata)
|
||||||
|
ir := pkg.NewIR()
|
||||||
|
for i, handle := range handles {
|
||||||
|
meta, a := rosa.Native().Std().MustLoad(handle)
|
||||||
|
m := metadata{
|
||||||
|
handle: handle,
|
||||||
|
|
||||||
|
Metadata: meta,
|
||||||
|
Version: meta.Version,
|
||||||
|
}
|
||||||
|
if m.Version == "" {
|
||||||
|
return errors.New("invalid version from " + m.Name)
|
||||||
|
}
|
||||||
|
if m.Version == rosa.Unversioned {
|
||||||
|
m.Version = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if report != nil {
|
||||||
|
id := ir.Ident(a)
|
||||||
|
m.ids = pkg.Encode(id.Value())
|
||||||
|
m.status, m.Size = report.ArtifactOf(id)
|
||||||
|
m.HasReport = m.Size >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
work[i] = &m
|
||||||
|
index.names[m.Name] = &m
|
||||||
|
}
|
||||||
|
|
||||||
|
index.sorts[declarationAscending] = work
|
||||||
|
index.sorts[declarationDescending] = slices.Clone(work)
|
||||||
|
slices.Reverse(index.sorts[declarationDescending][:])
|
||||||
|
|
||||||
|
index.sorts[nameAscending] = slices.Clone(work)
|
||||||
|
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
|
||||||
|
return strings.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
index.sorts[nameDescending] = slices.Clone(index.sorts[nameAscending])
|
||||||
|
slices.Reverse(index.sorts[nameDescending][:])
|
||||||
|
|
||||||
|
index.sorts[sizeAscending] = slices.Clone(work)
|
||||||
|
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
|
||||||
|
return cmp.Compare(a.Size, b.Size)
|
||||||
|
})
|
||||||
|
index.sorts[sizeDescending] = slices.Clone(index.sorts[sizeAscending])
|
||||||
|
slices.Reverse(index.sorts[sizeDescending][:])
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package pkgserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newIndex returns the address of a newly populated packageIndex.
|
||||||
|
func newIndex(t *testing.T) *packageIndex {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var index packageIndex
|
||||||
|
if err := index.populate(nil); err != nil {
|
||||||
|
t.Fatalf("populate: error = %v", err)
|
||||||
|
}
|
||||||
|
return &index
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkStatus checks response status code.
|
||||||
|
func checkStatus(t *testing.T, resp *http.Response, want int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if resp.StatusCode != want {
|
||||||
|
t.Errorf(
|
||||||
|
"StatusCode: %s, want %s",
|
||||||
|
http.StatusText(resp.StatusCode),
|
||||||
|
http.StatusText(want),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkHeader checks the value of a header entry.
|
||||||
|
func checkHeader(t *testing.T, h http.Header, key, want string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if got := h.Get(key); got != want {
|
||||||
|
t.Errorf("%s: %q, want %q", key, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAPIHeader checks common entries set for API endpoints.
|
||||||
|
func checkAPIHeader(t *testing.T, h http.Header) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
|
||||||
|
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
checkHeader(t, h, "Pragma", "no-cache")
|
||||||
|
checkHeader(t, h, "Expires", "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
|
||||||
|
func checkPayloadFunc[T any](
|
||||||
|
t *testing.T,
|
||||||
|
resp *http.Response,
|
||||||
|
f func(got *T) bool,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var got T
|
||||||
|
r := io.Reader(resp.Body)
|
||||||
|
if testing.Verbose() {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
r = io.TeeReader(r, &buf)
|
||||||
|
defer func() { t.Helper(); t.Log(buf.String()) }()
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r).Decode(&got); err != nil {
|
||||||
|
t.Fatalf("Decode: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f(&got) {
|
||||||
|
t.Errorf("Body: %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPayload checks the JSON response of an API endpoint.
|
||||||
|
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
checkPayloadFunc(t, resp, func(got *T) bool {
|
||||||
|
return reflect.DeepEqual(got, &want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkError(t *testing.T, resp *http.Response, error string, code int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
checkStatus(t, resp, code)
|
||||||
|
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
|
||||||
|
t.Errorf("Body: %q, want %q", string(got), error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package pkgserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"maps"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type searchCache map[string]searchCacheEntry
|
||||||
|
type searchResult struct {
|
||||||
|
NameIndices [][]int `json:"name_matches"`
|
||||||
|
DescIndices [][]int `json:"desc_matches,omitempty"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
*metadata
|
||||||
|
}
|
||||||
|
type searchCacheEntry struct {
|
||||||
|
query string
|
||||||
|
results []searchResult
|
||||||
|
expiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
|
||||||
|
query := search
|
||||||
|
if desc {
|
||||||
|
query += ";withDesc"
|
||||||
|
}
|
||||||
|
entry, ok := index.search[query]
|
||||||
|
if ok && len(entry.results) > 0 {
|
||||||
|
return len(entry.results), entry.results[min(i, len(entry.results)-1):min(i+limit, len(entry.results))], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
regex, err := regexp.Compile(search)
|
||||||
|
if err != nil {
|
||||||
|
return 0, make([]searchResult, 0), err
|
||||||
|
}
|
||||||
|
res := make([]searchResult, 0)
|
||||||
|
for p := range maps.Values(index.names) {
|
||||||
|
nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
|
||||||
|
var descIndices [][]int = nil
|
||||||
|
if desc {
|
||||||
|
descIndices = regex.FindAllIndex([]byte(p.Description), -1)
|
||||||
|
}
|
||||||
|
if nameIndices == nil && descIndices == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
|
||||||
|
if desc {
|
||||||
|
score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
|
||||||
|
}
|
||||||
|
res = append(res, searchResult{
|
||||||
|
NameIndices: nameIndices,
|
||||||
|
DescIndices: descIndices,
|
||||||
|
Score: score,
|
||||||
|
metadata: p,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
|
||||||
|
expiry := time.Now().Add(1 * time.Minute)
|
||||||
|
entry = searchCacheEntry{
|
||||||
|
query: search,
|
||||||
|
results: res,
|
||||||
|
expiry: expiry,
|
||||||
|
}
|
||||||
|
index.search[query] = entry
|
||||||
|
|
||||||
|
return len(res), res[i:min(i+limit, len(entry.results))], nil
|
||||||
|
}
|
||||||
|
func (s *searchCache) clean() {
|
||||||
|
maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
|
||||||
|
return v.expiry.Before(time.Now())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func indexsum(in [][]int) int {
|
||||||
|
sum := 0
|
||||||
|
for i := range in {
|
||||||
|
sum += in[i][1] - in[i][0]
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="icon" href="https://hakurei.app/favicon.ico"/>
|
||||||
|
<title>Rosa OS Packages</title>
|
||||||
|
<script src="index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Rosa OS Packages</h1>
|
||||||
|
<div class="top-controls" id="top-controls-regular">
|
||||||
|
<p>Showing entries <span id="entry-counter"></span>.</p>
|
||||||
|
<span id="search-bar">
|
||||||
|
<label for="search">Search: </label>
|
||||||
|
<input type="text" name="search" id="search"/>
|
||||||
|
<button onclick="doSearch()">Find</button>
|
||||||
|
<label for="include-desc">Include descriptions: </label>
|
||||||
|
<input type="checkbox" name="include-desc" id="include-desc" checked/>
|
||||||
|
</span>
|
||||||
|
<div><label for="count">Entries per page: </label><select name="count" id="count">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="20">20</option>
|
||||||
|
<option value="30">30</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</select></div>
|
||||||
|
<div><label for="sort">Sort by: </label><select name="sort" id="sort">
|
||||||
|
<option value="0">Definition (ascending)</option>
|
||||||
|
<option value="1">Definition (descending)</option>
|
||||||
|
<option value="2">Name (ascending)</option>
|
||||||
|
<option value="3">Name (descending)</option>
|
||||||
|
<option value="4">Size (ascending)</option>
|
||||||
|
<option value="5">Size (descending)</option>
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="top-controls" id="search-top-controls" hidden>
|
||||||
|
<p>Showing search results <span id="search-entry-counter"></span> for query "<span id="search-query"></span>".</p>
|
||||||
|
<button onclick="exitSearch()">Back</button>
|
||||||
|
<div><label for="search-count">Entries per page: </label><select name="search-count" id="search-count">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="20">20</option>
|
||||||
|
<option value="30">30</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</select></div>
|
||||||
|
<p>Sorted by best match</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
||||||
|
<table id="pkg-list">
|
||||||
|
<tr><td>Loading...</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
||||||
|
<footer>
|
||||||
|
<p>©<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
|
||||||
|
</footer>
|
||||||
|
<script>main();</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
interface PackageIndexEntry {
|
||||||
|
name: string
|
||||||
|
size?: number
|
||||||
|
description?: string
|
||||||
|
website?: string
|
||||||
|
version?: string
|
||||||
|
report?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryToHTML(entry: PackageIndexEntry | SearchResult): HTMLTableRowElement {
|
||||||
|
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
|
||||||
|
let s = entry.size != null && entry.size > 0 ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
|
||||||
|
let n: string
|
||||||
|
let d: string
|
||||||
|
if ('name_matches' in entry) {
|
||||||
|
n = `<h2>${nameMatches(entry as SearchResult)} ${v}</h2>`
|
||||||
|
} else {
|
||||||
|
n = `<h2>${escapeHtml(entry.name)} ${v}</h2>`
|
||||||
|
}
|
||||||
|
if ('desc_matches' in entry && STATE.getIncludeDescriptions()) {
|
||||||
|
d = descMatches(entry as SearchResult)
|
||||||
|
} else {
|
||||||
|
d = (entry as PackageIndexEntry).description != null ? `<p>${escapeHtml((entry as PackageIndexEntry).description)}</p>` : ""
|
||||||
|
}
|
||||||
|
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
|
||||||
|
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
|
||||||
|
let row = <HTMLTableRowElement>(document.createElement('tr'))
|
||||||
|
row.innerHTML = `<td>
|
||||||
|
${n}
|
||||||
|
${d}
|
||||||
|
${s}
|
||||||
|
${w}
|
||||||
|
${r}
|
||||||
|
</td>`
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
function nameMatches(sr: SearchResult): string {
|
||||||
|
return markMatches(sr.name, sr.name_matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
function descMatches(sr: SearchResult): string {
|
||||||
|
return markMatches(sr.description!, sr.desc_matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
function markMatches(str: string, indices: [number, number][]): string {
|
||||||
|
if (indices == null) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
let out: string = ""
|
||||||
|
let j = 0
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
if (j < indices.length) {
|
||||||
|
if (i === indices[j][0]) {
|
||||||
|
out += `<mark>${escapeHtmlChar(str[i])}`
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (i === indices[j][1]) {
|
||||||
|
out += `</mark>${escapeHtmlChar(str[i])}`
|
||||||
|
j++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += escapeHtmlChar(str[i])
|
||||||
|
}
|
||||||
|
if (indices[j] !== undefined) {
|
||||||
|
out += "</mark>"
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function toByteSizeString(bytes: number): string {
|
||||||
|
if (bytes == null) return `unspecified`
|
||||||
|
if (bytes < 1024) return `${bytes}B`
|
||||||
|
if (bytes < Math.pow(1024, 2)) return `${(bytes / 1024).toFixed(2)}kiB`
|
||||||
|
if (bytes < Math.pow(1024, 3)) return `${(bytes / Math.pow(1024, 2)).toFixed(2)}MiB`
|
||||||
|
if (bytes < Math.pow(1024, 4)) return `${(bytes / Math.pow(1024, 3)).toFixed(2)}GiB`
|
||||||
|
if (bytes < Math.pow(1024, 5)) return `${(bytes / Math.pow(1024, 4)).toFixed(2)}TiB`
|
||||||
|
return "not only is it big, it's large"
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_VERSION = 1
|
||||||
|
const ENDPOINT = `/api/v${API_VERSION}`
|
||||||
|
|
||||||
|
interface InfoPayload {
|
||||||
|
count?: number
|
||||||
|
hakurei_version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function infoRequest(): Promise<InfoPayload> {
|
||||||
|
const res = await fetch(`${ENDPOINT}/info`)
|
||||||
|
const payload = await res.json()
|
||||||
|
return payload as InfoPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetPayload {
|
||||||
|
values?: PackageIndexEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SortOrders {
|
||||||
|
DeclarationAscending,
|
||||||
|
DeclarationDescending,
|
||||||
|
NameAscending,
|
||||||
|
NameDescending
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
|
||||||
|
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
|
||||||
|
const payload = await res.json()
|
||||||
|
return payload as GetPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResult extends PackageIndexEntry {
|
||||||
|
name_matches: [number, number][]
|
||||||
|
desc_matches: [number, number][]
|
||||||
|
score: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchPayload {
|
||||||
|
count?: number
|
||||||
|
values?: SearchResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchRequest(limit: number, index: number, search: string, desc: boolean): Promise<SearchPayload> {
|
||||||
|
const res = await fetch(`${ENDPOINT}/search?limit=${limit}&index=${index}&search=${encodeURIComponent(search)}&desc=${desc}`)
|
||||||
|
if (!res.ok) {
|
||||||
|
exitSearch()
|
||||||
|
alert("invalid search query!")
|
||||||
|
return Promise.reject(res.statusText)
|
||||||
|
}
|
||||||
|
const payload = await res.json()
|
||||||
|
return payload as SearchPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
class State {
|
||||||
|
entriesPerPage: number = 10
|
||||||
|
entryIndex: number = 0
|
||||||
|
maxTotal: number = 0
|
||||||
|
maxEntries: number = 0
|
||||||
|
sort: SortOrders = SortOrders.DeclarationAscending
|
||||||
|
search: boolean = false
|
||||||
|
|
||||||
|
getEntriesPerPage(): number {
|
||||||
|
return this.entriesPerPage
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntriesPerPage(entriesPerPage: number) {
|
||||||
|
this.entriesPerPage = entriesPerPage
|
||||||
|
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntryIndex(): number {
|
||||||
|
return this.entryIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntryIndex(entryIndex: number) {
|
||||||
|
this.entryIndex = entryIndex
|
||||||
|
this.updatePage()
|
||||||
|
this.updateRange()
|
||||||
|
this.updateListings()
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxTotal(): number {
|
||||||
|
return this.maxTotal
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaxTotal(max: number) {
|
||||||
|
this.maxTotal = max
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortOrder(): SortOrders {
|
||||||
|
return this.sort
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortOrder(sortOrder: SortOrders) {
|
||||||
|
this.sort = sortOrder
|
||||||
|
this.setEntryIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePage() {
|
||||||
|
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
|
||||||
|
for (let e of document.getElementsByClassName("page-number")) {
|
||||||
|
(e as HTMLInputElement).value = String(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRange() {
|
||||||
|
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxTotal())
|
||||||
|
document.getElementById("entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxTotal()}`
|
||||||
|
if (this.search) {
|
||||||
|
document.getElementById("search-entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.maxTotal}/${this.maxEntries}`
|
||||||
|
document.getElementById("search-query")!.innerHTML = `<code>${escapeHtml(this.getSearchQuery())}</code>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchQuery(): string {
|
||||||
|
let queryString = document.getElementById("search")!;
|
||||||
|
return (queryString as HTMLInputElement).value
|
||||||
|
}
|
||||||
|
|
||||||
|
getIncludeDescriptions(): boolean {
|
||||||
|
let includeDesc = document.getElementById("include-desc")!;
|
||||||
|
return (includeDesc as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
|
||||||
|
updateListings() {
|
||||||
|
if (this.search) {
|
||||||
|
searchRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSearchQuery(), this.getIncludeDescriptions())
|
||||||
|
.then(res => {
|
||||||
|
let table = document.getElementById("pkg-list")!
|
||||||
|
table.innerHTML = ''
|
||||||
|
for (let row of res.values!) {
|
||||||
|
table.appendChild(entryToHTML(row))
|
||||||
|
}
|
||||||
|
STATE.maxTotal = res.count!
|
||||||
|
STATE.updateRange()
|
||||||
|
if(res.count! < 1) {
|
||||||
|
exitSearch()
|
||||||
|
alert("no results found!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
|
||||||
|
.then(res => {
|
||||||
|
let table = document.getElementById("pkg-list")!
|
||||||
|
table.innerHTML = ''
|
||||||
|
for (let row of res.values!) {
|
||||||
|
table.appendChild(entryToHTML(row))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let STATE: State
|
||||||
|
|
||||||
|
|
||||||
|
function lastPageIndex(): number {
|
||||||
|
return Math.floor(STATE.getMaxTotal() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPage(page: number) {
|
||||||
|
STATE.setEntryIndex(Math.max(0, Math.min(STATE.getEntriesPerPage() * (page - 1), lastPageIndex())))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function escapeHtml(str?: string): string {
|
||||||
|
let out: string = ''
|
||||||
|
if (str == undefined) return ""
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
out += escapeHtmlChar(str[i])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtmlChar(char: string): string {
|
||||||
|
if (char.length != 1) return char
|
||||||
|
switch (char[0]) {
|
||||||
|
case '&':
|
||||||
|
return "&"
|
||||||
|
case '<':
|
||||||
|
return "<"
|
||||||
|
case '>':
|
||||||
|
return ">"
|
||||||
|
case '"':
|
||||||
|
return """
|
||||||
|
case "'":
|
||||||
|
return "'"
|
||||||
|
default:
|
||||||
|
return char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstPage() {
|
||||||
|
STATE.setEntryIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() {
|
||||||
|
let index = STATE.getEntryIndex()
|
||||||
|
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastPage() {
|
||||||
|
STATE.setEntryIndex(lastPageIndex())
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
let index = STATE.getEntryIndex()
|
||||||
|
STATE.setEntryIndex(Math.min(lastPageIndex(), index + STATE.getEntriesPerPage()))
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSearch() {
|
||||||
|
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
||||||
|
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
||||||
|
STATE.search = true;
|
||||||
|
STATE.setEntryIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitSearch() {
|
||||||
|
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
||||||
|
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
||||||
|
STATE.search = false;
|
||||||
|
STATE.setMaxTotal(STATE.maxEntries)
|
||||||
|
STATE.setEntryIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
STATE = new State()
|
||||||
|
infoRequest()
|
||||||
|
.then(res => {
|
||||||
|
STATE.maxEntries = res.count!
|
||||||
|
STATE.setMaxTotal(STATE.maxEntries)
|
||||||
|
document.getElementById("hakurei-version")!.textContent = res.hakurei_version!
|
||||||
|
STATE.updateRange()
|
||||||
|
STATE.updateListings()
|
||||||
|
})
|
||||||
|
for (let e of document.getElementsByClassName("page-number")) {
|
||||||
|
e.addEventListener("change", (_) => {
|
||||||
|
setPage(parseInt((e as HTMLInputElement).value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
document.getElementById("count")?.addEventListener("change", (event) => {
|
||||||
|
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
|
||||||
|
})
|
||||||
|
document.getElementById("sort")?.addEventListener("change", (event) => {
|
||||||
|
STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
|
||||||
|
})
|
||||||
|
document.getElementById("search")?.addEventListener("keyup", (event) => {
|
||||||
|
if (event.key === 'Enter') doSearch()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.page-number {
|
||||||
|
width: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.page-number {
|
||||||
|
width: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html {
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
color: ghostwhite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
html {
|
||||||
|
background-color: #d3d3d3;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2024",
|
||||||
|
"strict": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"outDir": "static"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Package ui holds the static web UI.
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Register arranges for mux to serve the embedded frontend.
|
||||||
|
func Register(mux *http.ServeMux) {
|
||||||
|
mux.Handle("GET /", http.FileServer(http.FS(static)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//go:build frontend
|
||||||
|
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate tsc
|
||||||
|
//go:generate cp index.html style.css static
|
||||||
|
//go:embed static
|
||||||
|
var _static embed.FS
|
||||||
|
|
||||||
|
var static = func() fs.FS {
|
||||||
|
if f, err := fs.Sub(_static, "static"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
}()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !frontend
|
||||||
|
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import "testing/fstest"
|
||||||
|
|
||||||
|
var static fstest.MapFS
|
||||||
+670
-336
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/rosa"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
rosa.Native().DropCaches("", rosa.OptLLVMNoLTO)
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCureAll(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const env = "ROSA_TEST_DAEMON"
|
||||||
|
|
||||||
|
if !testing.Verbose() {
|
||||||
|
t.Skip("verbose flag not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
pathname, ok := os.LookupEnv(env)
|
||||||
|
if !ok {
|
||||||
|
t.Skip(env + " not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := net.UnixAddr{Net: "unix", Name: pathname}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if t.Failed() {
|
||||||
|
if err := abortRemote(t.Context(), &addr, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, handle := range rosa.Native().Collect() {
|
||||||
|
_, a := rosa.Native().Std().MustLoad(handle)
|
||||||
|
t.Run(handle.String(), func(t *testing.T) {
|
||||||
|
_, err := cureRemote(t.Context(), &addr, a, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
||||||
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
|
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
|
||||||
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
|
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
|
||||||
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
||||||
|
|
||||||
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
||||||
|
|||||||
+12
-9
@@ -19,7 +19,6 @@ import (
|
|||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -470,13 +469,14 @@ func _main(s ...string) (exitCode int) {
|
|||||||
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
|
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
|
||||||
))
|
))
|
||||||
|
|
||||||
var setupWriter io.WriteCloser
|
var setupPipe [2]*os.File
|
||||||
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
|
if r, w, err := os.Pipe(); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return 5
|
return 5
|
||||||
} else {
|
} else {
|
||||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
|
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
|
||||||
setupWriter = w
|
z.ExtraFiles = append(z.ExtraFiles, r)
|
||||||
|
setupPipe[0], setupPipe[1] = r, w
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := z.Start(); err != nil {
|
if err := z.Start(); err != nil {
|
||||||
@@ -487,6 +487,9 @@ func _main(s ...string) (exitCode int) {
|
|||||||
}
|
}
|
||||||
return 5
|
return 5
|
||||||
}
|
}
|
||||||
|
if err := setupPipe[0].Close(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
if err := z.Serve(); err != nil {
|
if err := z.Serve(); err != nil {
|
||||||
if m, ok := message.GetMessage(err); ok {
|
if m, ok := message.GetMessage(err); ok {
|
||||||
log.Println(m)
|
log.Println(m)
|
||||||
@@ -496,17 +499,17 @@ func _main(s ...string) (exitCode int) {
|
|||||||
return 5
|
return 5
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
|
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return 5
|
return 5
|
||||||
} else if err = setupWriter.Close(); err != nil {
|
} else if err = setupPipe[1].Close(); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !z.AllowOrphan {
|
if !z.AllowOrphan {
|
||||||
if err := z.Wait(); err != nil {
|
if err := z.Wait(); err != nil {
|
||||||
var exitError *exec.ExitError
|
exitError, ok := errors.AsType[*exec.ExitError](err)
|
||||||
if !errors.As(err, &exitError) || exitError == nil {
|
if !ok || exitError == nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return 5
|
return 5
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,14 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
virtualisation = {
|
virtualisation = {
|
||||||
|
# Hopefully reduces spurious test failures:
|
||||||
|
memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192;
|
||||||
|
|
||||||
diskSize = 6 * 1024;
|
diskSize = 6 * 1024;
|
||||||
|
|
||||||
qemu.options = [
|
qemu.options = [
|
||||||
# Increase test performance:
|
# Increase test performance:
|
||||||
"-smp 8"
|
"-smp 16"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ testers.nixosTest {
|
|||||||
# For go tests:
|
# For go tests:
|
||||||
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
|
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
|
||||||
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
|
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
|
||||||
${fhs}/bin/hakurei-fhs -c 'CC="clang -O3 -Werror" go test ./...'
|
${fhs}/bin/hakurei-fhs -c 'ROSA_SKIP_BINFMT=1 CC="clang -O3 -Werror" go test ./...'
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
//go:build raceattr
|
||||||
|
|
||||||
|
// The raceattr program reproduces vfs inode file attribute race.
|
||||||
|
//
|
||||||
|
// Even though libfuse high-level API presents the address of a struct stat
|
||||||
|
// alongside struct fuse_context, file attributes are actually inherent to the
|
||||||
|
// inode, instead of the specific call from userspace. The kernel implementation
|
||||||
|
// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
|
||||||
|
// call) impossible or very unlikely to reach userspace via the stat family of
|
||||||
|
// syscalls. However, when using default_permissions to have the VFS check
|
||||||
|
// permissions, this race still happens, despite the resulting struct stat being
|
||||||
|
// correct when overriding the check via capabilities otherwise.
|
||||||
|
//
|
||||||
|
// This program reproduces the failure, but because of its continuous nature, it
|
||||||
|
// is provided independent of the vm integration test suite.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStatAs(
|
||||||
|
ctx context.Context, cancel context.CancelFunc,
|
||||||
|
n *atomic.Uint64, ok *atomic.Bool,
|
||||||
|
uid uint32, pathname string,
|
||||||
|
continuous bool,
|
||||||
|
) func() {
|
||||||
|
return func() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_SETUID, uintptr(uid),
|
||||||
|
0, 0,
|
||||||
|
); errno != 0 {
|
||||||
|
cancel()
|
||||||
|
log.Printf("cannot set uid to %d: %s", uid, errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stat syscall.Stat_t
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syscall.Lstat(pathname, &stat); err != nil {
|
||||||
|
// SHAREFS_PERM_DIR not world executable, or
|
||||||
|
// SHAREFS_PERM_REG not world readable
|
||||||
|
if !continuous {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
ok.Store(true)
|
||||||
|
log.Printf("uid %d: %v", uid, err)
|
||||||
|
} else if stat.Uid != uid {
|
||||||
|
// appears to be unreachable
|
||||||
|
if !continuous {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
ok.Store(true)
|
||||||
|
log.Printf("got uid %d instead of %d", stat.Uid, uid)
|
||||||
|
}
|
||||||
|
n.Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix("raceattr: ")
|
||||||
|
|
||||||
|
p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
|
||||||
|
u0 := flag.Int("uid0", 1<<10-1, "first uid")
|
||||||
|
u1 := flag.Int("uid1", 1<<10-2, "second uid")
|
||||||
|
count := flag.Int("count", 1, "threads per uid")
|
||||||
|
continuous := flag.Bool("continuous", false, "keep running even after reproduce")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
log.Fatal("this program must run as root")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(
|
||||||
|
context.Background(),
|
||||||
|
syscall.SIGINT,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
syscall.SIGHUP,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := os.WriteFile(*p, nil, 0); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
n atomic.Uint64
|
||||||
|
ok atomic.Bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if *count < 1 {
|
||||||
|
*count = 1
|
||||||
|
}
|
||||||
|
for range *count {
|
||||||
|
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
|
||||||
|
if *u1 >= 0 {
|
||||||
|
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
if !*continuous && ok.Load() {
|
||||||
|
log.Printf("reproduced after %d calls", n.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -91,8 +91,8 @@ func (n *node) MustParse(arguments []string, handleError func(error)) {
|
|||||||
case ErrEmptyTree:
|
case ErrEmptyTree:
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
default:
|
default:
|
||||||
var flagError FlagError
|
flagError, ok := errors.AsType[FlagError](err)
|
||||||
if !errors.As(err, &flagError) { // returned by HandlerFunc
|
if !ok { // returned by HandlerFunc
|
||||||
handleError(err)
|
handleError(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
|
)
|
||||||
|
|
||||||
|
// escapeBinfmt escapes magic/mask sequences in a [BinfmtEntry].
|
||||||
|
func escapeBinfmt(buf *strings.Builder, s string) string {
|
||||||
|
const lowerhex = "0123456789abcdef"
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
for _, c := range unsafe.Slice(unsafe.StringData(s), len(s)) {
|
||||||
|
switch c {
|
||||||
|
case 0, '\\', ':':
|
||||||
|
buf.WriteString(`\x`)
|
||||||
|
buf.WriteByte(lowerhex[c>>4])
|
||||||
|
buf.WriteByte(lowerhex[c&0xf])
|
||||||
|
|
||||||
|
default:
|
||||||
|
buf.WriteByte(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinfmtEntry is an entry to be registered by the init process.
|
||||||
|
type BinfmtEntry struct {
|
||||||
|
// The offset of the magic/mask in the file, counted in bytes.
|
||||||
|
Offset byte
|
||||||
|
// The byte sequence binfmt_misc is matching for.
|
||||||
|
Magic string
|
||||||
|
// An (optional, defaults to all 0xff) mask.
|
||||||
|
Mask string
|
||||||
|
// The program that should be invoked with the binary as first argument.
|
||||||
|
Interpreter *check.Absolute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid returns whether e can be registered into the kernel.
|
||||||
|
func (e *BinfmtEntry) Valid() bool {
|
||||||
|
return e != nil &&
|
||||||
|
int(e.Offset)+max(len(e.Magic), len(e.Mask)) < 128 &&
|
||||||
|
e.Interpreter != nil && len(e.Interpreter.String()) < 128
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/fhs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEscapeBinfmt(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
magic string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"packed DOS applications", "\x0eDEX", "\x0eDEX"},
|
||||||
|
|
||||||
|
{"riscv64 magic",
|
||||||
|
"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
|
||||||
|
"\x7fELF\x02\x01\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\x02\\x00\xf3\\x00"},
|
||||||
|
{"riscv64 mask",
|
||||||
|
"\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
|
||||||
|
"\xff\xff\xff\xff\xff\xff\xff\\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := escapeBinfmt(new(strings.Builder), tc.magic)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("escapeBinfmt: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBinfmtEntry(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
e BinfmtEntry
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"zero", BinfmtEntry{}, false},
|
||||||
|
{"large offset", BinfmtEntry{Offset: 128}, false},
|
||||||
|
{"long magic", BinfmtEntry{Magic: strings.Repeat("\x00", 128)}, false},
|
||||||
|
{"long mask", BinfmtEntry{Mask: strings.Repeat("\x00", 128)}, false},
|
||||||
|
{"valid", BinfmtEntry{Interpreter: fhs.AbsRoot}, true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if tc.e.Valid() != tc.valid {
|
||||||
|
t.Errorf("Valid: %v", !tc.valid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ const (
|
|||||||
CAP_SETPCAP = 0x8
|
CAP_SETPCAP = 0x8
|
||||||
CAP_NET_ADMIN = 0xc
|
CAP_NET_ADMIN = 0xc
|
||||||
CAP_DAC_OVERRIDE = 0x1
|
CAP_DAC_OVERRIDE = 0x1
|
||||||
|
CAP_SETFCAP = 0x1f
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|||||||
+74
-36
@@ -21,6 +21,7 @@ import (
|
|||||||
"hakurei.app/container/std"
|
"hakurei.app/container/std"
|
||||||
"hakurei.app/ext"
|
"hakurei.app/ext"
|
||||||
"hakurei.app/fhs"
|
"hakurei.app/fhs"
|
||||||
|
"hakurei.app/internal/landlock"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,9 +29,6 @@ const (
|
|||||||
// CancelSignal is the signal expected by container init on context cancel.
|
// CancelSignal is the signal expected by container init on context cancel.
|
||||||
// A custom [Container.Cancel] function must eventually deliver this signal.
|
// A custom [Container.Cancel] function must eventually deliver this signal.
|
||||||
CancelSignal = SIGUSR2
|
CancelSignal = SIGUSR2
|
||||||
|
|
||||||
// Timeout for writing initParams to Container.setup.
|
|
||||||
initSetupTimeout = 5 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -53,7 +51,7 @@ type (
|
|||||||
ExtraFiles []*os.File
|
ExtraFiles []*os.File
|
||||||
|
|
||||||
// Write end of a pipe connected to the init to deliver [Params].
|
// Write end of a pipe connected to the init to deliver [Params].
|
||||||
setup *os.File
|
setup [2]*os.File
|
||||||
// Cancels the context passed to the underlying cmd.
|
// Cancels the context passed to the underlying cmd.
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
// Closed after Wait returns. Keeps the spawning thread alive.
|
// Closed after Wait returns. Keeps the spawning thread alive.
|
||||||
@@ -69,6 +67,9 @@ type (
|
|||||||
// Copied to the underlying [exec.Cmd].
|
// Copied to the underlying [exec.Cmd].
|
||||||
WaitDelay time.Duration
|
WaitDelay time.Duration
|
||||||
|
|
||||||
|
// Suppress verbose output of init.
|
||||||
|
Quiet bool
|
||||||
|
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
msg message.Msg
|
msg message.Msg
|
||||||
@@ -90,12 +91,20 @@ type (
|
|||||||
// Time to wait for processes lingering after the initial process terminates.
|
// Time to wait for processes lingering after the initial process terminates.
|
||||||
AdoptWaitDelay time.Duration
|
AdoptWaitDelay time.Duration
|
||||||
|
|
||||||
|
// Map uid/gid 0 in the init process. Requires [FstypeProc] attached to
|
||||||
|
// [fhs.Proc] in the container filesystem.
|
||||||
|
InitAsRoot bool
|
||||||
// Mapped Uid in user namespace.
|
// Mapped Uid in user namespace.
|
||||||
Uid int
|
Uid int
|
||||||
// Mapped Gid in user namespace.
|
// Mapped Gid in user namespace.
|
||||||
Gid int
|
Gid int
|
||||||
// Hostname value in UTS namespace.
|
// Hostname value in UTS namespace.
|
||||||
Hostname string
|
Hostname string
|
||||||
|
// Register binfmt_misc entries.
|
||||||
|
Binfmt []BinfmtEntry
|
||||||
|
// Alternative pathname to attach binfmt_misc filesystem. The zero value
|
||||||
|
// requires [FstypeProc] to be made available at [fhs.Proc].
|
||||||
|
BinfmtPath *check.Absolute
|
||||||
// Sequential container setup ops.
|
// Sequential container setup ops.
|
||||||
*Ops
|
*Ops
|
||||||
|
|
||||||
@@ -145,11 +154,8 @@ func (e *StartError) Error() string {
|
|||||||
return e.Step
|
return e.Step
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if se, ok := errors.AsType[*os.SyscallError](e.Err); ok && se != nil {
|
||||||
var syscallError *os.SyscallError
|
return e.Step + " " + se.Error()
|
||||||
if errors.As(e.Err, &syscallError) && syscallError != nil {
|
|
||||||
return e.Step + " " + syscallError.Error()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.Step + ": " + e.Err.Error()
|
return e.Step + ": " + e.Err.Error()
|
||||||
@@ -215,6 +221,9 @@ func (p *Container) Start() error {
|
|||||||
if p.cmd.Process != nil {
|
if p.cmd.Process != nil {
|
||||||
return errors.New("container: already started")
|
return errors.New("container: already started")
|
||||||
}
|
}
|
||||||
|
if !p.InitAsRoot && len(p.Binfmt) > 0 {
|
||||||
|
return errors.New("container: init as root required, but not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
if err := ensureCloseOnExec(); err != nil {
|
if err := ensureCloseOnExec(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -285,16 +294,30 @@ func (p *Container) Start() error {
|
|||||||
if !p.HostNet {
|
if !p.HostNet {
|
||||||
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
|
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
|
||||||
}
|
}
|
||||||
|
if p.InitAsRoot {
|
||||||
|
p.cmd.SysProcAttr.AmbientCaps = append(p.cmd.SysProcAttr.AmbientCaps,
|
||||||
|
// mappings during init as root
|
||||||
|
CAP_SETFCAP,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !p.SeccompDisable &&
|
||||||
|
len(p.SeccompRules) == 0 &&
|
||||||
|
p.SeccompPresets&std.PresetDenyNS != 0 {
|
||||||
|
return errors.New("container: as root requires late namespace creation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// place setup pipe before user supplied extra files, this is later restored by init
|
// place setup pipe before user supplied extra files, this is later restored by init
|
||||||
if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
if r, w, err := os.Pipe(); err != nil {
|
||||||
return &StartError{
|
return &StartError{
|
||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "set up params stream",
|
Step: "set up params stream",
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p.setup = f
|
fd := 3 + len(p.cmd.ExtraFiles)
|
||||||
|
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, r)
|
||||||
|
p.setup[0], p.setup[1] = r, w
|
||||||
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||||
}
|
}
|
||||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||||
@@ -308,7 +331,7 @@ func (p *Container) Start() error {
|
|||||||
done <- func() error {
|
done <- func() error {
|
||||||
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
|
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
|
||||||
// created from the calling thread
|
// created from the calling thread
|
||||||
if err := SetNoNewPrivs(); err != nil {
|
if err := setNoNewPrivs(); err != nil {
|
||||||
return &StartError{
|
return &StartError{
|
||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||||
@@ -318,15 +341,17 @@ func (p *Container) Start() error {
|
|||||||
|
|
||||||
// landlock: depends on per-thread state but acts on a process group
|
// landlock: depends on per-thread state but acts on a process group
|
||||||
{
|
{
|
||||||
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
|
rulesetAttr := &landlock.RulesetAttr{
|
||||||
|
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||||
|
}
|
||||||
if !p.HostAbstract {
|
if !p.HostAbstract {
|
||||||
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||||
}
|
}
|
||||||
|
|
||||||
if abi, err := LandlockGetABI(); err != nil {
|
if abi, err := landlock.GetABI(); err != nil {
|
||||||
if p.HostAbstract {
|
if p.HostAbstract || !p.HostNet {
|
||||||
// landlock can be skipped here as it restricts access
|
// landlock can be skipped here as it restricts access
|
||||||
// to resources already covered by namespaces (pid)
|
// to resources already covered by namespaces (pid, net)
|
||||||
goto landlockOut
|
goto landlockOut
|
||||||
}
|
}
|
||||||
return &StartError{Step: "get landlock ABI", Err: err}
|
return &StartError{Step: "get landlock ABI", Err: err}
|
||||||
@@ -340,8 +365,6 @@ func (p *Container) Start() error {
|
|||||||
Err: ENOSYS,
|
Err: ENOSYS,
|
||||||
Origin: true,
|
Origin: true,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
p.msg.Verbosef("landlock abi version %d", abi)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
||||||
@@ -351,8 +374,7 @@ func (p *Container) Start() error {
|
|||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
|
||||||
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
|
||||||
_ = Close(rulesetFd)
|
_ = Close(rulesetFd)
|
||||||
return &StartError{
|
return &StartError{
|
||||||
Fatal: true,
|
Fatal: true,
|
||||||
@@ -408,7 +430,6 @@ func (p *Container) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.msg.Verbose("starting container init")
|
|
||||||
if err := p.cmd.Start(); err != nil {
|
if err := p.cmd.Start(); err != nil {
|
||||||
return &StartError{
|
return &StartError{
|
||||||
Step: "start container init",
|
Step: "start container init",
|
||||||
@@ -428,24 +449,33 @@ func (p *Container) Start() error {
|
|||||||
// Serve serves [Container.Params] to the container init.
|
// Serve serves [Container.Params] to the container init.
|
||||||
//
|
//
|
||||||
// Serve must only be called once.
|
// Serve must only be called once.
|
||||||
func (p *Container) Serve() error {
|
func (p *Container) Serve() (err error) {
|
||||||
if p.setup == nil {
|
if p.setup[0] == nil || p.setup[1] == nil {
|
||||||
panic("invalid serve")
|
panic("invalid serve")
|
||||||
}
|
}
|
||||||
|
|
||||||
setup := p.setup
|
done := make(chan struct{})
|
||||||
p.setup = nil
|
defer func() {
|
||||||
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
|
if closeErr := p.setup[1].Close(); err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
p.cancel()
|
||||||
|
}
|
||||||
|
close(done)
|
||||||
|
p.setup[0], p.setup[1] = nil, nil
|
||||||
|
}()
|
||||||
|
if err = p.setup[0].Close(); err != nil {
|
||||||
return &StartError{
|
return &StartError{
|
||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "set init pipe deadline",
|
Step: "close read end of init pipe",
|
||||||
Err: err,
|
Err: err,
|
||||||
Passthrough: true,
|
Passthrough: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Path == nil {
|
if p.Path == nil {
|
||||||
p.cancel()
|
|
||||||
return &StartError{
|
return &StartError{
|
||||||
Step: "invalid executable pathname",
|
Step: "invalid executable pathname",
|
||||||
Err: EINVAL,
|
Err: EINVAL,
|
||||||
@@ -461,18 +491,26 @@ func (p *Container) Serve() error {
|
|||||||
p.SeccompRules = make([]std.NativeRule, 0)
|
p.SeccompRules = make([]std.NativeRule, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := gob.NewEncoder(setup).Encode(&initParams{
|
t := time.Now().UTC()
|
||||||
|
go func(f *os.File) {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
if cancelErr := f.SetWriteDeadline(t); cancelErr != nil {
|
||||||
|
p.msg.Verbose(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(p.setup[1])
|
||||||
|
|
||||||
|
return gob.NewEncoder(p.setup[1]).Encode(&initParams{
|
||||||
p.Params,
|
p.Params,
|
||||||
Getuid(),
|
Getuid(),
|
||||||
Getgid(),
|
Getgid(),
|
||||||
len(p.ExtraFiles),
|
len(p.ExtraFiles),
|
||||||
p.msg.IsVerbose(),
|
p.msg.IsVerbose() && !p.Quiet,
|
||||||
})
|
})
|
||||||
_ = setup.Close()
|
|
||||||
if err != nil {
|
|
||||||
p.cancel()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait blocks until the container init process to exit and releases any
|
// Wait blocks until the container init process to exit and releases any
|
||||||
|
|||||||
+218
-91
@@ -17,6 +17,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/check"
|
||||||
"hakurei.app/command"
|
"hakurei.app/command"
|
||||||
@@ -26,6 +27,9 @@ import (
|
|||||||
"hakurei.app/ext"
|
"hakurei.app/ext"
|
||||||
"hakurei.app/fhs"
|
"hakurei.app/fhs"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/info"
|
||||||
|
"hakurei.app/internal/landlock"
|
||||||
|
"hakurei.app/internal/params"
|
||||||
"hakurei.app/ldd"
|
"hakurei.app/ldd"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
"hakurei.app/vfs"
|
"hakurei.app/vfs"
|
||||||
@@ -84,9 +88,9 @@ func TestStartError(t *testing.T) {
|
|||||||
{"params env", &container.StartError{
|
{"params env", &container.StartError{
|
||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "set up params stream",
|
Step: "set up params stream",
|
||||||
Err: container.ErrReceiveEnv,
|
Err: params.ErrReceiveEnv,
|
||||||
}, "set up params stream: environment variable not set",
|
}, "set up params stream: environment variable not set",
|
||||||
container.ErrReceiveEnv, syscall.EBADF,
|
params.ErrReceiveEnv, syscall.EBADF,
|
||||||
"cannot set up params stream: environment variable not set"},
|
"cannot set up params stream: environment variable not set"},
|
||||||
|
|
||||||
{"params", &container.StartError{
|
{"params", &container.StartError{
|
||||||
@@ -231,6 +235,9 @@ func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*v
|
|||||||
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
|
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:linkname toHost hakurei.app/container.toHost
|
||||||
|
func toHost(name string) string
|
||||||
|
|
||||||
var containerTestCases = []struct {
|
var containerTestCases = []struct {
|
||||||
name string
|
name string
|
||||||
filter bool
|
filter bool
|
||||||
@@ -330,13 +337,15 @@ var containerTestCases = []struct {
|
|||||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||||
return []*vfs.MountInfoEntry{
|
return []*vfs.MountInfoEntry{
|
||||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
||||||
"rw,lowerdir="+
|
"rw"+
|
||||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
|
",lowerdir+="+
|
||||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
|
||||||
|
",lowerdir+="+
|
||||||
|
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
||||||
",upperdir="+
|
",upperdir="+
|
||||||
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
|
toHost(ctx.Value(testVal("upper")).(*check.Absolute).String())+
|
||||||
",workdir="+
|
",workdir="+
|
||||||
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*check.Absolute).String())+
|
toHost(ctx.Value(testVal("work")).(*check.Absolute).String())+
|
||||||
",redirect_dir=nofollow,uuid=on,userxattr"),
|
",redirect_dir=nofollow,uuid=on,userxattr"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -386,9 +395,11 @@ var containerTestCases = []struct {
|
|||||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||||
return []*vfs.MountInfoEntry{
|
return []*vfs.MountInfoEntry{
|
||||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
||||||
"ro,lowerdir="+
|
"ro"+
|
||||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
|
",lowerdir+="+
|
||||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
|
||||||
|
",lowerdir+="+
|
||||||
|
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
||||||
",redirect_dir=nofollow,userxattr"),
|
",redirect_dir=nofollow,userxattr"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -398,49 +409,18 @@ var containerTestCases = []struct {
|
|||||||
func TestContainer(t *testing.T) {
|
func TestContainer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
|
var suffix string
|
||||||
wantErr := context.Canceled
|
runTests:
|
||||||
wantExitCode := 0
|
|
||||||
if err := c.Wait(); !reflect.DeepEqual(err, wantErr) {
|
|
||||||
if m, ok := container.InternalMessageFromError(err); ok {
|
|
||||||
t.Error(m)
|
|
||||||
}
|
|
||||||
t.Errorf("Wait: error = %#v, want %#v", err, wantErr)
|
|
||||||
}
|
|
||||||
if ps := c.ProcessState(); ps == nil {
|
|
||||||
t.Errorf("ProcessState unexpectedly returned nil")
|
|
||||||
} else if code := ps.ExitCode(); code != wantExitCode {
|
|
||||||
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
t.Run("forward", testContainerCancel(func(c *container.Container) {
|
|
||||||
c.ForwardCancel = true
|
|
||||||
}, func(t *testing.T, c *container.Container) {
|
|
||||||
var exitError *exec.ExitError
|
|
||||||
if err := c.Wait(); !errors.As(err, &exitError) {
|
|
||||||
if m, ok := container.InternalMessageFromError(err); ok {
|
|
||||||
t.Error(m)
|
|
||||||
}
|
|
||||||
t.Errorf("Wait: error = %v", err)
|
|
||||||
}
|
|
||||||
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
|
||||||
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
for i, tc := range containerTestCases {
|
for i, tc := range containerTestCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
_suffix := suffix
|
||||||
|
t.Run(tc.name+_suffix, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
wantOps, wantOpsCtx := tc.ops(t)
|
wantOps, wantOpsCtx := tc.ops(t)
|
||||||
wantMnt := tc.mnt(t, wantOpsCtx)
|
wantMnt := tc.mnt(t, wantOpsCtx)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var libPaths []*check.Absolute
|
var libPaths []*check.Absolute
|
||||||
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
|
c := helperNewContainerLibPaths(t.Context(), &libPaths, "container", strconv.Itoa(i))
|
||||||
c.Uid = tc.uid
|
c.Uid = tc.uid
|
||||||
c.Gid = tc.gid
|
c.Gid = tc.gid
|
||||||
c.Hostname = hostnameFromTestCase(tc.name)
|
c.Hostname = hostnameFromTestCase(tc.name)
|
||||||
@@ -450,7 +430,6 @@ func TestContainer(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||||
}
|
}
|
||||||
c.WaitDelay = helperDefaultTimeout
|
|
||||||
*c.Ops = append(*c.Ops, *wantOps...)
|
*c.Ops = append(*c.Ops, *wantOps...)
|
||||||
c.SeccompRules = tc.rules
|
c.SeccompRules = tc.rules
|
||||||
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
|
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
|
||||||
@@ -458,6 +437,20 @@ func TestContainer(t *testing.T) {
|
|||||||
c.SeccompDisable = !tc.filter
|
c.SeccompDisable = !tc.filter
|
||||||
c.RetainSession = tc.session
|
c.RetainSession = tc.session
|
||||||
c.HostNet = tc.net
|
c.HostNet = tc.net
|
||||||
|
c.InitAsRoot = _suffix != ""
|
||||||
|
c.Env = append(c.Env, "HAKUREI_TEST_SUFFIX="+_suffix)
|
||||||
|
if info.CanDegrade {
|
||||||
|
if _, err := landlock.GetABI(); err != nil {
|
||||||
|
if !errors.Is(err, syscall.ENOSYS) {
|
||||||
|
t.Fatalf("LandlockGetABI: error = %v", err)
|
||||||
|
}
|
||||||
|
c.HostAbstract = true
|
||||||
|
t.Log("Landlock LSM is unavailable, enabling HostAbstract")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.InitAsRoot {
|
||||||
|
c.SeccompPresets &= ^std.PresetDenyNS
|
||||||
|
}
|
||||||
|
|
||||||
c.
|
c.
|
||||||
Readonly(check.MustAbs(pathReadonly), 0755).
|
Readonly(check.MustAbs(pathReadonly), 0755).
|
||||||
@@ -526,6 +519,11 @@ func TestContainer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if suffix == "" {
|
||||||
|
suffix = " as root"
|
||||||
|
goto runTests
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
|
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
|
||||||
@@ -548,50 +546,118 @@ func hostnameFromTestCase(name string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testContainerCancel(
|
func testContainerCancel(
|
||||||
|
t *testing.T,
|
||||||
containerExtra func(c *container.Container),
|
containerExtra func(c *container.Container),
|
||||||
waitCheck func(t *testing.T, c *container.Container),
|
waitCheck func(ps *os.ProcessState, waitErr error),
|
||||||
) func(t *testing.T) {
|
) {
|
||||||
return func(t *testing.T) {
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
t.Parallel()
|
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
|
||||||
|
|
||||||
c := helperNewContainer(ctx, "block")
|
c := helperNewContainer(ctx, "block")
|
||||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||||
c.WaitDelay = helperDefaultTimeout
|
if containerExtra != nil {
|
||||||
if containerExtra != nil {
|
containerExtra(c)
|
||||||
containerExtra(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
ready := make(chan struct{})
|
|
||||||
if r, w, err := os.Pipe(); err != nil {
|
|
||||||
t.Fatalf("cannot pipe: %v", err)
|
|
||||||
} else {
|
|
||||||
c.ExtraFiles = append(c.ExtraFiles, w)
|
|
||||||
go func() {
|
|
||||||
defer close(ready)
|
|
||||||
if _, err = r.Read(make([]byte, 1)); err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Start(); err != nil {
|
|
||||||
if m, ok := container.InternalMessageFromError(err); ok {
|
|
||||||
t.Fatal(m)
|
|
||||||
} else {
|
|
||||||
t.Fatalf("cannot start container: %v", err)
|
|
||||||
}
|
|
||||||
} else if err = c.Serve(); err != nil {
|
|
||||||
if m, ok := container.InternalMessageFromError(err); ok {
|
|
||||||
t.Error(m)
|
|
||||||
} else {
|
|
||||||
t.Errorf("cannot serve setup params: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<-ready
|
|
||||||
cancel()
|
|
||||||
waitCheck(t, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ready := make(chan struct{})
|
||||||
|
var waitErr error
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot pipe: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ExtraFiles = append(c.ExtraFiles, w)
|
||||||
|
go func() {
|
||||||
|
defer close(ready)
|
||||||
|
if _, _err := r.Read(make([]byte, 1)); _err != nil {
|
||||||
|
panic(_err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = c.Start(); err != nil {
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Fatal(m)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("cannot start container: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
waitErr = c.Wait()
|
||||||
|
_ = r.SetReadDeadline(time.Now())
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = c.Serve(); err != nil {
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Error(m)
|
||||||
|
} else {
|
||||||
|
t.Errorf("cannot serve setup params: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<-ready
|
||||||
|
cancel()
|
||||||
|
<-done
|
||||||
|
waitCheck(c.ProcessState(), waitErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForward(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
f := func(ps *os.ProcessState, waitErr error) {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if !errors.As(waitErr, &exitError) {
|
||||||
|
if m, ok := container.InternalMessageFromError(waitErr); ok {
|
||||||
|
t.Error(m)
|
||||||
|
}
|
||||||
|
t.Errorf("Wait: error = %v", waitErr)
|
||||||
|
}
|
||||||
|
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
||||||
|
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Run("direct", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
testContainerCancel(t, func(c *container.Container) {
|
||||||
|
c.ForwardCancel = true
|
||||||
|
}, f)
|
||||||
|
})
|
||||||
|
t.Run("as root", func(t *testing.T) {
|
||||||
|
testContainerCancel(t, func(c *container.Container) {
|
||||||
|
c.ForwardCancel = true
|
||||||
|
c.InitAsRoot = true
|
||||||
|
c.Proc(fhs.AbsProc)
|
||||||
|
}, f)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCancel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
f := func(ps *os.ProcessState, waitErr error) {
|
||||||
|
wantErr := context.Canceled
|
||||||
|
if !reflect.DeepEqual(waitErr, wantErr) {
|
||||||
|
if m, ok := container.InternalMessageFromError(waitErr); ok {
|
||||||
|
t.Error(m)
|
||||||
|
}
|
||||||
|
t.Errorf("Wait: error = %#v, want %#v", waitErr, wantErr)
|
||||||
|
}
|
||||||
|
if ps == nil {
|
||||||
|
t.Errorf("ProcessState unexpectedly returned nil")
|
||||||
|
} else if code := ps.ExitCode(); code != 0 {
|
||||||
|
t.Errorf("ExitCode: %d, want %d", code, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Run("direct", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
testContainerCancel(t, nil, f)
|
||||||
|
})
|
||||||
|
t.Run("as root", func(t *testing.T) {
|
||||||
|
testContainerCancel(t, func(c *container.Container) {
|
||||||
|
c.InitAsRoot = true
|
||||||
|
c.Proc(fhs.AbsProc)
|
||||||
|
}, f)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainerString(t *testing.T) {
|
func TestContainerString(t *testing.T) {
|
||||||
@@ -627,6 +693,8 @@ func init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
c.Command("container", command.UsageInternal, func(args []string) error {
|
c.Command("container", command.UsageInternal, func(args []string) error {
|
||||||
|
asRoot := os.Getenv("HAKUREI_TEST_SUFFIX") == " as root"
|
||||||
|
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return syscall.EINVAL
|
return syscall.EINVAL
|
||||||
}
|
}
|
||||||
@@ -644,6 +712,66 @@ func init() {
|
|||||||
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
|
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// no attack surface increase during as root due to no_new_privs
|
||||||
|
var wantBounding uintptr = 1
|
||||||
|
asRootNot := " not"
|
||||||
|
if !asRoot {
|
||||||
|
wantBounding = 0
|
||||||
|
asRootNot = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
PR_CAP_AMBIENT = 0x2f
|
||||||
|
PR_CAP_AMBIENT_IS_SET = 0x1
|
||||||
|
)
|
||||||
|
for i := range container.LastCap(nil) + 1 {
|
||||||
|
r, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_PRCTL,
|
||||||
|
PR_CAP_AMBIENT,
|
||||||
|
PR_CAP_AMBIENT_IS_SET,
|
||||||
|
i,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return os.NewSyscallError("prctl", errno)
|
||||||
|
}
|
||||||
|
if r != 0 {
|
||||||
|
return fmt.Errorf("capability %d in ambient set", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, _, errno = syscall.Syscall(
|
||||||
|
syscall.SYS_PRCTL,
|
||||||
|
syscall.PR_CAPBSET_READ,
|
||||||
|
i,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return os.NewSyscallError("prctl", errno)
|
||||||
|
}
|
||||||
|
if r != wantBounding {
|
||||||
|
return fmt.Errorf("capability %d%s in bounding set", i, asRootNot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
||||||
|
var capData struct {
|
||||||
|
effective uint32
|
||||||
|
permitted uint32
|
||||||
|
inheritable uint32
|
||||||
|
}
|
||||||
|
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&struct {
|
||||||
|
version uint32
|
||||||
|
pid int32
|
||||||
|
}{_LINUX_CAPABILITY_VERSION_3, 0})), uintptr(unsafe.Pointer(&capData)), 0); errno != 0 {
|
||||||
|
return os.NewSyscallError("capget", errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
if max(capData.effective, capData.permitted, capData.inheritable) != 0 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"effective = %d, permitted = %d, inheritable = %d",
|
||||||
|
capData.effective, capData.permitted, capData.inheritable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
wantHost := hostnameFromTestCase(tc.name)
|
wantHost := hostnameFromTestCase(tc.name)
|
||||||
if host, err := os.Hostname(); err != nil {
|
if host, err := os.Hostname(); err != nil {
|
||||||
return fmt.Errorf("cannot get hostname: %v", err)
|
return fmt.Errorf("cannot get hostname: %v", err)
|
||||||
@@ -738,8 +866,7 @@ func init() {
|
|||||||
const (
|
const (
|
||||||
envDoCheck = "HAKUREI_TEST_DO_CHECK"
|
envDoCheck = "HAKUREI_TEST_DO_CHECK"
|
||||||
|
|
||||||
helperDefaultTimeout = 5 * time.Second
|
helperInnerPath = "/usr/bin/helper"
|
||||||
helperInnerPath = "/usr/bin/helper"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -762,7 +889,7 @@ func TestMain(m *testing.M) {
|
|||||||
}
|
}
|
||||||
c.MustParse(os.Args[1:], func(err error) {
|
c.MustParse(os.Args[1:], func(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
+10
-4
@@ -16,6 +16,7 @@ import (
|
|||||||
"hakurei.app/container/std"
|
"hakurei.app/container/std"
|
||||||
"hakurei.app/ext"
|
"hakurei.app/ext"
|
||||||
"hakurei.app/internal/netlink"
|
"hakurei.app/internal/netlink"
|
||||||
|
"hakurei.app/internal/params"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ type syscallDispatcher interface {
|
|||||||
// isatty provides [Isatty].
|
// isatty provides [Isatty].
|
||||||
isatty(fd int) bool
|
isatty(fd int) bool
|
||||||
// receive provides [Receive].
|
// receive provides [Receive].
|
||||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
||||||
|
|
||||||
// bindMount provides procPaths.bindMount.
|
// bindMount provides procPaths.bindMount.
|
||||||
bindMount(msg message.Msg, source, target string, flags uintptr) error
|
bindMount(msg message.Msg, source, target string, flags uintptr) error
|
||||||
@@ -64,6 +65,8 @@ type syscallDispatcher interface {
|
|||||||
remount(msg message.Msg, target string, flags uintptr) error
|
remount(msg message.Msg, target string, flags uintptr) error
|
||||||
// mountTmpfs provides mountTmpfs.
|
// mountTmpfs provides mountTmpfs.
|
||||||
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
|
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
|
||||||
|
// mountOverlay provides mountOverlay.
|
||||||
|
mountOverlay(target string, options [][2]string) error
|
||||||
// ensureFile provides ensureFile.
|
// ensureFile provides ensureFile.
|
||||||
ensureFile(name string, perm, pperm os.FileMode) error
|
ensureFile(name string, perm, pperm os.FileMode) error
|
||||||
// mustLoopback provides mustLoopback.
|
// mustLoopback provides mustLoopback.
|
||||||
@@ -147,7 +150,7 @@ func (direct) lockOSThread() { runtime.LockOSThread() }
|
|||||||
|
|
||||||
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
|
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
|
||||||
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
||||||
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
|
func (direct) setNoNewPrivs() error { return setNoNewPrivs() }
|
||||||
|
|
||||||
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
|
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
|
||||||
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
||||||
@@ -155,8 +158,8 @@ func (direct) capBoundingSetDrop(cap uintptr) error { return capBound
|
|||||||
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
||||||
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
||||||
func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
|
func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
|
||||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
||||||
return Receive(key, e, fdp)
|
return params.Receive(key, e, fdp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
|
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
|
||||||
@@ -168,6 +171,9 @@ func (direct) remount(msg message.Msg, target string, flags uintptr) error {
|
|||||||
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||||
return mountTmpfs(k, fsname, target, flags, size, perm)
|
return mountTmpfs(k, fsname, target, flags, size, perm)
|
||||||
}
|
}
|
||||||
|
func (k direct) mountOverlay(target string, options [][2]string) error {
|
||||||
|
return mountOverlay(target, options)
|
||||||
|
}
|
||||||
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
|
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||||
return ensureFile(name, perm, pperm)
|
return ensureFile(name, perm, pperm)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,8 +235,6 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func sliceAddr[S any](s []S) *[]S { return &s }
|
|
||||||
|
|
||||||
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
|
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
|
||||||
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
|
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
|
||||||
// check happens in Close, and cleanup is not guaranteed to run, so relying
|
// check happens in Close, and cleanup is not guaranteed to run, so relying
|
||||||
@@ -390,7 +388,7 @@ func (k *kstub) isatty(fd int) bool {
|
|||||||
return expect.Ret.(bool)
|
return expect.Ret.(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
expect := k.Expects("receive")
|
expect := k.Expects("receive")
|
||||||
|
|
||||||
@@ -408,10 +406,17 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// avoid changing test cases
|
||||||
|
var fdpComp *uintptr
|
||||||
|
if fdp != nil {
|
||||||
|
fdpComp = new(uintptr(*fdp))
|
||||||
|
}
|
||||||
|
|
||||||
err = expect.Error(
|
err = expect.Error(
|
||||||
stub.CheckArg(k.Stub, "key", key, 0),
|
stub.CheckArg(k.Stub, "key", key, 0),
|
||||||
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
||||||
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
|
stub.CheckArgReflect(k.Stub, "fdp", fdpComp, 2))
|
||||||
|
|
||||||
// 3 is unused so stores params
|
// 3 is unused so stores params
|
||||||
if expect.Args[3] != nil {
|
if expect.Args[3] != nil {
|
||||||
@@ -426,7 +431,7 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
|||||||
if expect.Args[4] != nil {
|
if expect.Args[4] != nil {
|
||||||
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
|
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
|
||||||
if fdp != nil {
|
if fdp != nil {
|
||||||
*fdp = v
|
*fdp = int(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,6 +466,14 @@ func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm
|
|||||||
stub.CheckArg(k.Stub, "perm", perm, 4))
|
stub.CheckArg(k.Stub, "perm", perm, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (k *kstub) mountOverlay(target string, options [][2]string) error {
|
||||||
|
k.Helper()
|
||||||
|
return k.Expects("mountOverlay").Error(
|
||||||
|
stub.CheckArg(k.Stub, "target", target, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "options", options, 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
return k.Expects("ensureFile").Error(
|
return k.Expects("ensureFile").Error(
|
||||||
|
|||||||
+10
-8
@@ -46,9 +46,8 @@ func messageFromError(err error) (m string, ok bool) {
|
|||||||
// While this is usable for pointer errors, such use should be avoided as nil
|
// While this is usable for pointer errors, such use should be avoided as nil
|
||||||
// check is omitted.
|
// check is omitted.
|
||||||
func messagePrefix[T error](prefix string, err error) (string, bool) {
|
func messagePrefix[T error](prefix string, err error) (string, bool) {
|
||||||
var targetError T
|
if e, ok := errors.AsType[T](err); ok {
|
||||||
if errors.As(err, &targetError) {
|
return prefix + e.Error(), true
|
||||||
return prefix + targetError.Error(), true
|
|
||||||
}
|
}
|
||||||
return zeroString, false
|
return zeroString, false
|
||||||
}
|
}
|
||||||
@@ -58,9 +57,8 @@ func messagePrefixP[V any, T interface {
|
|||||||
*V
|
*V
|
||||||
error
|
error
|
||||||
}](prefix string, err error) (string, bool) {
|
}](prefix string, err error) (string, bool) {
|
||||||
var targetError T
|
if e, ok := errors.AsType[T](err); ok && e != nil {
|
||||||
if errors.As(err, &targetError) && targetError != nil {
|
return prefix + e.Error(), true
|
||||||
return prefix + targetError.Error(), true
|
|
||||||
}
|
}
|
||||||
return zeroString, false
|
return zeroString, false
|
||||||
}
|
}
|
||||||
@@ -109,8 +107,8 @@ func optionalErrorUnwrap(err error) error {
|
|||||||
|
|
||||||
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
|
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
|
||||||
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
||||||
var errno syscall.Errno
|
errno, ok := errors.AsType[syscall.Errno](err)
|
||||||
if !errors.As(err, &errno) {
|
if !ok {
|
||||||
return 0, &os.PathError{Op: op, Path: path, Err: err}
|
return 0, &os.PathError{Op: op, Path: path, Err: err}
|
||||||
}
|
}
|
||||||
return errno, nil
|
return errno, nil
|
||||||
@@ -118,6 +116,10 @@ func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
|||||||
|
|
||||||
// mount wraps syscall.Mount for error handling.
|
// mount wraps syscall.Mount for error handling.
|
||||||
func mount(source, target, fstype string, flags uintptr, data string) error {
|
func mount(source, target, fstype string, flags uintptr, data string) error {
|
||||||
|
if max(len(source), len(target), len(data))+1 > os.Getpagesize() {
|
||||||
|
return &MountError{source, target, fstype, flags, data, syscall.ENOMEM}
|
||||||
|
}
|
||||||
|
|
||||||
err := syscall.Mount(source, target, fstype, flags, data)
|
err := syscall.Mount(source, target, fstype, flags, data)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+139
-57
@@ -11,14 +11,17 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
. "syscall"
|
. "syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/check"
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/ext"
|
"hakurei.app/ext"
|
||||||
"hakurei.app/fhs"
|
"hakurei.app/fhs"
|
||||||
|
"hakurei.app/internal/params"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,35 +150,33 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
params initParams
|
param initParams
|
||||||
closeSetup func() error
|
closeSetup func() error
|
||||||
setupFd uintptr
|
setupFd int
|
||||||
offsetSetup int
|
|
||||||
)
|
)
|
||||||
if f, err := k.receive(setupEnv, ¶ms, &setupFd); err != nil {
|
if f, err := k.receive(setupEnv, ¶m, &setupFd); err != nil {
|
||||||
if errors.Is(err, EBADF) {
|
if errors.Is(err, EBADF) {
|
||||||
k.fatal(msg, "invalid setup descriptor")
|
k.fatal(msg, "invalid setup descriptor")
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrReceiveEnv) {
|
if errors.Is(err, params.ErrReceiveEnv) {
|
||||||
k.fatal(msg, setupEnv+" not set")
|
k.fatal(msg, setupEnv+" not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
k.fatalf(msg, "cannot decode init setup payload: %v", err)
|
k.fatalf(msg, "cannot decode init setup payload: %v", err)
|
||||||
} else {
|
} else {
|
||||||
if params.Ops == nil {
|
if param.Ops == nil {
|
||||||
k.fatal(msg, "invalid setup parameters")
|
k.fatal(msg, "invalid setup parameters")
|
||||||
}
|
}
|
||||||
if params.ParentPerm == 0 {
|
if param.ParentPerm == 0 {
|
||||||
params.ParentPerm = 0755
|
param.ParentPerm = 0755
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.SwapVerbose(params.Verbose)
|
msg.SwapVerbose(param.Verbose)
|
||||||
msg.Verbose("received setup parameters")
|
msg.Verbose("received setup parameters")
|
||||||
closeSetup = f
|
closeSetup = f
|
||||||
offsetSetup = int(setupFd + 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !params.HostNet {
|
if !param.HostNet {
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
|
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
|
||||||
os.Interrupt, SIGTERM, SIGQUIT)
|
os.Interrupt, SIGTERM, SIGQUIT)
|
||||||
defer cancel() // for panics
|
defer cancel() // for panics
|
||||||
@@ -183,23 +184,33 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uid, gid := param.Uid, param.Gid
|
||||||
|
if param.InitAsRoot {
|
||||||
|
uid, gid = 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
// write uid/gid map here so parent does not need to set dumpable
|
// write uid/gid map here so parent does not need to set dumpable
|
||||||
if err := k.setDumpable(ext.SUID_DUMP_USER); err != nil {
|
if err := k.setDumpable(ext.SUID_DUMP_USER); err != nil {
|
||||||
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
|
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
|
||||||
}
|
}
|
||||||
if err := k.writeFile(fhs.Proc+"self/uid_map",
|
if err := k.writeFile(
|
||||||
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
|
fhs.Proc+"self/uid_map",
|
||||||
0); err != nil {
|
[]byte(strconv.Itoa(uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"),
|
||||||
|
0,
|
||||||
|
); err != nil {
|
||||||
k.fatalf(msg, "%v", err)
|
k.fatalf(msg, "%v", err)
|
||||||
}
|
}
|
||||||
if err := k.writeFile(fhs.Proc+"self/setgroups",
|
if err := k.writeFile(
|
||||||
|
fhs.Proc+"self/setgroups",
|
||||||
[]byte("deny\n"),
|
[]byte("deny\n"),
|
||||||
0); err != nil && !os.IsNotExist(err) {
|
0,
|
||||||
|
); err != nil && !os.IsNotExist(err) {
|
||||||
k.fatalf(msg, "%v", err)
|
k.fatalf(msg, "%v", err)
|
||||||
}
|
}
|
||||||
if err := k.writeFile(fhs.Proc+"self/gid_map",
|
if err := k.writeFile(fhs.Proc+"self/gid_map",
|
||||||
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
|
[]byte(strconv.Itoa(gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"),
|
||||||
0); err != nil {
|
0,
|
||||||
|
); err != nil {
|
||||||
k.fatalf(msg, "%v", err)
|
k.fatalf(msg, "%v", err)
|
||||||
}
|
}
|
||||||
if err := k.setDumpable(ext.SUID_DUMP_DISABLE); err != nil {
|
if err := k.setDumpable(ext.SUID_DUMP_DISABLE); err != nil {
|
||||||
@@ -207,8 +218,8 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
oldmask := k.umask(0)
|
oldmask := k.umask(0)
|
||||||
if params.Hostname != "" {
|
if param.Hostname != "" {
|
||||||
if err := k.sethostname([]byte(params.Hostname)); err != nil {
|
if err := k.sethostname([]byte(param.Hostname)); err != nil {
|
||||||
k.fatalf(msg, "cannot set hostname: %v", err)
|
k.fatalf(msg, "cannot set hostname: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,15 +232,32 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶ms.Params, Msg: msg, Context: ctx}
|
state := &setupState{process: make(map[int]WaitStatus), Params: ¶m.Params, Msg: msg, Context: ctx}
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
|
||||||
|
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
|
||||||
|
}
|
||||||
|
if err := k.chdir(intermediateHostPath); err != nil {
|
||||||
|
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(param.Binfmt) > 0 {
|
||||||
|
for i, e := range param.Binfmt {
|
||||||
|
if pathname, err := k.evalSymlinks(e.Interpreter.String()); err != nil {
|
||||||
|
k.fatal(msg, err)
|
||||||
|
} else if param.Binfmt[i].Interpreter, err = check.NewAbs(pathname); err != nil {
|
||||||
|
k.fatal(msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* early is called right before pivot_root into intermediate root;
|
/* early is called right before pivot_root into intermediate root;
|
||||||
this step is mostly for gathering information that would otherwise be
|
this step is mostly for gathering information that would otherwise be
|
||||||
difficult to obtain via library functions after pivot_root, and
|
difficult to obtain via library functions after pivot_root, and
|
||||||
implementations are expected to avoid changing the state of the mount
|
implementations are expected to avoid changing the state of the mount
|
||||||
namespace */
|
namespace */
|
||||||
for i, op := range *params.Ops {
|
for i, op := range *param.Ops {
|
||||||
if op == nil || !op.Valid() {
|
if op == nil || !op.Valid() {
|
||||||
k.fatalf(msg, "invalid op at index %d", i)
|
k.fatalf(msg, "invalid op at index %d", i)
|
||||||
}
|
}
|
||||||
@@ -243,13 +271,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
|
|
||||||
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
|
|
||||||
}
|
|
||||||
if err := k.chdir(intermediateHostPath); err != nil {
|
|
||||||
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := k.mkdir(sysrootDir, 0755); err != nil {
|
if err := k.mkdir(sysrootDir, 0755); err != nil {
|
||||||
k.fatalf(msg, "%v", err)
|
k.fatalf(msg, "%v", err)
|
||||||
}
|
}
|
||||||
@@ -272,7 +293,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
step sets up the container filesystem, and implementations are expected to
|
step sets up the container filesystem, and implementations are expected to
|
||||||
keep the host root and sysroot mount points intact but otherwise can do
|
keep the host root and sysroot mount points intact but otherwise can do
|
||||||
whatever they need to. Calling chdir is allowed but discouraged. */
|
whatever they need to. Calling chdir is allowed but discouraged. */
|
||||||
for i, op := range *params.Ops {
|
for i, op := range *param.Ops {
|
||||||
// ops already checked during early setup
|
// ops already checked during early setup
|
||||||
if prefix, ok := op.prefix(); ok {
|
if prefix, ok := op.prefix(); ok {
|
||||||
msg.Verbosef("%s %s", prefix, op)
|
msg.Verbosef("%s %s", prefix, op)
|
||||||
@@ -286,6 +307,48 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(param.Binfmt) > 0 {
|
||||||
|
const interpreter = "/interpreter"
|
||||||
|
|
||||||
|
if param.BinfmtPath == nil {
|
||||||
|
param.BinfmtPath = fhs.AbsProcSys.Append("fs/binfmt_misc")
|
||||||
|
}
|
||||||
|
binfmt := sysrootPath + param.BinfmtPath.String()
|
||||||
|
if err := k.mkdirAll(binfmt, 0); err != nil {
|
||||||
|
k.fatal(msg, err)
|
||||||
|
}
|
||||||
|
if err := k.mount(
|
||||||
|
SourceBinfmtMisc,
|
||||||
|
binfmt,
|
||||||
|
FstypeBinfmtMisc,
|
||||||
|
MS_NOSUID|MS_NOEXEC|MS_NODEV,
|
||||||
|
zeroString,
|
||||||
|
); err != nil {
|
||||||
|
k.fatal(msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
buf.Grow(1920)
|
||||||
|
|
||||||
|
register := binfmt + "/register"
|
||||||
|
for i, e := range param.Binfmt {
|
||||||
|
if err := k.symlink(hostPath+e.Interpreter.String(), interpreter); err != nil {
|
||||||
|
k.fatal(msg, err)
|
||||||
|
} else if err = k.writeFile(register, []byte(":"+
|
||||||
|
strconv.Itoa(i)+":"+
|
||||||
|
"M:"+
|
||||||
|
strconv.Itoa(int(e.Offset))+":"+
|
||||||
|
escapeBinfmt(&buf, e.Magic)+":"+
|
||||||
|
escapeBinfmt(&buf, e.Mask)+":"+
|
||||||
|
interpreter+":"+
|
||||||
|
"F"), 0); err != nil {
|
||||||
|
k.fatal(msg, err)
|
||||||
|
} else if err = k.remove(interpreter); err != nil {
|
||||||
|
k.fatal(msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// setup requiring host root complete at this point
|
// setup requiring host root complete at this point
|
||||||
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
|
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
|
||||||
k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
|
k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
|
||||||
@@ -324,11 +387,19 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keepCaps []uintptr
|
||||||
|
if param.Privileged {
|
||||||
|
keepCaps = append(keepCaps, CAP_SYS_ADMIN, CAP_SETPCAP)
|
||||||
|
}
|
||||||
|
if param.InitAsRoot {
|
||||||
|
keepCaps = append(keepCaps, CAP_SETFCAP)
|
||||||
|
}
|
||||||
|
|
||||||
if err := k.capAmbientClearAll(); err != nil {
|
if err := k.capAmbientClearAll(); err != nil {
|
||||||
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
|
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
|
||||||
}
|
}
|
||||||
for i := uintptr(0); i <= lastcap; i++ {
|
for i := range lastcap + 1 {
|
||||||
if params.Privileged && i == CAP_SYS_ADMIN {
|
if slices.Contains(keepCaps, i) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := k.capBoundingSetDrop(i); err != nil {
|
if err := k.capBoundingSetDrop(i); err != nil {
|
||||||
@@ -337,27 +408,30 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var keep [2]uint32
|
var keep [2]uint32
|
||||||
if params.Privileged {
|
for _, c := range keepCaps {
|
||||||
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
keep[capToIndex(c)] |= capToMask(c)
|
||||||
|
|
||||||
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
|
|
||||||
k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.capset(
|
if err := k.capset(
|
||||||
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
|
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
|
||||||
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
|
&[2]capData{{keep[0], keep[0], keep[0]}, {keep[1], keep[1], keep[1]}},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
k.fatalf(msg, "cannot capset: %v", err)
|
k.fatalf(msg, "cannot capset: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !params.SeccompDisable {
|
for _, c := range keepCaps {
|
||||||
rules := params.SeccompRules
|
if err := k.capAmbientRaise(c); err != nil {
|
||||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
k.fatalf(msg, "cannot raise %#x: %v", c, err)
|
||||||
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
|
|
||||||
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
|
|
||||||
}
|
}
|
||||||
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
|
}
|
||||||
|
|
||||||
|
if !param.SeccompDisable {
|
||||||
|
rules := param.SeccompRules
|
||||||
|
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||||
|
msg.Verbosef("resolving presets %#x", param.SeccompPresets)
|
||||||
|
rules = seccomp.Preset(param.SeccompPresets, param.SeccompFlags)
|
||||||
|
}
|
||||||
|
if err := k.seccompLoad(rules, param.SeccompFlags); err != nil {
|
||||||
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
|
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
|
||||||
k.fatalf(msg, "cannot load syscall filter: %v", err)
|
k.fatalf(msg, "cannot load syscall filter: %v", err)
|
||||||
}
|
}
|
||||||
@@ -366,10 +440,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
msg.Verbose("syscall filter not configured")
|
msg.Verbose("syscall filter not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
extraFiles := make([]*os.File, params.Count)
|
extraFiles := make([]*os.File, param.Count)
|
||||||
for i := range extraFiles {
|
for i := range extraFiles {
|
||||||
// setup fd is placed before all extra files
|
// setup fd is placed before all extra files
|
||||||
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
extraFiles[i] = k.newFile(uintptr(setupFd+1+i), "extra file "+strconv.Itoa(i))
|
||||||
}
|
}
|
||||||
k.umask(oldmask)
|
k.umask(oldmask)
|
||||||
|
|
||||||
@@ -447,7 +521,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
|
|
||||||
// called right before startup of initial process, all state changes to the
|
// called right before startup of initial process, all state changes to the
|
||||||
// current process is prohibited during late
|
// current process is prohibited during late
|
||||||
for i, op := range *params.Ops {
|
for i, op := range *param.Ops {
|
||||||
// ops already checked during early setup
|
// ops already checked during early setup
|
||||||
if err := op.late(state, k); err != nil {
|
if err := op.late(state, k); err != nil {
|
||||||
if m, ok := messageFromError(err); ok {
|
if m, ok := messageFromError(err); ok {
|
||||||
@@ -468,14 +542,22 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(params.Path.String())
|
cmd := exec.Command(param.Path.String())
|
||||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
cmd.Args = params.Args
|
cmd.Args = param.Args
|
||||||
cmd.Env = params.Env
|
cmd.Env = param.Env
|
||||||
cmd.ExtraFiles = extraFiles
|
cmd.ExtraFiles = extraFiles
|
||||||
cmd.Dir = params.Dir.String()
|
cmd.Dir = param.Dir.String()
|
||||||
|
|
||||||
msg.Verbosef("starting initial process %s", params.Path)
|
if param.InitAsRoot {
|
||||||
|
cmd.SysProcAttr = &SysProcAttr{
|
||||||
|
Cloneflags: CLONE_NEWUSER,
|
||||||
|
UidMappings: []SysProcIDMap{{ContainerID: param.Uid, HostID: 0, Size: 1}},
|
||||||
|
GidMappings: []SysProcIDMap{{ContainerID: param.Gid, HostID: 0, Size: 1}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Verbosef("starting initial process %s", param.Path)
|
||||||
if err := k.start(cmd); err != nil {
|
if err := k.start(cmd); err != nil {
|
||||||
k.fatalf(msg, "%v", err)
|
k.fatalf(msg, "%v", err)
|
||||||
}
|
}
|
||||||
@@ -493,9 +575,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case s := <-sig:
|
case s := <-sig:
|
||||||
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
|
if s == CancelSignal && param.ForwardCancel && cmd.Process != nil {
|
||||||
msg.Verbose("forwarding context cancellation")
|
msg.Verbose("forwarding context cancellation")
|
||||||
if err := k.signal(cmd, os.Interrupt); err != nil {
|
if err := k.signal(cmd, os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
||||||
k.printf(msg, "cannot forward cancellation: %v", err)
|
k.printf(msg, "cannot forward cancellation: %v", err)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -525,7 +607,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
// start timeout early
|
// start timeout early
|
||||||
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
go func() { time.Sleep(param.AdoptWaitDelay); close(timeout) }()
|
||||||
|
|
||||||
// close initial process files; this also keeps them alive
|
// close initial process files; this also keeps them alive
|
||||||
for _, f := range extraFiles {
|
for _, f := range extraFiles {
|
||||||
|
|||||||
+83
-82
@@ -10,6 +10,7 @@ import (
|
|||||||
"hakurei.app/check"
|
"hakurei.app/check"
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/container/std"
|
"hakurei.app/container/std"
|
||||||
|
"hakurei.app/internal/params"
|
||||||
"hakurei.app/internal/stub"
|
"hakurei.app/internal/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||||
call("getpid", stub.ExpectArgs{}, 1, nil),
|
call("getpid", stub.ExpectArgs{}, 1, nil),
|
||||||
call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, nil),
|
call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, nil),
|
||||||
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, ErrReceiveEnv),
|
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, params.ErrReceiveEnv),
|
||||||
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
|
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
|
||||||
},
|
},
|
||||||
}, nil},
|
}, nil},
|
||||||
@@ -94,7 +95,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
Uid: 1 << 16,
|
Uid: 1 << 16,
|
||||||
Gid: 1 << 15,
|
Gid: 1 << 15,
|
||||||
Hostname: "hakurei-check",
|
Hostname: "hakurei-check",
|
||||||
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
|
Ops: new(make(Ops, 1)),
|
||||||
SeccompRules: make([]std.NativeRule, 0),
|
SeccompRules: make([]std.NativeRule, 0),
|
||||||
SeccompPresets: std.PresetStrict,
|
SeccompPresets: std.PresetStrict,
|
||||||
RetainSession: true,
|
RetainSession: true,
|
||||||
@@ -122,7 +123,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
Uid: 1 << 16,
|
Uid: 1 << 16,
|
||||||
Gid: 1 << 15,
|
Gid: 1 << 15,
|
||||||
Hostname: "hakurei-check",
|
Hostname: "hakurei-check",
|
||||||
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
|
Ops: new(make(Ops, 1)),
|
||||||
SeccompRules: make([]std.NativeRule, 0),
|
SeccompRules: make([]std.NativeRule, 0),
|
||||||
SeccompPresets: std.PresetStrict,
|
SeccompPresets: std.PresetStrict,
|
||||||
RetainSession: true,
|
RetainSession: true,
|
||||||
@@ -151,7 +152,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
Uid: 1 << 16,
|
Uid: 1 << 16,
|
||||||
Gid: 1 << 15,
|
Gid: 1 << 15,
|
||||||
Hostname: "hakurei-check",
|
Hostname: "hakurei-check",
|
||||||
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
|
Ops: new(make(Ops, 1)),
|
||||||
SeccompRules: make([]std.NativeRule, 0),
|
SeccompRules: make([]std.NativeRule, 0),
|
||||||
SeccompPresets: std.PresetStrict,
|
SeccompPresets: std.PresetStrict,
|
||||||
RetainSession: true,
|
RetainSession: true,
|
||||||
@@ -181,7 +182,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
Uid: 1 << 16,
|
Uid: 1 << 16,
|
||||||
Gid: 1 << 15,
|
Gid: 1 << 15,
|
||||||
Hostname: "hakurei-check",
|
Hostname: "hakurei-check",
|
||||||
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
|
Ops: new(make(Ops, 1)),
|
||||||
SeccompRules: make([]std.NativeRule, 0),
|
SeccompRules: make([]std.NativeRule, 0),
|
||||||
SeccompPresets: std.PresetStrict,
|
SeccompPresets: std.PresetStrict,
|
||||||
RetainSession: true,
|
RetainSession: true,
|
||||||
@@ -212,7 +213,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
Uid: 1 << 16,
|
Uid: 1 << 16,
|
||||||
Gid: 1 << 15,
|
Gid: 1 << 15,
|
||||||
Hostname: "hakurei-check",
|
Hostname: "hakurei-check",
|
||||||
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
|
Ops: new(make(Ops, 1)),
|
||||||
SeccompRules: make([]std.NativeRule, 0),
|
SeccompRules: make([]std.NativeRule, 0),
|
||||||
SeccompPresets: std.PresetStrict,
|
SeccompPresets: std.PresetStrict,
|
||||||
RetainSession: true,
|
RetainSession: true,
|
||||||
@@ -244,7 +245,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
Uid: 1 << 16,
|
Uid: 1 << 16,
|
||||||
Gid: 1 << 15,
|
Gid: 1 << 15,
|
||||||
Hostname: "hakurei-check",
|
Hostname: "hakurei-check",
|
||||||
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
|
Ops: new(make(Ops, 1)),
|
||||||
SeccompRules: make([]std.NativeRule, 0),
|
SeccompRules: make([]std.NativeRule, 0),
|
||||||
SeccompPresets: std.PresetStrict,
|
SeccompPresets: std.PresetStrict,
|
||||||
RetainSession: true,
|
RetainSession: true,
|
||||||
@@ -278,7 +279,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
Uid: 1 << 16,
|
Uid: 1 << 16,
|
||||||
Gid: 1 << 15,
|
Gid: 1 << 15,
|
||||||
Hostname: "hakurei-check",
|
Hostname: "hakurei-check",
|
||||||
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
|
Ops: new(make(Ops, 1)),
|
||||||
SeccompRules: make([]std.NativeRule, 0),
|
SeccompRules: make([]std.NativeRule, 0),
|
||||||
SeccompPresets: std.PresetStrict,
|
SeccompPresets: std.PresetStrict,
|
||||||
RetainSession: true,
|
RetainSession: true,
|
||||||
@@ -314,7 +315,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
Uid: 1 << 16,
|
Uid: 1 << 16,
|
||||||
Gid: 1 << 15,
|
Gid: 1 << 15,
|
||||||
Hostname: "hakurei-check",
|
Hostname: "hakurei-check",
|
||||||
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
|
Ops: new(make(Ops, 1)),
|
||||||
SeccompRules: make([]std.NativeRule, 0),
|
SeccompRules: make([]std.NativeRule, 0),
|
||||||
SeccompPresets: std.PresetStrict,
|
SeccompPresets: std.PresetStrict,
|
||||||
RetainSession: true,
|
RetainSession: true,
|
||||||
@@ -331,6 +332,8 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("fatalf", stub.ExpectArgs{"invalid op at index %d", []any{0}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"invalid op at index %d", []any{0}}, nil, nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
@@ -369,6 +372,8 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("fatalf", stub.ExpectArgs{"invalid op at index %d", []any{0}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"invalid op at index %d", []any{0}}, nil, nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
@@ -407,6 +412,8 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", stub.UniqueError(61)),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", stub.UniqueError(61)),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot prepare op at index %d: %v", []any{0, stub.UniqueError(61)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot prepare op at index %d: %v", []any{0, stub.UniqueError(61)}}, nil, nil),
|
||||||
@@ -446,6 +453,8 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", &os.PathError{Op: "readlink", Path: "/", Err: stub.UniqueError(60)}),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", &os.PathError{Op: "readlink", Path: "/", Err: stub.UniqueError(60)}),
|
||||||
call("fatal", stub.ExpectArgs{[]any{"cannot readlink /: unique error 60 injected by the test suite"}}, nil, nil),
|
call("fatal", stub.ExpectArgs{[]any{"cannot readlink /: unique error 60 injected by the test suite"}}, nil, nil),
|
||||||
@@ -485,9 +494,6 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
/* begin early */
|
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
|
||||||
/* end early */
|
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, stub.UniqueError(58)),
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, stub.UniqueError(58)),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot mount intermediate root: %v", []any{stub.UniqueError(58)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot mount intermediate root: %v", []any{stub.UniqueError(58)}}, nil, nil),
|
||||||
},
|
},
|
||||||
@@ -525,9 +531,6 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
/* begin early */
|
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
|
||||||
/* end early */
|
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, stub.UniqueError(56)),
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, stub.UniqueError(56)),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot enter intermediate host path: %v", []any{stub.UniqueError(56)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot enter intermediate host path: %v", []any{stub.UniqueError(56)}}, nil, nil),
|
||||||
@@ -566,11 +569,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, stub.UniqueError(54)),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, stub.UniqueError(54)),
|
||||||
call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(54)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(54)}}, nil, nil),
|
||||||
},
|
},
|
||||||
@@ -608,11 +611,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, stub.UniqueError(52)),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, stub.UniqueError(52)),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot bind sysroot: %v", []any{stub.UniqueError(52)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot bind sysroot: %v", []any{stub.UniqueError(52)}}, nil, nil),
|
||||||
@@ -651,11 +654,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, stub.UniqueError(50)),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, stub.UniqueError(50)),
|
||||||
@@ -695,11 +698,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -740,11 +743,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -786,11 +789,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -841,11 +844,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -896,11 +899,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -952,11 +955,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1009,11 +1012,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1068,11 +1071,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1128,11 +1131,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1189,11 +1192,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1251,11 +1254,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1314,11 +1317,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1378,11 +1381,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1443,11 +1446,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1509,11 +1512,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1583,11 +1586,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1621,7 +1624,6 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
|
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
|
||||||
@@ -1653,8 +1655,9 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
|
||||||
|
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0x200100, 0x200100, 0x200100}, {0, 0, 0}}}, nil, nil),
|
||||||
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, stub.UniqueError(19)),
|
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, stub.UniqueError(19)),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot raise CAP_SYS_ADMIN: %v", []any{stub.UniqueError(19)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot raise %#x: %v", []any{uintptr(0x15), stub.UniqueError(19)}}, nil, nil),
|
||||||
},
|
},
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
@@ -1690,11 +1693,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1728,7 +1731,6 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
|
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
|
||||||
@@ -1760,8 +1762,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
|
||||||
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, nil),
|
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0x200100, 0x200100, 0x200100}, {0, 0, 0}}}, nil, stub.UniqueError(17)),
|
||||||
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0, 0x200000, 0x200000}, {0, 0, 0}}}, nil, stub.UniqueError(17)),
|
|
||||||
call("fatalf", stub.ExpectArgs{"cannot capset: %v", []any{stub.UniqueError(17)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot capset: %v", []any{stub.UniqueError(17)}}, nil, nil),
|
||||||
},
|
},
|
||||||
}, nil},
|
}, nil},
|
||||||
@@ -1798,11 +1799,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -1836,7 +1837,6 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
|
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
|
||||||
@@ -1868,8 +1868,9 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
|
||||||
|
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0x200100, 0x200100, 0x200100}, {0, 0, 0}}}, nil, nil),
|
||||||
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, nil),
|
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, nil),
|
||||||
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0, 0x200000, 0x200000}, {0, 0, 0}}}, nil, nil),
|
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"resolving presets %#x", []any{std.FilterPreset(0xf)}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"resolving presets %#x", []any{std.FilterPreset(0xf)}}, nil, nil),
|
||||||
call("seccompLoad", stub.ExpectArgs{seccomp.Preset(0xf, 0), seccomp.ExportFlag(0)}, nil, stub.UniqueError(15)),
|
call("seccompLoad", stub.ExpectArgs{seccomp.Preset(0xf, 0), seccomp.ExportFlag(0)}, nil, stub.UniqueError(15)),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot load syscall filter: %v", []any{stub.UniqueError(15)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot load syscall filter: %v", []any{stub.UniqueError(15)}}, nil, nil),
|
||||||
@@ -1907,11 +1908,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -2031,11 +2032,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -2131,11 +2132,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -2231,11 +2232,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -2322,11 +2323,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -2417,11 +2418,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -2519,11 +2520,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -2658,11 +2659,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
|
||||||
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
call("lastcap", stub.ExpectArgs{}, uintptr(40), nil),
|
||||||
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
|
||||||
|
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
||||||
|
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
||||||
/* begin early */
|
/* begin early */
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
|
||||||
/* end early */
|
/* end early */
|
||||||
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
|
|
||||||
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
|
|
||||||
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
|
||||||
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
|
||||||
@@ -2696,7 +2697,6 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x5)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x6)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x7)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
|
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x9)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xa)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0xb)}, nil, nil),
|
||||||
@@ -2728,8 +2728,9 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x26)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x27)}, nil, nil),
|
||||||
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
|
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x28)}, nil, nil),
|
||||||
|
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0x200100, 0x200100, 0x200100}, {0, 0, 0}}}, nil, nil),
|
||||||
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, nil),
|
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x15)}, nil, nil),
|
||||||
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0, 0x200000, 0x200000}, {0, 0, 0}}}, nil, nil),
|
call("capAmbientRaise", stub.ExpectArgs{uintptr(0x8)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"resolving presets %#x", []any{std.FilterPreset(0xf)}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"resolving presets %#x", []any{std.FilterPreset(0xf)}}, nil, nil),
|
||||||
call("seccompLoad", stub.ExpectArgs{seccomp.Preset(0xf, 0), seccomp.ExportFlag(0)}, nil, nil),
|
call("seccompLoad", stub.ExpectArgs{seccomp.Preset(0xf, 0), seccomp.ExportFlag(0)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"%d filter rules loaded", []any{73}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%d filter rules loaded", []any{73}}, nil, nil),
|
||||||
|
|||||||
+40
-12
@@ -4,9 +4,9 @@ import (
|
|||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/ext"
|
||||||
"hakurei.app/fhs"
|
"hakurei.app/fhs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
|
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
o.upper = check.EscapeOverlayDataSegment(toHost(v))
|
o.upper = toHost(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
|
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
o.work = check.EscapeOverlayDataSegment(toHost(v))
|
o.work = toHost(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,12 +168,39 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
if v, err := k.evalSymlinks(a.String()); err != nil {
|
if v, err := k.evalSymlinks(a.String()); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
o.lower[i] = check.EscapeOverlayDataSegment(toHost(v))
|
o.lower[i] = toHost(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mountOverlay sets up an overlay mount via [ext.FS].
|
||||||
|
func mountOverlay(target string, options [][2]string) error {
|
||||||
|
fs, err := ext.OpenFS(SourceOverlay, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = fs.SetString("source", SourceOverlay); err != nil {
|
||||||
|
_ = fs.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
if err = fs.SetString(option[0], option[1]); err != nil {
|
||||||
|
_ = fs.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = fs.SetFlag(OptionOverlayUserxattr); err != nil {
|
||||||
|
_ = fs.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = fs.Mount(target, 0); err != nil {
|
||||||
|
_ = fs.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fs.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
||||||
target := o.Target.String()
|
target := o.Target.String()
|
||||||
if !o.noPrefix {
|
if !o.noPrefix {
|
||||||
@@ -194,7 +221,7 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
options := make([]string, 0, 4)
|
options := make([][2]string, 0, 2+len(o.lower))
|
||||||
|
|
||||||
if o.upper == zeroString && o.work == zeroString { // readonly
|
if o.upper == zeroString && o.work == zeroString { // readonly
|
||||||
if len(o.Lower) < 2 {
|
if len(o.Lower) < 2 {
|
||||||
@@ -205,15 +232,16 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
if len(o.Lower) == 0 {
|
if len(o.Lower) == 0 {
|
||||||
return &OverlayArgumentError{OverlayEmptyLower, zeroString}
|
return &OverlayArgumentError{OverlayEmptyLower, zeroString}
|
||||||
}
|
}
|
||||||
options = append(options,
|
options = append(options, [][2]string{
|
||||||
OptionOverlayUpperdir+"="+o.upper,
|
{OptionOverlayUpperdir, o.upper},
|
||||||
OptionOverlayWorkdir+"="+o.work)
|
{OptionOverlayWorkdir, o.work},
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
for _, lower := range o.lower {
|
||||||
|
options = append(options, [2]string{OptionOverlayLowerdir + "+", lower})
|
||||||
}
|
}
|
||||||
options = append(options,
|
|
||||||
OptionOverlayLowerdir+"="+strings.Join(o.lower, check.SpecialOverlayPath),
|
|
||||||
OptionOverlayUserxattr)
|
|
||||||
|
|
||||||
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
|
return k.mountOverlay(target, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
|
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|||||||
@@ -97,13 +97,12 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
||||||
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
|
||||||
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
|
||||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
|
call("mountOverlay", stub.ExpectArgs{"/sysroot", [][2]string{
|
||||||
"upperdir=overlay.upper.32768," +
|
{"upperdir", "overlay.upper.32768"},
|
||||||
"workdir=overlay.work.32768," +
|
{"workdir", "overlay.work.32768"},
|
||||||
"lowerdir=" +
|
{"lowerdir+", `/host/var/lib/planterette/base/debian:f92c9052`},
|
||||||
`/host/var/lib/planterette/base/debian\:f92c9052:` +
|
{"lowerdir+", `/host/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052`},
|
||||||
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
|
}}, nil, nil),
|
||||||
"userxattr"}, nil, nil),
|
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||||
@@ -129,11 +128,10 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
|
call("mountOverlay", stub.ExpectArgs{"/nix/store", [][2]string{
|
||||||
"lowerdir=" +
|
{"lowerdir+", "/host/mnt-root/nix/.ro-store"},
|
||||||
"/host/mnt-root/nix/.ro-store:" +
|
{"lowerdir+", "/host/mnt-root/nix/.ro-store0"},
|
||||||
"/host/mnt-root/nix/.ro-store0," +
|
}}, nil, nil),
|
||||||
"userxattr"}, nil, nil),
|
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||||
@@ -147,11 +145,10 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
|
||||||
"lowerdir=" +
|
{"lowerdir+", "/host/mnt-root/nix/.ro-store"},
|
||||||
"/host/mnt-root/nix/.ro-store:" +
|
{"lowerdir+", "/host/mnt-root/nix/.ro-store0"},
|
||||||
"/host/mnt-root/nix/.ro-store0," +
|
}}, nil, nil),
|
||||||
"userxattr"}, nil, nil),
|
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
@@ -219,7 +216,11 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, stub.UniqueError(0)),
|
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
|
||||||
|
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
|
||||||
|
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
|
||||||
|
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
|
||||||
|
}}, nil, stub.UniqueError(0)),
|
||||||
}, stub.UniqueError(0)},
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
@@ -233,11 +234,11 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
|
||||||
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
|
||||||
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
|
||||||
"lowerdir=/host/mnt-root/nix/ro-store," +
|
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
|
||||||
"userxattr"}, nil, nil),
|
}}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
@@ -261,16 +262,15 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
|
||||||
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
|
||||||
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
|
||||||
"lowerdir=" +
|
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
|
||||||
"/host/mnt-root/nix/ro-store:" +
|
{"lowerdir+", "/host/mnt-root/nix/ro-store0"},
|
||||||
"/host/mnt-root/nix/ro-store0:" +
|
{"lowerdir+", "/host/mnt-root/nix/ro-store1"},
|
||||||
"/host/mnt-root/nix/ro-store1:" +
|
{"lowerdir+", "/host/mnt-root/nix/ro-store2"},
|
||||||
"/host/mnt-root/nix/ro-store2:" +
|
{"lowerdir+", "/host/mnt-root/nix/ro-store3"},
|
||||||
"/host/mnt-root/nix/ro-store3," +
|
}}, nil, nil),
|
||||||
"userxattr"}, nil, nil),
|
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package container_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"hakurei.app/container"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLandlockString(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
rulesetAttr *container.RulesetAttr
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"nil", nil, "NULL"},
|
|
||||||
{"zero", new(container.RulesetAttr), "0"},
|
|
||||||
{"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
|
|
||||||
{"set", &container.RulesetAttr{
|
|
||||||
HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
|
|
||||||
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
|
|
||||||
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
|
|
||||||
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
|
|
||||||
{"all", &container.RulesetAttr{
|
|
||||||
HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
|
|
||||||
container.LANDLOCK_ACCESS_FS_WRITE_FILE |
|
|
||||||
container.LANDLOCK_ACCESS_FS_READ_FILE |
|
|
||||||
container.LANDLOCK_ACCESS_FS_READ_DIR |
|
|
||||||
container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
|
|
||||||
container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
|
|
||||||
container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
|
|
||||||
container.LANDLOCK_ACCESS_FS_MAKE_DIR |
|
|
||||||
container.LANDLOCK_ACCESS_FS_MAKE_REG |
|
|
||||||
container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
|
|
||||||
container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
|
|
||||||
container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
|
|
||||||
container.LANDLOCK_ACCESS_FS_MAKE_SYM |
|
|
||||||
container.LANDLOCK_ACCESS_FS_REFER |
|
|
||||||
container.LANDLOCK_ACCESS_FS_TRUNCATE |
|
|
||||||
container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
|
|
||||||
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
|
|
||||||
container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
|
|
||||||
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
|
|
||||||
container.LANDLOCK_SCOPE_SIGNAL,
|
|
||||||
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
if got := tc.rulesetAttr.String(); got != tc.want {
|
|
||||||
t.Errorf("String: %s, want %s", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLandlockAttrSize(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
want := 24
|
|
||||||
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
|
|
||||||
t.Errorf("Sizeof: %d, want %d", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,6 +40,9 @@ const (
|
|||||||
// SourceMqueue is used when mounting mqueue.
|
// SourceMqueue is used when mounting mqueue.
|
||||||
// Note that any source value is allowed when fstype is [FstypeMqueue].
|
// Note that any source value is allowed when fstype is [FstypeMqueue].
|
||||||
SourceMqueue = "mqueue"
|
SourceMqueue = "mqueue"
|
||||||
|
// SourceBinfmtMisc is used when mounting binfmt_misc.
|
||||||
|
// Note that any source value is allowed when fstype is [SourceBinfmtMisc].
|
||||||
|
SourceBinfmtMisc = "binfmt_misc"
|
||||||
// SourceOverlay is used when mounting overlay.
|
// SourceOverlay is used when mounting overlay.
|
||||||
// Note that any source value is allowed when fstype is [FstypeOverlay].
|
// Note that any source value is allowed when fstype is [FstypeOverlay].
|
||||||
SourceOverlay = "overlay"
|
SourceOverlay = "overlay"
|
||||||
@@ -70,6 +73,9 @@ const (
|
|||||||
// FstypeMqueue represents the mqueue pseudo-filesystem.
|
// FstypeMqueue represents the mqueue pseudo-filesystem.
|
||||||
// This filesystem type is usually mounted on /dev/mqueue.
|
// This filesystem type is usually mounted on /dev/mqueue.
|
||||||
FstypeMqueue = "mqueue"
|
FstypeMqueue = "mqueue"
|
||||||
|
// FstypeBinfmtMisc represents the binfmt_misc pseudo-filesystem.
|
||||||
|
// This filesystem type is usually mounted on /proc/sys/fs/binfmt_misc.
|
||||||
|
FstypeBinfmtMisc = "binfmt_misc"
|
||||||
// FstypeOverlay represents the overlay pseudo-filesystem.
|
// FstypeOverlay represents the overlay pseudo-filesystem.
|
||||||
// This filesystem type can be mounted anywhere in the container filesystem.
|
// This filesystem type can be mounted anywhere in the container filesystem.
|
||||||
FstypeOverlay = "overlay"
|
FstypeOverlay = "overlay"
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/gob"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
|
||||||
func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
|
|
||||||
if r, w, err := os.Pipe(); err != nil {
|
|
||||||
return -1, nil, err
|
|
||||||
} else {
|
|
||||||
fd := 3 + len(*extraFiles)
|
|
||||||
*extraFiles = append(*extraFiles, r)
|
|
||||||
return fd, w, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrReceiveEnv = errors.New("environment variable not set")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Receive retrieves setup fd from the environment and receives params.
|
|
||||||
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
|
|
||||||
var setup *os.File
|
|
||||||
|
|
||||||
if s, ok := os.LookupEnv(key); !ok {
|
|
||||||
return nil, ErrReceiveEnv
|
|
||||||
} else {
|
|
||||||
if fd, err := strconv.Atoi(s); err != nil {
|
|
||||||
return nil, optionalErrorUnwrap(err)
|
|
||||||
} else {
|
|
||||||
setup = os.NewFile(uintptr(fd), "setup")
|
|
||||||
if setup == nil {
|
|
||||||
return nil, syscall.EDOM
|
|
||||||
}
|
|
||||||
if fdp != nil {
|
|
||||||
*fdp = setup.Fd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return setup.Close, gob.NewDecoder(setup).Decode(e)
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/vfs"
|
"hakurei.app/vfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,9 +49,6 @@ func TestToHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
|
|
||||||
func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
|
|
||||||
|
|
||||||
func TestCreateFile(t *testing.T) {
|
func TestCreateFile(t *testing.T) {
|
||||||
t.Run("nonexistent", func(t *testing.T) {
|
t.Run("nonexistent", func(t *testing.T) {
|
||||||
t.Run("mkdir", func(t *testing.T) {
|
t.Run("mkdir", func(t *testing.T) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"hakurei.app/ext"
|
"hakurei.app/ext"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetNoNewPrivs sets the calling thread's no_new_privs attribute.
|
// setNoNewPrivs sets the calling thread's no_new_privs attribute.
|
||||||
func SetNoNewPrivs() error {
|
func setNoNewPrivs() error {
|
||||||
return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0)
|
return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
1000 0
|
|
||||||
Vendored
-12
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
cd "$(dirname -- "$0")" || exit 1
|
|
||||||
|
|
||||||
install -vDm0755 "bin/hakurei" "${DESTDIR}/usr/bin/hakurei"
|
|
||||||
install -vDm0755 "bin/sharefs" "${DESTDIR}/usr/bin/sharefs"
|
|
||||||
|
|
||||||
install -vDm4511 "bin/hsu" "${DESTDIR}/usr/bin/hsu"
|
|
||||||
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
|
||||||
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
install -vDm0644 "comp/_hakurei" "${DESTDIR}/usr/share/zsh/site-functions/_hakurei"
|
|
||||||
Vendored
-31
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/sh -e
|
|
||||||
cd "$(dirname -- "$0")/.."
|
|
||||||
VERSION="${HAKUREI_VERSION:-untagged}"
|
|
||||||
pname="hakurei-${VERSION}-$(go env GOARCH)"
|
|
||||||
out="${DESTDIR:-dist}/${pname}"
|
|
||||||
|
|
||||||
echo '# Preparing distribution files.'
|
|
||||||
mkdir -p "${out}"
|
|
||||||
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
|
|
||||||
cp -rv "dist/comp" "${out}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo '# Building hakurei.'
|
|
||||||
go generate ./...
|
|
||||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
|
|
||||||
-buildid= -linkmode external -extldflags=-static
|
|
||||||
-X hakurei.app/internal/info.buildVersion=${VERSION}
|
|
||||||
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
|
|
||||||
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
|
|
||||||
-X main.hakureiPath=/usr/bin/hakurei" ./...
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo '# Testing hakurei.'
|
|
||||||
go test -ldflags='-buildid= -linkmode external -extldflags=-static' ./...
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo '# Creating distribution.'
|
|
||||||
rm -f "${out}.tar.gz" && tar -C "${out}/.." -vczf "${out}.tar.gz" "${pname}"
|
|
||||||
rm -rf "${out}"
|
|
||||||
(cd "${out}/.." && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
|
|
||||||
echo
|
|
||||||
+1
-1
@@ -39,7 +39,7 @@ func TestSyscall(t *testing.T) {
|
|||||||
t.Errorf("Unmarshal: %v, want %v", got, tc.want)
|
t.Errorf("Unmarshal: %v, want %v", got, tc.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if errors.As(tc.err, new(ext.SyscallNameError)) {
|
if _, ok := errors.AsType[ext.SyscallNameError](tc.err); ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
package ext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// include/uapi/linux/mount.h
|
||||||
|
|
||||||
|
/*
|
||||||
|
* move_mount() flags.
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
MOVE_MOUNT_F_SYMLINKS = 1 << iota /* Follow symlinks on from path */
|
||||||
|
MOVE_MOUNT_F_AUTOMOUNTS /* Follow automounts on from path */
|
||||||
|
MOVE_MOUNT_F_EMPTY_PATH /* Empty from path permitted */
|
||||||
|
_
|
||||||
|
MOVE_MOUNT_T_SYMLINKS /* Follow symlinks on to path */
|
||||||
|
MOVE_MOUNT_T_AUTOMOUNTS /* Follow automounts on to path */
|
||||||
|
MOVE_MOUNT_T_EMPTY_PATH /* Empty to path permitted */
|
||||||
|
_
|
||||||
|
MOVE_MOUNT_SET_GROUP /* Set sharing group instead */
|
||||||
|
MOVE_MOUNT_BENEATH /* Mount beneath top mount */
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fsopen() flags.
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
FSOPEN_CLOEXEC = 1 << iota
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fspick() flags.
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
FSPICK_CLOEXEC = 1 << iota
|
||||||
|
FSPICK_SYMLINK_NOFOLLOW
|
||||||
|
FSPICK_NO_AUTOMOUNT
|
||||||
|
FSPICK_EMPTY_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The type of fsconfig() call made.
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
FSCONFIG_SET_FLAG = iota /* Set parameter, supplying no value */
|
||||||
|
FSCONFIG_SET_STRING /* Set parameter, supplying a string value */
|
||||||
|
FSCONFIG_SET_BINARY /* Set parameter, supplying a binary blob value */
|
||||||
|
FSCONFIG_SET_PATH /* Set parameter, supplying an object by path */
|
||||||
|
FSCONFIG_SET_PATH_EMPTY /* Set parameter, supplying an object by (empty) path */
|
||||||
|
FSCONFIG_SET_FD /* Set parameter, supplying an object by fd */
|
||||||
|
FSCONFIG_CMD_CREATE /* Create new or reuse existing superblock */
|
||||||
|
FSCONFIG_CMD_RECONFIGURE /* Invoke superblock reconfiguration */
|
||||||
|
FSCONFIG_CMD_CREATE_EXCL /* Create new superblock, fail if reusing existing superblock */
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fsmount() flags.
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
FSMOUNT_CLOEXEC = 1 << iota
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mount attributes.
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
MOUNT_ATTR_RDONLY = 0x00000001 /* Mount read-only */
|
||||||
|
MOUNT_ATTR_NOSUID = 0x00000002 /* Ignore suid and sgid bits */
|
||||||
|
MOUNT_ATTR_NODEV = 0x00000004 /* Disallow access to device special files */
|
||||||
|
MOUNT_ATTR_NOEXEC = 0x00000008 /* Disallow program execution */
|
||||||
|
MOUNT_ATTR__ATIME = 0x00000070 /* Setting on how atime should be updated */
|
||||||
|
MOUNT_ATTR_RELATIME = 0x00000000 /* - Update atime relative to mtime/ctime. */
|
||||||
|
MOUNT_ATTR_NOATIME = 0x00000010 /* - Do not update access times. */
|
||||||
|
MOUNT_ATTR_STRICTATIME = 0x00000020 /* - Always perform atime updates */
|
||||||
|
MOUNT_ATTR_NODIRATIME = 0x00000080 /* Do not update directory access times */
|
||||||
|
MOUNT_ATTR_IDMAP = 0x00100000 /* Idmap mount to @userns_fd in struct mount_attr. */
|
||||||
|
MOUNT_ATTR_NOSYMFOLLOW = 0x00200000 /* Do not follow symlinks */
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS provides low-level wrappers around the suite of file-descriptor-based
|
||||||
|
// mount facilities in Linux.
|
||||||
|
type FS struct {
|
||||||
|
fd uintptr
|
||||||
|
c runtime.Cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFS allocates a new [FS] for the specified fd.
|
||||||
|
func newFS(fd uintptr) *FS {
|
||||||
|
fs := FS{fd: fd}
|
||||||
|
fs.c = runtime.AddCleanup(&fs, func(fd uintptr) {
|
||||||
|
_ = syscall.Close(int(fd))
|
||||||
|
}, fd)
|
||||||
|
return &fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying filesystem context.
|
||||||
|
func (fs *FS) Close() error {
|
||||||
|
if fs == nil {
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
err := syscall.Close(int(fs.fd))
|
||||||
|
fs.c.Stop()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFS creates a new filesystem context.
|
||||||
|
func OpenFS(fsname string, flags int) (fs *FS, err error) {
|
||||||
|
var s *byte
|
||||||
|
s, err = syscall.BytePtrFromString(fsname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fd, _, errno := syscall.Syscall(
|
||||||
|
SYS_FSOPEN,
|
||||||
|
uintptr(unsafe.Pointer(s)),
|
||||||
|
uintptr(flags|FSOPEN_CLOEXEC),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
err = os.NewSyscallError("fsopen", errno)
|
||||||
|
} else {
|
||||||
|
fs = newFS(fd)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PickFS selects filesystem for reconfiguration.
|
||||||
|
func PickFS(dirfd int, pathname string, flags int) (fs *FS, err error) {
|
||||||
|
var s *byte
|
||||||
|
s, err = syscall.BytePtrFromString(pathname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fd, _, errno := syscall.Syscall(
|
||||||
|
SYS_FSPICK,
|
||||||
|
uintptr(dirfd),
|
||||||
|
uintptr(unsafe.Pointer(s)),
|
||||||
|
uintptr(flags|FSPICK_CLOEXEC),
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
err = os.NewSyscallError("fspick", errno)
|
||||||
|
} else {
|
||||||
|
fs = newFS(fd)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// config configures new or existing filesystem context.
|
||||||
|
func (fs *FS) config(cmd uint, key *byte, value unsafe.Pointer, aux int) (err error) {
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
SYS_FSCONFIG,
|
||||||
|
fs.fd,
|
||||||
|
uintptr(cmd),
|
||||||
|
uintptr(unsafe.Pointer(key)),
|
||||||
|
uintptr(value),
|
||||||
|
uintptr(aux),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
err = os.NewSyscallError("fsconfig", errno)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFlag sets the flag parameter named by key. ([FSCONFIG_SET_FLAG])
|
||||||
|
func (fs *FS) SetFlag(key string) (err error) {
|
||||||
|
var s *byte
|
||||||
|
s, err = syscall.BytePtrFromString(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.config(FSCONFIG_SET_FLAG, s, nil, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetString sets the string parameter named by key to the value specified by
|
||||||
|
// value. ([FSCONFIG_SET_STRING])
|
||||||
|
func (fs *FS) SetString(key, value string) (err error) {
|
||||||
|
var s0 *byte
|
||||||
|
s0, err = syscall.BytePtrFromString(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var s1 *byte
|
||||||
|
s1, err = syscall.BytePtrFromString(value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.config(FSCONFIG_SET_STRING, s0, unsafe.Pointer(s1), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mount instantiates mount object from filesystem context.
|
||||||
|
func (fs *FS) mount(flags, attrFlags int) (fsfd int, err error) {
|
||||||
|
r, _, errno := syscall.Syscall(
|
||||||
|
SYS_FSMOUNT,
|
||||||
|
fs.fd,
|
||||||
|
uintptr(flags|FSMOUNT_CLOEXEC),
|
||||||
|
uintptr(attrFlags),
|
||||||
|
)
|
||||||
|
fsfd = int(r)
|
||||||
|
if errno != 0 {
|
||||||
|
err = os.NewSyscallError("fsmount", errno)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveMount moves or attaches mount object to filesystem.
|
||||||
|
func MoveMount(
|
||||||
|
fromDirfd int,
|
||||||
|
fromPathname string,
|
||||||
|
toDirfd int,
|
||||||
|
toPathname string,
|
||||||
|
flags int,
|
||||||
|
) (err error) {
|
||||||
|
var s0 *byte
|
||||||
|
s0, err = syscall.BytePtrFromString(fromPathname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var s1 *byte
|
||||||
|
s1, err = syscall.BytePtrFromString(toPathname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
SYS_MOVE_MOUNT,
|
||||||
|
uintptr(fromDirfd),
|
||||||
|
uintptr(unsafe.Pointer(s0)),
|
||||||
|
uintptr(toDirfd),
|
||||||
|
uintptr(unsafe.Pointer(s1)),
|
||||||
|
uintptr(flags),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
err = os.NewSyscallError("move_mount", errno)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount attaches the underlying filesystem context to the specified pathname.
|
||||||
|
func (fs *FS) Mount(pathname string, attrFlags int) error {
|
||||||
|
if err := fs.config(FSCONFIG_CMD_CREATE_EXCL, nil, nil, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fd, err := fs.mount(0, attrFlags)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = MoveMount(
|
||||||
|
fd, "",
|
||||||
|
-1, pathname,
|
||||||
|
MOVE_MOUNT_F_EMPTY_PATH,
|
||||||
|
)
|
||||||
|
closeErr := syscall.Close(fd)
|
||||||
|
if err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ var (
|
|||||||
AbsDevShm = unsafeAbs(DevShm)
|
AbsDevShm = unsafeAbs(DevShm)
|
||||||
// AbsProc is [Proc] as [check.Absolute].
|
// AbsProc is [Proc] as [check.Absolute].
|
||||||
AbsProc = unsafeAbs(Proc)
|
AbsProc = unsafeAbs(Proc)
|
||||||
|
// AbsProcSys is [ProcSys] as [check.Absolute].
|
||||||
|
AbsProcSys = unsafeAbs(ProcSys)
|
||||||
// AbsProcSelfExe is [ProcSelfExe] as [check.Absolute].
|
// AbsProcSelfExe is [ProcSelfExe] as [check.Absolute].
|
||||||
AbsProcSelfExe = unsafeAbs(ProcSelfExe)
|
AbsProcSelfExe = unsafeAbs(ProcSelfExe)
|
||||||
// AbsSys is [Sys] as [check.Absolute].
|
// AbsSys is [Sys] as [check.Absolute].
|
||||||
|
|||||||
Generated
+8
-8
@@ -7,32 +7,32 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1772985280,
|
"lastModified": 1780361225,
|
||||||
"narHash": "sha256-FdrNykOoY9VStevU4zjSUdvsL9SzJTcXt4omdEDZDLk=",
|
"narHash": "sha256-wnV9ttf4fPWNonBIQmvlrSlNpQYgx5HgWWd007mwIFA=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "8f736f007139d7f70752657dff6a401a585d6cbc",
|
"rev": "e28654b71096e08c019d4861ca26acb646f583d8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"ref": "release-25.11",
|
"ref": "release-26.05",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1772822230,
|
"lastModified": 1780453794,
|
||||||
"narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=",
|
"narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "71caefce12ba78d84fe618cf61644dce01cf3a96",
|
"rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-25.11",
|
"ref": "nixos-26.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
description = "hakurei container tool and nixos module";
|
description = "hakurei container tool and nixos module";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
|
||||||
|
|
||||||
home-manager = {
|
home-manager = {
|
||||||
url = "github:nix-community/home-manager/release-25.11";
|
url = "github:nix-community/home-manager/release-26.05";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
inherit (pkgs)
|
inherit (pkgs)
|
||||||
runCommandLocal
|
runCommandLocal
|
||||||
callPackage
|
callPackage
|
||||||
nixfmt-rfc-style
|
nixfmt
|
||||||
deadnix
|
deadnix
|
||||||
statix
|
statix
|
||||||
;
|
;
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
|
sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
|
||||||
|
|
||||||
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt ]; } ''
|
||||||
cd ${./.}
|
cd ${./.}
|
||||||
|
|
||||||
echo "running nixfmt..."
|
echo "running nixfmt..."
|
||||||
@@ -137,11 +137,9 @@
|
|||||||
|
|
||||||
CC="musl-clang -O3 -Werror -Qunused-arguments" \
|
CC="musl-clang -O3 -Werror -Qunused-arguments" \
|
||||||
GOCACHE="$(mktemp -d)" \
|
GOCACHE="$(mktemp -d)" \
|
||||||
HAKUREI_TEST_SKIP_ACL=1 \
|
|
||||||
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
|
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
|
||||||
DESTDIR="$out" \
|
DESTDIR="$out" \
|
||||||
HAKUREI_VERSION="v${hakurei.version}" \
|
./all.sh
|
||||||
./dist/release.sh
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -196,6 +194,7 @@
|
|||||||
./test/interactive/vm.nix
|
./test/interactive/vm.nix
|
||||||
./test/interactive/hakurei.nix
|
./test/interactive/hakurei.nix
|
||||||
./test/interactive/trace.nix
|
./test/interactive/trace.nix
|
||||||
|
./test/interactive/raceattr.nix
|
||||||
|
|
||||||
self.nixosModules.hakurei
|
self.nixosModules.hakurei
|
||||||
home-manager.nixosModules.home-manager
|
home-manager.nixosModules.home-manager
|
||||||
|
|||||||
+36
-11
@@ -140,21 +140,29 @@ var (
|
|||||||
ErrInsecure = errors.New("configuration is insecure")
|
ErrInsecure = errors.New("configuration is insecure")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VAllowInsecure allows use of compatibility options considered insecure
|
||||||
|
// under any configuration, to work around ecosystem-wide flaws.
|
||||||
|
VAllowInsecure = 1 << iota
|
||||||
|
)
|
||||||
|
|
||||||
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
||||||
func (config *Config) Validate() error {
|
func (config *Config) Validate(flags int) error {
|
||||||
|
const step = "validate configuration"
|
||||||
|
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
return &AppError{Step: step, Err: ErrConfigNull,
|
||||||
Msg: "invalid configuration"}
|
Msg: "invalid configuration"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is checked again in hsu
|
// this is checked again in hsu
|
||||||
if config.Identity < IdentityStart || config.Identity > IdentityEnd {
|
if config.Identity < IdentityStart || config.Identity > IdentityEnd {
|
||||||
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
|
return &AppError{Step: step, Err: ErrIdentityBounds,
|
||||||
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
|
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST {
|
if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST {
|
||||||
return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds,
|
return &AppError{Step: step, Err: ErrSchedPolicyBounds,
|
||||||
Msg: "scheduling policy " +
|
Msg: "scheduling policy " +
|
||||||
strconv.Itoa(int(config.SchedPolicy)) +
|
strconv.Itoa(int(config.SchedPolicy)) +
|
||||||
" out of range"}
|
" out of range"}
|
||||||
@@ -168,34 +176,51 @@ func (config *Config) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.Container == nil {
|
if config.Container == nil {
|
||||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
return &AppError{Step: step, Err: ErrConfigNull,
|
||||||
Msg: "configuration missing container state"}
|
Msg: "configuration missing container state"}
|
||||||
}
|
}
|
||||||
if config.Container.Home == nil {
|
if config.Container.Home == nil {
|
||||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
return &AppError{Step: step, Err: ErrConfigNull,
|
||||||
Msg: "container configuration missing path to home directory"}
|
Msg: "container configuration missing path to home directory"}
|
||||||
}
|
}
|
||||||
if config.Container.Shell == nil {
|
if config.Container.Shell == nil {
|
||||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
return &AppError{Step: step, Err: ErrConfigNull,
|
||||||
Msg: "container configuration missing path to shell"}
|
Msg: "container configuration missing path to shell"}
|
||||||
}
|
}
|
||||||
if config.Container.Path == nil {
|
if config.Container.Path == nil {
|
||||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
return &AppError{Step: step, Err: ErrConfigNull,
|
||||||
Msg: "container configuration missing path to initial program"}
|
Msg: "container configuration missing path to initial program"}
|
||||||
}
|
}
|
||||||
|
|
||||||
for key := range config.Container.Env {
|
for key := range config.Container.Env {
|
||||||
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
|
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
|
||||||
return &AppError{Step: "validate configuration", Err: ErrEnviron,
|
return &AppError{Step: step, Err: ErrEnviron,
|
||||||
Msg: "invalid environment variable " + strconv.Quote(key)}
|
Msg: "invalid environment variable " + strconv.Quote(key)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
|
et := config.Enablements.Unwrap()
|
||||||
return &AppError{Step: "validate configuration", Err: ErrInsecure,
|
if !config.DirectPulse && et&EPulse != 0 {
|
||||||
|
return &AppError{Step: step, Err: ErrInsecure,
|
||||||
Msg: "enablement PulseAudio is insecure and no longer supported"}
|
Msg: "enablement PulseAudio is insecure and no longer supported"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flags&VAllowInsecure == 0 {
|
||||||
|
switch {
|
||||||
|
case et&EWayland != 0 && config.DirectWayland:
|
||||||
|
return &AppError{Step: step, Err: ErrInsecure,
|
||||||
|
Msg: "direct_wayland is insecure and no longer supported"}
|
||||||
|
|
||||||
|
case et&EPipeWire != 0 && config.DirectPipeWire:
|
||||||
|
return &AppError{Step: step, Err: ErrInsecure,
|
||||||
|
Msg: "direct_pipewire is insecure and no longer supported"}
|
||||||
|
|
||||||
|
case et&EPulse != 0 && config.DirectPulse:
|
||||||
|
return &AppError{Step: step, Err: ErrInsecure,
|
||||||
|
Msg: "direct_pulse is insecure and no longer supported"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+61
-17
@@ -14,65 +14,109 @@ func TestConfigValidate(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
config *hst.Config
|
config *hst.Config
|
||||||
|
flags int
|
||||||
wantErr error
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
{"nil", nil, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||||
Msg: "invalid configuration"}},
|
Msg: "invalid configuration"}},
|
||||||
{"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
|
||||||
|
{"identity lower", &hst.Config{Identity: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||||
Msg: "identity -1 out of range"}},
|
Msg: "identity -1 out of range"}},
|
||||||
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
{"identity upper", &hst.Config{Identity: 10000}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||||
Msg: "identity 10000 out of range"}},
|
Msg: "identity 10000 out of range"}},
|
||||||
{"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
|
||||||
|
{"sched lower", &hst.Config{SchedPolicy: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||||
Msg: "scheduling policy -1 out of range"}},
|
Msg: "scheduling policy -1 out of range"}},
|
||||||
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||||
Msg: "scheduling policy 51966 out of range"}},
|
Msg: "scheduling policy 51966 out of range"}},
|
||||||
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
|
|
||||||
|
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, 0,
|
||||||
&hst.BadInterfaceError{Interface: "", Segment: "session"}},
|
&hst.BadInterfaceError{Interface: "", Segment: "session"}},
|
||||||
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},
|
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, 0,
|
||||||
&hst.BadInterfaceError{Interface: "", Segment: "system"}},
|
&hst.BadInterfaceError{Interface: "", Segment: "system"}},
|
||||||
{"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
|
||||||
|
{"container", &hst.Config{}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||||
Msg: "configuration missing container state"}},
|
Msg: "configuration missing container state"}},
|
||||||
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||||
Msg: "container configuration missing path to home directory"}},
|
Msg: "container configuration missing path to home directory"}},
|
||||||
{"shell", &hst.Config{Container: &hst.ContainerConfig{
|
{"shell", &hst.Config{Container: &hst.ContainerConfig{
|
||||||
Home: fhs.AbsTmp,
|
Home: fhs.AbsTmp,
|
||||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||||
Msg: "container configuration missing path to shell"}},
|
Msg: "container configuration missing path to shell"}},
|
||||||
{"path", &hst.Config{Container: &hst.ContainerConfig{
|
{"path", &hst.Config{Container: &hst.ContainerConfig{
|
||||||
Home: fhs.AbsTmp,
|
Home: fhs.AbsTmp,
|
||||||
Shell: fhs.AbsTmp,
|
Shell: fhs.AbsTmp,
|
||||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||||
Msg: "container configuration missing path to initial program"}},
|
Msg: "container configuration missing path to initial program"}},
|
||||||
|
|
||||||
{"env equals", &hst.Config{Container: &hst.ContainerConfig{
|
{"env equals", &hst.Config{Container: &hst.ContainerConfig{
|
||||||
Home: fhs.AbsTmp,
|
Home: fhs.AbsTmp,
|
||||||
Shell: fhs.AbsTmp,
|
Shell: fhs.AbsTmp,
|
||||||
Path: fhs.AbsTmp,
|
Path: fhs.AbsTmp,
|
||||||
Env: map[string]string{"TERM=": ""},
|
Env: map[string]string{"TERM=": ""},
|
||||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||||
Msg: `invalid environment variable "TERM="`}},
|
Msg: `invalid environment variable "TERM="`}},
|
||||||
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{
|
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{
|
||||||
Home: fhs.AbsTmp,
|
Home: fhs.AbsTmp,
|
||||||
Shell: fhs.AbsTmp,
|
Shell: fhs.AbsTmp,
|
||||||
Path: fhs.AbsTmp,
|
Path: fhs.AbsTmp,
|
||||||
Env: map[string]string{"TERM\x00": ""},
|
Env: map[string]string{"TERM\x00": ""},
|
||||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||||
Msg: `invalid environment variable "TERM\x00"`}},
|
Msg: `invalid environment variable "TERM\x00"`}},
|
||||||
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
|
|
||||||
|
{"insecure pulse", &hst.Config{Enablements: new(hst.EPulse), Container: &hst.ContainerConfig{
|
||||||
Home: fhs.AbsTmp,
|
Home: fhs.AbsTmp,
|
||||||
Shell: fhs.AbsTmp,
|
Shell: fhs.AbsTmp,
|
||||||
Path: fhs.AbsTmp,
|
Path: fhs.AbsTmp,
|
||||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||||
Msg: "enablement PulseAudio is insecure and no longer supported"}},
|
Msg: "enablement PulseAudio is insecure and no longer supported"}},
|
||||||
|
|
||||||
|
{"direct wayland", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
|
||||||
|
Home: fhs.AbsTmp,
|
||||||
|
Shell: fhs.AbsTmp,
|
||||||
|
Path: fhs.AbsTmp,
|
||||||
|
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||||
|
Msg: "direct_wayland is insecure and no longer supported"}},
|
||||||
|
{"direct wayland allow", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
|
||||||
|
Home: fhs.AbsTmp,
|
||||||
|
Shell: fhs.AbsTmp,
|
||||||
|
Path: fhs.AbsTmp,
|
||||||
|
}}, hst.VAllowInsecure, nil},
|
||||||
|
|
||||||
|
{"direct pipewire", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||||
|
Home: fhs.AbsTmp,
|
||||||
|
Shell: fhs.AbsTmp,
|
||||||
|
Path: fhs.AbsTmp,
|
||||||
|
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||||
|
Msg: "direct_pipewire is insecure and no longer supported"}},
|
||||||
|
{"direct pipewire allow", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||||
|
Home: fhs.AbsTmp,
|
||||||
|
Shell: fhs.AbsTmp,
|
||||||
|
Path: fhs.AbsTmp,
|
||||||
|
}}, hst.VAllowInsecure, nil},
|
||||||
|
|
||||||
|
{"direct pulse", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
|
||||||
|
Home: fhs.AbsTmp,
|
||||||
|
Shell: fhs.AbsTmp,
|
||||||
|
Path: fhs.AbsTmp,
|
||||||
|
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||||
|
Msg: "direct_pulse is insecure and no longer supported"}},
|
||||||
|
{"direct pulse allow", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
|
||||||
|
Home: fhs.AbsTmp,
|
||||||
|
Shell: fhs.AbsTmp,
|
||||||
|
Path: fhs.AbsTmp,
|
||||||
|
}}, hst.VAllowInsecure, nil},
|
||||||
|
|
||||||
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
||||||
Home: fhs.AbsTmp,
|
Home: fhs.AbsTmp,
|
||||||
Shell: fhs.AbsTmp,
|
Shell: fhs.AbsTmp,
|
||||||
Path: fhs.AbsTmp,
|
Path: fhs.AbsTmp,
|
||||||
}}, nil},
|
}}, 0, nil},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
|
if err := tc.config.Validate(tc.flags); !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
|
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package hst
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -64,10 +65,14 @@ const (
|
|||||||
// Some programs fail to connect to dbus session running as a different uid,
|
// Some programs fail to connect to dbus session running as a different uid,
|
||||||
// this option works around it by mapping priv-side caller uid in container.
|
// this option works around it by mapping priv-side caller uid in container.
|
||||||
FMapRealUID
|
FMapRealUID
|
||||||
|
// FNoPlace disables placement of /etc/passwd and /etc/group.
|
||||||
|
FNoPlace
|
||||||
|
|
||||||
// FDevice mount /dev/ from the init mount namespace as is in the container
|
// FDevice mount /dev/ from the init mount namespace as is in the container
|
||||||
// mount namespace.
|
// mount namespace.
|
||||||
FDevice
|
FDevice
|
||||||
|
// FCoverRun covers /run/ in the container mount namespace early.
|
||||||
|
FCoverRun
|
||||||
|
|
||||||
// FShareRuntime shares XDG_RUNTIME_DIR between containers under the same identity.
|
// FShareRuntime shares XDG_RUNTIME_DIR between containers under the same identity.
|
||||||
FShareRuntime
|
FShareRuntime
|
||||||
@@ -98,8 +103,12 @@ func (flags Flags) String() string {
|
|||||||
return "tty"
|
return "tty"
|
||||||
case FMapRealUID:
|
case FMapRealUID:
|
||||||
return "mapuid"
|
return "mapuid"
|
||||||
|
case FNoPlace:
|
||||||
|
return "noplace"
|
||||||
case FDevice:
|
case FDevice:
|
||||||
return "device"
|
return "device"
|
||||||
|
case FCoverRun:
|
||||||
|
return "cover_run"
|
||||||
case FShareRuntime:
|
case FShareRuntime:
|
||||||
return "runtime"
|
return "runtime"
|
||||||
case FShareTmpdir:
|
case FShareTmpdir:
|
||||||
@@ -161,6 +170,10 @@ type ContainerConfig struct {
|
|||||||
Flags Flags `json:"-"`
|
Flags Flags `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ContainerConfig) GoString() string {
|
||||||
|
return fmt.Sprintf("&%#v", *c)
|
||||||
|
}
|
||||||
|
|
||||||
// ContainerConfigF is [ContainerConfig] stripped of its methods.
|
// ContainerConfigF is [ContainerConfig] stripped of its methods.
|
||||||
//
|
//
|
||||||
// The [ContainerConfig.Flags] field does not survive a [json] round trip.
|
// The [ContainerConfig.Flags] field does not survive a [json] round trip.
|
||||||
@@ -188,9 +201,13 @@ type containerConfigJSON = struct {
|
|||||||
|
|
||||||
// Corresponds to [FMapRealUID].
|
// Corresponds to [FMapRealUID].
|
||||||
MapRealUID bool `json:"map_real_uid"`
|
MapRealUID bool `json:"map_real_uid"`
|
||||||
|
// Corresponds to [FNoPlace].
|
||||||
|
NoPlace bool `json:"noplace,omitempty"`
|
||||||
|
|
||||||
// Corresponds to [FDevice].
|
// Corresponds to [FDevice].
|
||||||
Device bool `json:"device,omitempty"`
|
Device bool `json:"device,omitempty"`
|
||||||
|
// Corresponds to [FCoverRun].
|
||||||
|
CoverRun bool `json:"cover_run,omitempty"`
|
||||||
|
|
||||||
// Corresponds to [FShareRuntime].
|
// Corresponds to [FShareRuntime].
|
||||||
ShareRuntime bool `json:"share_runtime,omitempty"`
|
ShareRuntime bool `json:"share_runtime,omitempty"`
|
||||||
@@ -213,7 +230,9 @@ func (c *ContainerConfig) MarshalJSON() ([]byte, error) {
|
|||||||
Tty: c.Flags&FTty != 0,
|
Tty: c.Flags&FTty != 0,
|
||||||
Multiarch: c.Flags&FMultiarch != 0,
|
Multiarch: c.Flags&FMultiarch != 0,
|
||||||
MapRealUID: c.Flags&FMapRealUID != 0,
|
MapRealUID: c.Flags&FMapRealUID != 0,
|
||||||
|
NoPlace: c.Flags&FNoPlace != 0,
|
||||||
Device: c.Flags&FDevice != 0,
|
Device: c.Flags&FDevice != 0,
|
||||||
|
CoverRun: c.Flags&FCoverRun != 0,
|
||||||
ShareRuntime: c.Flags&FShareRuntime != 0,
|
ShareRuntime: c.Flags&FShareRuntime != 0,
|
||||||
ShareTmpdir: c.Flags&FShareTmpdir != 0,
|
ShareTmpdir: c.Flags&FShareTmpdir != 0,
|
||||||
})
|
})
|
||||||
@@ -254,9 +273,15 @@ func (c *ContainerConfig) UnmarshalJSON(data []byte) error {
|
|||||||
if v.MapRealUID {
|
if v.MapRealUID {
|
||||||
c.Flags |= FMapRealUID
|
c.Flags |= FMapRealUID
|
||||||
}
|
}
|
||||||
|
if v.NoPlace {
|
||||||
|
c.Flags |= FNoPlace
|
||||||
|
}
|
||||||
if v.Device {
|
if v.Device {
|
||||||
c.Flags |= FDevice
|
c.Flags |= FDevice
|
||||||
}
|
}
|
||||||
|
if v.CoverRun {
|
||||||
|
c.Flags |= FCoverRun
|
||||||
|
}
|
||||||
if v.ShareRuntime {
|
if v.ShareRuntime {
|
||||||
c.Flags |= FShareRuntime
|
c.Flags |= FShareRuntime
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ func TestFlagsString(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"none", 0, "none"},
|
{"none", 0, "none"},
|
||||||
{"none high", hst.FAll + 1, "none"},
|
{"none high", hst.FAll + 1, "none"},
|
||||||
{"all", hst.FAll, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir"},
|
{"all", hst.FAll, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, noplace, device, cover_run, runtime, tmpdir"},
|
||||||
{"all high", math.MaxUint, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir"},
|
{"all high", math.MaxUint, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, noplace, device, cover_run, runtime, tmpdir"},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -53,7 +53,7 @@ func TestContainerConfig(t *testing.T) {
|
|||||||
{"hostnet hostabstract mapuid", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID},
|
{"hostnet hostabstract mapuid", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID},
|
||||||
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":true}`},
|
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":true}`},
|
||||||
{"all", &hst.ContainerConfig{Flags: hst.FAll},
|
{"all", &hst.ContainerConfig{Flags: hst.FAll},
|
||||||
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"devel":true,"userns":true,"host_net":true,"host_abstract":true,"tty":true,"multiarch":true,"map_real_uid":true,"device":true,"share_runtime":true,"share_tmpdir":true}`},
|
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"devel":true,"userns":true,"host_net":true,"host_abstract":true,"tty":true,"multiarch":true,"map_real_uid":true,"noplace":true,"device":true,"cover_run":true,"share_runtime":true,"share_tmpdir":true}`},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package hst
|
package hst
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -61,6 +62,10 @@ type BusConfig struct {
|
|||||||
Filter bool `json:"filter"`
|
Filter bool `json:"filter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *BusConfig) GoString() string {
|
||||||
|
return fmt.Sprintf("&%#v", *c)
|
||||||
|
}
|
||||||
|
|
||||||
// Interfaces iterates over all interface strings specified in [BusConfig].
|
// Interfaces iterates over all interface strings specified in [BusConfig].
|
||||||
func (c *BusConfig) Interfaces(yield func(string) bool) {
|
func (c *BusConfig) Interfaces(yield func(string) bool) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
|
|||||||
+21
-31
@@ -7,12 +7,12 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enablement represents an optional host service to export to the target user.
|
// Enablements denotes optional host service to export to the target user.
|
||||||
type Enablement byte
|
type Enablements byte
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// EWayland exposes a Wayland pathname socket via security-context-v1.
|
// EWayland exposes a Wayland pathname socket via security-context-v1.
|
||||||
EWayland Enablement = 1 << iota
|
EWayland Enablements = 1 << iota
|
||||||
// EX11 adds the target user via X11 ChangeHosts and exposes the X11
|
// EX11 adds the target user via X11 ChangeHosts and exposes the X11
|
||||||
// pathname socket.
|
// pathname socket.
|
||||||
EX11
|
EX11
|
||||||
@@ -28,8 +28,8 @@ const (
|
|||||||
EM
|
EM
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a string representation of the flags set on [Enablement].
|
// String returns a string representation of the flags set on [Enablements].
|
||||||
func (e Enablement) String() string {
|
func (e Enablements) String() string {
|
||||||
switch e {
|
switch e {
|
||||||
case 0:
|
case 0:
|
||||||
return "(no enablements)"
|
return "(no enablements)"
|
||||||
@@ -47,7 +47,7 @@ func (e Enablement) String() string {
|
|||||||
buf := new(strings.Builder)
|
buf := new(strings.Builder)
|
||||||
buf.Grow(32)
|
buf.Grow(32)
|
||||||
|
|
||||||
for i := Enablement(1); i < EM; i <<= 1 {
|
for i := Enablements(1); i < EM; i <<= 1 {
|
||||||
if e&i != 0 {
|
if e&i != 0 {
|
||||||
buf.WriteString(", " + i.String())
|
buf.WriteString(", " + i.String())
|
||||||
}
|
}
|
||||||
@@ -60,12 +60,6 @@ func (e Enablement) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEnablements returns the address of [Enablement] as [Enablements].
|
|
||||||
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
|
|
||||||
|
|
||||||
// Enablements is the [json] adapter for [Enablement].
|
|
||||||
type Enablements Enablement
|
|
||||||
|
|
||||||
// enablementsJSON is the [json] representation of [Enablements].
|
// enablementsJSON is the [json] representation of [Enablements].
|
||||||
type enablementsJSON = struct {
|
type enablementsJSON = struct {
|
||||||
Wayland bool `json:"wayland,omitempty"`
|
Wayland bool `json:"wayland,omitempty"`
|
||||||
@@ -75,24 +69,21 @@ type enablementsJSON = struct {
|
|||||||
Pulse bool `json:"pulse,omitempty"`
|
Pulse bool `json:"pulse,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap returns the underlying [Enablement].
|
// Unwrap returns the value pointed to by e.
|
||||||
func (e *Enablements) Unwrap() Enablement {
|
func (e *Enablements) Unwrap() Enablements {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return Enablement(*e)
|
return *e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Enablements) MarshalJSON() ([]byte, error) {
|
func (e Enablements) MarshalJSON() ([]byte, error) {
|
||||||
if e == nil {
|
|
||||||
return nil, syscall.EINVAL
|
|
||||||
}
|
|
||||||
return json.Marshal(&enablementsJSON{
|
return json.Marshal(&enablementsJSON{
|
||||||
Wayland: Enablement(*e)&EWayland != 0,
|
Wayland: e&EWayland != 0,
|
||||||
X11: Enablement(*e)&EX11 != 0,
|
X11: e&EX11 != 0,
|
||||||
DBus: Enablement(*e)&EDBus != 0,
|
DBus: e&EDBus != 0,
|
||||||
PipeWire: Enablement(*e)&EPipeWire != 0,
|
PipeWire: e&EPipeWire != 0,
|
||||||
Pulse: Enablement(*e)&EPulse != 0,
|
Pulse: e&EPulse != 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,22 +97,21 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var ve Enablement
|
*e = 0
|
||||||
if v.Wayland {
|
if v.Wayland {
|
||||||
ve |= EWayland
|
*e |= EWayland
|
||||||
}
|
}
|
||||||
if v.X11 {
|
if v.X11 {
|
||||||
ve |= EX11
|
*e |= EX11
|
||||||
}
|
}
|
||||||
if v.DBus {
|
if v.DBus {
|
||||||
ve |= EDBus
|
*e |= EDBus
|
||||||
}
|
}
|
||||||
if v.PipeWire {
|
if v.PipeWire {
|
||||||
ve |= EPipeWire
|
*e |= EPipeWire
|
||||||
}
|
}
|
||||||
if v.Pulse {
|
if v.Pulse {
|
||||||
ve |= EPulse
|
*e |= EPulse
|
||||||
}
|
}
|
||||||
*e = Enablements(ve)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-12
@@ -13,7 +13,7 @@ func TestEnablementString(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
flags hst.Enablement
|
flags hst.Enablements
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{0, "(no enablements)"},
|
{0, "(no enablements)"},
|
||||||
@@ -59,13 +59,13 @@ func TestEnablements(t *testing.T) {
|
|||||||
sData string
|
sData string
|
||||||
}{
|
}{
|
||||||
{"nil", nil, "null", `{"value":null,"magic":3236757504}`},
|
{"nil", nil, "null", `{"value":null,"magic":3236757504}`},
|
||||||
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
|
{"zero", new(hst.Enablements(0)), `{}`, `{"value":{},"magic":3236757504}`},
|
||||||
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
{"wayland", new(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||||
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
{"x11", new(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||||
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
{"dbus", new(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||||
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
{"pipewire", new(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||||
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
{"pulse", new(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||||
{"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
{"all", new(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -137,7 +137,7 @@ func TestEnablements(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("val", func(t *testing.T) {
|
t.Run("val", func(t *testing.T) {
|
||||||
if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
|
if got := new(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
|
||||||
t.Errorf("Unwrap: %v", got)
|
t.Errorf("Unwrap: %v", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -146,9 +146,6 @@ func TestEnablements(t *testing.T) {
|
|||||||
t.Run("passthrough", func(t *testing.T) {
|
t.Run("passthrough", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
|
|
||||||
t.Errorf("MarshalJSON: error = %v", err)
|
|
||||||
}
|
|
||||||
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
|
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
|
||||||
t.Errorf("UnmarshalJSON: error = %v", err)
|
t.Errorf("UnmarshalJSON: error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/check"
|
||||||
)
|
)
|
||||||
@@ -36,6 +37,8 @@ type Ops interface {
|
|||||||
Bind(source, target *check.Absolute, flags int) Ops
|
Bind(source, target *check.Absolute, flags int) Ops
|
||||||
// Overlay appends an op that mounts the overlay pseudo filesystem.
|
// Overlay appends an op that mounts the overlay pseudo filesystem.
|
||||||
Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops
|
Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops
|
||||||
|
// OverlayEphemeral appends a MountOverlayOp with an ephemeral upperdir and workdir.
|
||||||
|
OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) Ops
|
||||||
// OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly.
|
// OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly.
|
||||||
OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops
|
OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops
|
||||||
|
|
||||||
@@ -56,8 +59,10 @@ type Ops interface {
|
|||||||
|
|
||||||
// ApplyState holds the address of [Ops] and any relevant application state.
|
// ApplyState holds the address of [Ops] and any relevant application state.
|
||||||
type ApplyState struct {
|
type ApplyState struct {
|
||||||
// AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition.
|
// Prefix for [FSBind] in autoetc [FSBind.Special] condition.
|
||||||
AutoEtcPrefix string
|
AutoEtcPrefix string
|
||||||
|
// Whether to skip remounting root.
|
||||||
|
NoRemountRoot bool
|
||||||
|
|
||||||
Ops
|
Ops
|
||||||
}
|
}
|
||||||
@@ -76,17 +81,17 @@ type FSImplError struct{ Value FilesystemConfig }
|
|||||||
|
|
||||||
func (f FSImplError) Error() string {
|
func (f FSImplError) Error() string {
|
||||||
implType := reflect.TypeOf(f.Value)
|
implType := reflect.TypeOf(f.Value)
|
||||||
var name string
|
var buf strings.Builder
|
||||||
for implType != nil && implType.Kind() == reflect.Ptr {
|
for implType != nil && implType.Kind() == reflect.Pointer {
|
||||||
name += "*"
|
buf.WriteByte('*')
|
||||||
implType = implType.Elem()
|
implType = implType.Elem()
|
||||||
}
|
}
|
||||||
if implType != nil {
|
if implType != nil {
|
||||||
name += implType.Name()
|
buf.WriteString(implType.Name())
|
||||||
} else {
|
} else {
|
||||||
name += "nil"
|
buf.WriteString("nil")
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("implementation %s not supported", name)
|
return "implementation " + buf.String() + " not supported"
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
|
// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
|
||||||
|
|||||||
+9
-4
@@ -3,6 +3,7 @@ package hst_test
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -103,7 +104,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
|
|||||||
t.Run("marshal", func(t *testing.T) {
|
t.Run("marshal", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
wantErr := tc.wantErr
|
wantErr := tc.wantErr
|
||||||
if errors.As(wantErr, new(hst.FSTypeError)) {
|
if _, ok := errors.AsType[hst.FSTypeError](wantErr); ok {
|
||||||
// for unsupported implementation tc
|
// for unsupported implementation tc
|
||||||
wantErr = hst.FSImplError{Value: stubFS{"cat"}}
|
wantErr = hst.FSImplError{Value: stubFS{"cat"}}
|
||||||
}
|
}
|
||||||
@@ -139,7 +140,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
|
|||||||
t.Run("unmarshal", func(t *testing.T) {
|
t.Run("unmarshal", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
if tc.data == "\x00" && tc.sData == "\x00" {
|
if tc.data == "\x00" && tc.sData == "\x00" {
|
||||||
if errors.As(tc.wantErr, new(hst.FSImplError)) {
|
if _, ok := errors.AsType[hst.FSImplError](tc.wantErr); ok {
|
||||||
// this error is only returned on marshal
|
// this error is only returned on marshal
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -283,11 +284,11 @@ func checkFs(t *testing.T, testCases []fsTestCase) {
|
|||||||
if !reflect.DeepEqual(ops, &tc.ops) {
|
if !reflect.DeepEqual(ops, &tc.ops) {
|
||||||
gotString := new(strings.Builder)
|
gotString := new(strings.Builder)
|
||||||
for _, op := range *ops {
|
for _, op := range *ops {
|
||||||
gotString.WriteString("\n" + op.String())
|
gotString.WriteString("\n" + fmt.Sprintf("%#v", op))
|
||||||
}
|
}
|
||||||
wantString := new(strings.Builder)
|
wantString := new(strings.Builder)
|
||||||
for _, op := range tc.ops {
|
for _, op := range tc.ops {
|
||||||
wantString.WriteString("\n" + op.String())
|
wantString.WriteString("\n" + fmt.Sprintf("%#v", op))
|
||||||
}
|
}
|
||||||
t.Errorf("Apply: %s, want %s", gotString, wantString)
|
t.Errorf("Apply: %s, want %s", gotString, wantString)
|
||||||
}
|
}
|
||||||
@@ -339,6 +340,10 @@ func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*chec
|
|||||||
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
|
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p opsAdapter) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
|
||||||
|
return opsAdapter{p.Ops.OverlayEphemeral(target, layers...)}
|
||||||
|
}
|
||||||
|
|
||||||
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
|
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
|
||||||
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
|
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-6
@@ -43,18 +43,13 @@ func (e *FSEphemeral) Apply(z *ApplyState) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
size := e.Size
|
|
||||||
if size < 0 {
|
|
||||||
size = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
perm := e.Perm
|
perm := e.Perm
|
||||||
if perm == 0 {
|
if perm == 0 {
|
||||||
perm = fsEphemeralDefaultPerm
|
perm = fsEphemeralDefaultPerm
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Write {
|
if e.Write {
|
||||||
z.Tmpfs(e.Target, size, perm)
|
z.Tmpfs(e.Target, max(e.Size, 0), perm)
|
||||||
} else {
|
} else {
|
||||||
z.Readonly(e.Target, perm)
|
z.Readonly(e.Target, perm)
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-10
@@ -2,9 +2,11 @@ package hst
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/fhs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { gob.Register(new(FSOverlay)) }
|
func init() { gob.Register(new(FSOverlay)) }
|
||||||
@@ -39,7 +41,7 @@ func (o *FSOverlay) Valid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if o.Upper != nil { // rw
|
if o.Upper != nil { // rw
|
||||||
return o.Work != nil && len(o.Lower) > 0
|
return o.Work != nil || len(o.Lower) > 0
|
||||||
} else { // ro
|
} else { // ro
|
||||||
return len(o.Lower) >= 2
|
return len(o.Lower) >= 2
|
||||||
}
|
}
|
||||||
@@ -57,8 +59,11 @@ func (o *FSOverlay) Host() []*check.Absolute {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
p := make([]*check.Absolute, 0, 2+len(o.Lower))
|
p := make([]*check.Absolute, 0, 2+len(o.Lower))
|
||||||
if o.Upper != nil && o.Work != nil {
|
if o.Upper != nil {
|
||||||
p = append(p, o.Upper, o.Work)
|
p = append(p, o.Upper)
|
||||||
|
if o.Work != nil {
|
||||||
|
p = append(p, o.Work)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p = append(p, o.Lower...)
|
p = append(p, o.Lower...)
|
||||||
return p
|
return p
|
||||||
@@ -69,9 +74,19 @@ func (o *FSOverlay) Apply(z *ApplyState) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.Upper != nil && o.Work != nil { // rw
|
if o.Upper != nil {
|
||||||
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
|
if o.Target.Is(fhs.AbsRoot) {
|
||||||
} else { // ro
|
z.NoRemountRoot = true
|
||||||
|
}
|
||||||
|
if o.Work != nil {
|
||||||
|
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
|
||||||
|
} else {
|
||||||
|
z.OverlayEphemeral(o.Target, slices.Concat(
|
||||||
|
[]*check.Absolute{o.Upper},
|
||||||
|
o.Lower,
|
||||||
|
)...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
z.OverlayReadonly(o.Target, o.Lower...)
|
z.OverlayReadonly(o.Target, o.Lower...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,12 +101,19 @@ func (o *FSOverlay) String() string {
|
|||||||
lower[i] = check.EscapeOverlayDataSegment(a.String())
|
lower[i] = check.EscapeOverlayDataSegment(a.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.Upper != nil && o.Work != nil {
|
if o.Upper != nil {
|
||||||
return "w*" + strings.Join(append([]string{
|
if o.Work != nil {
|
||||||
|
return "w*" + strings.Join(append([]string{
|
||||||
|
check.EscapeOverlayDataSegment(o.Target.String()),
|
||||||
|
check.EscapeOverlayDataSegment(o.Upper.String()),
|
||||||
|
check.EscapeOverlayDataSegment(o.Work.String())},
|
||||||
|
lower...), check.SpecialOverlayPath)
|
||||||
|
}
|
||||||
|
return "e*" + strings.Join(append([]string{
|
||||||
check.EscapeOverlayDataSegment(o.Target.String()),
|
check.EscapeOverlayDataSegment(o.Target.String()),
|
||||||
check.EscapeOverlayDataSegment(o.Upper.String()),
|
check.EscapeOverlayDataSegment(o.Upper.String())},
|
||||||
check.EscapeOverlayDataSegment(o.Work.String())},
|
|
||||||
lower...), check.SpecialOverlayPath)
|
lower...), check.SpecialOverlayPath)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return "*" + strings.Join(append([]string{
|
return "*" + strings.Join(append([]string{
|
||||||
check.EscapeOverlayDataSegment(o.Target.String())},
|
check.EscapeOverlayDataSegment(o.Target.String())},
|
||||||
|
|||||||
+26
-1
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/check"
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/fhs"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ func TestFSOverlay(t *testing.T) {
|
|||||||
checkFs(t, []fsTestCase{
|
checkFs(t, []fsTestCase{
|
||||||
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
|
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
|
||||||
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*check.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
|
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*check.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
|
||||||
{"zero lower", &hst.FSOverlay{Target: m("/etc"), Upper: m("/"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
|
{"zero lower", &hst.FSOverlay{Target: m("/etc"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
|
||||||
{"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, "<invalid>"},
|
{"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, "<invalid>"},
|
||||||
{"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"},
|
{"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"},
|
||||||
|
|
||||||
@@ -49,5 +50,29 @@ func TestFSOverlay(t *testing.T) {
|
|||||||
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||||
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
|
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
|
||||||
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
|
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
|
||||||
|
|
||||||
|
{"no remount root", &hst.FSOverlay{
|
||||||
|
Target: m("/"),
|
||||||
|
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||||
|
Upper: m("/tmp/upper"),
|
||||||
|
Work: m("/tmp/work"),
|
||||||
|
}, true, container.Ops{&container.MountOverlayOp{
|
||||||
|
Target: m("/"),
|
||||||
|
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||||
|
Upper: m("/tmp/upper"),
|
||||||
|
Work: m("/tmp/work"),
|
||||||
|
}}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"),
|
||||||
|
"w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"},
|
||||||
|
|
||||||
|
{"ephemeral", &hst.FSOverlay{
|
||||||
|
Target: m("/"),
|
||||||
|
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||||
|
Upper: m("/tmp/upper"),
|
||||||
|
}, true, container.Ops{&container.MountOverlayOp{
|
||||||
|
Target: m("/"),
|
||||||
|
Lower: ms("/tmp/upper", "/tmp/.src0", "/tmp/.src1"),
|
||||||
|
Upper: fhs.AbsRoot,
|
||||||
|
}}, m("/"), ms("/tmp/upper", "/tmp/.src0", "/tmp/.src1"),
|
||||||
|
"e*/:/tmp/upper:/tmp/.src0:/tmp/.src1"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ func Template() *Config {
|
|||||||
return &Config{
|
return &Config{
|
||||||
ID: "org.chromium.Chromium",
|
ID: "org.chromium.Chromium",
|
||||||
|
|
||||||
Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
|
Enablements: new(EWayland | EDBus | EPipeWire),
|
||||||
|
|
||||||
SessionBus: &BusConfig{
|
SessionBus: &BusConfig{
|
||||||
See: nil,
|
See: nil,
|
||||||
|
|||||||
@@ -244,7 +244,9 @@ func TestTemplate(t *testing.T) {
|
|||||||
"tty": true,
|
"tty": true,
|
||||||
"multiarch": true,
|
"multiarch": true,
|
||||||
"map_real_uid": true,
|
"map_real_uid": true,
|
||||||
|
"noplace": true,
|
||||||
"device": true,
|
"device": true,
|
||||||
|
"cover_run": true,
|
||||||
"share_runtime": true,
|
"share_runtime": true,
|
||||||
"share_tmpdir": true
|
"share_tmpdir": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/internal/acl"
|
"hakurei.app/internal/acl"
|
||||||
|
"hakurei.app/internal/info"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testFileName = "acl.test"
|
const testFileName = "acl.test"
|
||||||
@@ -24,8 +26,14 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestUpdate(t *testing.T) {
|
func TestUpdate(t *testing.T) {
|
||||||
if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" {
|
if info.CanDegrade {
|
||||||
t.Skip("acl test skipped")
|
name := filepath.Join(t.TempDir(), "check-degrade")
|
||||||
|
if err := os.WriteFile(name, nil, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := acl.Update(name, os.Geteuid()); errors.Is(err, syscall.ENOTSUP) {
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testFilePath := filepath.Join(t.TempDir(), testFileName)
|
testFilePath := filepath.Join(t.TempDir(), testFileName)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func unescapeValue(v []byte) (val []byte, errno ParseError) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\*
|
if found := bytes.Contains([]byte("-_/.\\*"), []byte{b}); found { // - // _/.\*
|
||||||
goto opt
|
goto opt
|
||||||
} else if b >= '0' && b <= '9' { // 0-9
|
} else if b >= '0' && b <= '9' { // 0-9
|
||||||
goto opt
|
goto opt
|
||||||
@@ -101,7 +101,7 @@ func unescapeValue(v []byte) (val []byte, errno ParseError) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil {
|
if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil {
|
||||||
if errors.As(err, new(hex.InvalidByteError)) {
|
if _, ok := errors.AsType[hex.InvalidByteError](err); ok {
|
||||||
errno = ErrBadValHexByte
|
errno = ErrBadValHexByte
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+24
-4
@@ -1,21 +1,29 @@
|
|||||||
// Package env provides the [Paths] struct for efficiently building paths from the environment.
|
// Package env provides the [Paths] struct for efficiently building paths from
|
||||||
|
// the environment.
|
||||||
package env
|
package env
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/check"
|
||||||
|
"hakurei.app/fhs"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const VarRunNscd = fhs.Var + "run/nscd"
|
||||||
|
|
||||||
// Paths holds paths copied from the environment and is used to create [hst.Paths].
|
// Paths holds paths copied from the environment and is used to create [hst.Paths].
|
||||||
type Paths struct {
|
type Paths struct {
|
||||||
// TempDir is returned by [os.TempDir].
|
// TempDir is returned by [os.TempDir].
|
||||||
TempDir *check.Absolute
|
TempDir *check.Absolute
|
||||||
// RuntimePath is copied from $XDG_RUNTIME_DIR.
|
// RuntimePath is copied from $XDG_RUNTIME_DIR.
|
||||||
RuntimePath *check.Absolute
|
RuntimePath *check.Absolute
|
||||||
|
// Whether [VarRunNscd] is a directory.
|
||||||
|
HasNscd bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy expands [Paths] into [hst.Paths].
|
// Copy expands [Paths] into [hst.Paths].
|
||||||
@@ -37,14 +45,17 @@ func (env *Paths) Copy(v *hst.Paths, userid int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CopyPaths returns a populated [Paths].
|
// CopyPaths returns a populated [Paths].
|
||||||
func CopyPaths() *Paths { return CopyPathsFunc(log.Fatalf, os.TempDir, os.Getenv) }
|
func CopyPaths() *Paths {
|
||||||
|
return CopyPathsFunc(log.Fatalf, os.TempDir, os.Getenv, os.Stat)
|
||||||
|
}
|
||||||
|
|
||||||
// CopyPathsFunc returns a populated [Paths],
|
// CopyPathsFunc returns a populated [Paths], using the provided [log.Fatalf],
|
||||||
// using the provided [log.Fatalf], [os.TempDir], [os.Getenv] functions.
|
// [os.TempDir], [os.Getenv] functions.
|
||||||
func CopyPathsFunc(
|
func CopyPathsFunc(
|
||||||
fatalf func(format string, v ...any),
|
fatalf func(format string, v ...any),
|
||||||
tempdir func() string,
|
tempdir func() string,
|
||||||
getenv func(key string) string,
|
getenv func(key string) string,
|
||||||
|
stat func(name string) (fs.FileInfo, error),
|
||||||
) *Paths {
|
) *Paths {
|
||||||
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||||
|
|
||||||
@@ -61,5 +72,14 @@ func CopyPathsFunc(
|
|||||||
env.RuntimePath = a
|
env.RuntimePath = a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fi, err := stat(VarRunNscd); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
fatalf("%v", err)
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
env.HasNscd = fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
return &env
|
return &env
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-1
@@ -2,6 +2,7 @@ package env_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -104,7 +105,9 @@ func TestCopyPaths(t *testing.T) {
|
|||||||
t.Fatalf("fatalf: %q, want %q", got, tc.fatal)
|
t.Fatalf("fatalf: %q, want %q", got, tc.fatal)
|
||||||
}
|
}
|
||||||
panic(stub.PanicExit)
|
panic(stub.PanicExit)
|
||||||
}, func() string { return tc.tmp }, func(key string) string { return tc.env[key] })
|
}, func() string { return tc.tmp }, func(key string) string { return tc.env[key] }, func(name string) (fs.FileInfo, error) {
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
})
|
||||||
|
|
||||||
if tc.fatal != "" {
|
if tc.fatal != "" {
|
||||||
t.Fatalf("copyPaths: expected fatal %q", tc.fatal)
|
t.Fatalf("copyPaths: expected fatal %q", tc.fatal)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !noskip
|
||||||
|
|
||||||
|
package info
|
||||||
|
|
||||||
|
// CanDegrade is whether tests are allowed to transparently degrade or skip due
|
||||||
|
// to required system features being denied or unavailable.
|
||||||
|
const CanDegrade = true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build noskip
|
||||||
|
|
||||||
|
package info
|
||||||
|
|
||||||
|
const CanDegrade = false
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package kobject
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"maps"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"hakurei.app/internal/uevent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event is a [uevent.Message] with known environment variables processed.
|
||||||
|
type Event struct {
|
||||||
|
// alloc_uevent_skb: action_string
|
||||||
|
Action uevent.KobjectAction `json:"action"`
|
||||||
|
// alloc_uevent_skb: devpath
|
||||||
|
DevPath string `json:"devpath"`
|
||||||
|
|
||||||
|
// Uninterpreted environment variable pairs. An entry missing a separator
|
||||||
|
// gains the value "\x00".
|
||||||
|
Env map[string]string `json:"env"`
|
||||||
|
|
||||||
|
// SEQNUM value set by the kernel.
|
||||||
|
Sequence uint64 `json:"seqnum"`
|
||||||
|
// SYNTH_UUID value set on trigger, nil denotes a non-synthetic event.
|
||||||
|
Synth *uevent.UUID `json:"synth_uuid,omitempty"`
|
||||||
|
// SUBSYSTEM value set by the kernel.
|
||||||
|
Subsystem string `json:"subsystem"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a copy of e.
|
||||||
|
func (e *Event) Clone() Event {
|
||||||
|
v := *e
|
||||||
|
v.Env = maps.Clone(e.Env)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeColdboot allocates a new [Object] from e in [StateColdboot].
|
||||||
|
func (e *Event) makeColdboot() *Object {
|
||||||
|
return &Object{
|
||||||
|
State: StateColdboot,
|
||||||
|
DevPath: e.DevPath,
|
||||||
|
Subsystem: e.Subsystem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate populates e with the contents of a [uevent.Message].
|
||||||
|
//
|
||||||
|
// The ACTION and DEVPATH environment variables are ignored and assumed to be
|
||||||
|
// consistent with the header.
|
||||||
|
func (e *Event) Populate(reportErr func(error), m *uevent.Message) {
|
||||||
|
if reportErr == nil {
|
||||||
|
reportErr = func(error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = Event{
|
||||||
|
Action: m.Action,
|
||||||
|
DevPath: m.DevPath,
|
||||||
|
Env: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range m.Env {
|
||||||
|
k, v, ok := strings.Cut(s, "=")
|
||||||
|
if !ok {
|
||||||
|
if _, ok = e.Env[s]; !ok {
|
||||||
|
e.Env[s] = "\x00"
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch k {
|
||||||
|
case "ACTION", "DEVPATH":
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "SEQNUM":
|
||||||
|
seq, err := strconv.ParseUint(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
if _e := errors.Unwrap(err); _e != nil {
|
||||||
|
err = _e
|
||||||
|
}
|
||||||
|
reportErr(err)
|
||||||
|
|
||||||
|
e.Env[k] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.Sequence = seq
|
||||||
|
|
||||||
|
case "SYNTH_UUID":
|
||||||
|
var uuid uevent.UUID
|
||||||
|
err := uuid.UnmarshalText(unsafe.Slice(unsafe.StringData(v), len(v)))
|
||||||
|
if err != nil {
|
||||||
|
reportErr(err)
|
||||||
|
|
||||||
|
e.Env[k] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.Synth = &uuid
|
||||||
|
|
||||||
|
case "SUBSYSTEM":
|
||||||
|
e.Subsystem = v
|
||||||
|
|
||||||
|
default:
|
||||||
|
e.Env[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package kobject_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/kobject"
|
||||||
|
"hakurei.app/internal/uevent"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEvent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
msg uevent.Message
|
||||||
|
want kobject.Event
|
||||||
|
errs []error
|
||||||
|
}{
|
||||||
|
{"sample coldboot qemu", uevent.Message{
|
||||||
|
Action: uevent.KOBJ_ADD,
|
||||||
|
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||||
|
Env: []string{
|
||||||
|
"ACTION=add",
|
||||||
|
"DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||||
|
"SUBSYSTEM=acpi",
|
||||||
|
"SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed",
|
||||||
|
"MODALIAS=acpi:LNXPWRBN:",
|
||||||
|
"SEQNUM=777",
|
||||||
|
}}, kobject.Event{
|
||||||
|
Action: uevent.KOBJ_ADD,
|
||||||
|
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||||
|
Env: map[string]string{
|
||||||
|
"MODALIAS": "acpi:LNXPWRBN:",
|
||||||
|
},
|
||||||
|
Sequence: 777,
|
||||||
|
Synth: &uevent.UUID{
|
||||||
|
0xfe, 0x4d, 0x7c, 0x9d,
|
||||||
|
0xb8, 0xc6,
|
||||||
|
0x4a, 0x70,
|
||||||
|
0x9e, 0xf1,
|
||||||
|
0x3d, 0x8a, 0x58, 0xd1, 0x8e, 0xed,
|
||||||
|
},
|
||||||
|
Subsystem: "acpi",
|
||||||
|
}, []error{}},
|
||||||
|
|
||||||
|
{"nil reportErr", uevent.Message{Env: []string{
|
||||||
|
"SEQNUM=\x00",
|
||||||
|
}}, kobject.Event{Env: map[string]string{
|
||||||
|
"SEQNUM": "\x00",
|
||||||
|
}}, nil},
|
||||||
|
|
||||||
|
{"bad SEQNUM SYNTH_UUID", uevent.Message{Env: []string{
|
||||||
|
"SEQNUM=\x00",
|
||||||
|
"SYNTH_UUID=\x00",
|
||||||
|
"SUBSYSTEM=\x00",
|
||||||
|
}}, kobject.Event{Subsystem: "\x00", Env: map[string]string{
|
||||||
|
"SEQNUM": "\x00",
|
||||||
|
"SYNTH_UUID": "\x00",
|
||||||
|
}}, []error{strconv.ErrSyntax, uevent.UUIDSizeError(1)}},
|
||||||
|
|
||||||
|
{"bad sep", uevent.Message{Env: []string{
|
||||||
|
"SYNTH_UUID",
|
||||||
|
}}, kobject.Event{Env: map[string]string{
|
||||||
|
"SYNTH_UUID": "\x00",
|
||||||
|
}}, []error{}},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var f func(error)
|
||||||
|
gotErrs := make([]error, 0)
|
||||||
|
if tc.errs != nil {
|
||||||
|
f = func(err error) {
|
||||||
|
gotErrs = append(gotErrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var got kobject.Event
|
||||||
|
got.Populate(f, &tc.msg)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(&got, &tc.want) {
|
||||||
|
t.Errorf("Populate: %#v, want %#v", got, tc.want)
|
||||||
|
}
|
||||||
|
if tc.errs != nil && !reflect.DeepEqual(gotErrs, tc.errs) {
|
||||||
|
t.Errorf("Populate: errs = %v, want %v", gotErrs, tc.errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,491 @@
|
|||||||
|
// Package kobject interprets uevent messages from a NETLINK_KOBJECT_UEVENT socket.
|
||||||
|
package kobject
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"hakurei.app/internal/report"
|
||||||
|
"hakurei.app/internal/uevent"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StateColdboot denotes an [Object] populated by a coldboot event. It is
|
||||||
|
// eligible for all event actions.
|
||||||
|
StateColdboot = iota
|
||||||
|
// StateNew denotes an [Object] previously populated by a [uevent.KOBJ_ADD]
|
||||||
|
// event, but has not yet been targeted by a [uevent.KOBJ_BIND] event, or
|
||||||
|
// has been targeted by a [uevent.KOBJ_UNBIND] event.
|
||||||
|
StateNew
|
||||||
|
// StateBound denotes an [Object] that has been targeted by a
|
||||||
|
// [uevent.KOBJ_BIND] and has not been targeted by a [uevent.KOBJ_UNBIND]
|
||||||
|
// after that.
|
||||||
|
StateBound
|
||||||
|
)
|
||||||
|
|
||||||
|
// Object represents a kernel object.
|
||||||
|
type Object struct {
|
||||||
|
// Origin of the object.
|
||||||
|
State int `json:"state,omitempty"`
|
||||||
|
// Set by [uevent.KOBJ_OFFLINE] and [uevent.KOBJ_ONLINE].
|
||||||
|
Offline bool `json:"offline,omitempty"`
|
||||||
|
|
||||||
|
// alloc_uevent_skb: devpath
|
||||||
|
DevPath string `json:"devpath"`
|
||||||
|
// registered per-driver (optional)
|
||||||
|
ModAlias string `json:"modalias,omitempty"`
|
||||||
|
// dev_driver_uevent: drv->name (optional)
|
||||||
|
Driver string `json:"driver,omitempty"`
|
||||||
|
|
||||||
|
// SUBSYSTEM value set by the kernel.
|
||||||
|
Subsystem string `json:"subsystem"`
|
||||||
|
|
||||||
|
// Uninterpreted environment variable pairs. An entry missing a separator
|
||||||
|
// gains the value "\x00".
|
||||||
|
Env map[string]string `json:"env"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns the address of a copy of o.
|
||||||
|
func (o *Object) Clone() *Object {
|
||||||
|
v := *o
|
||||||
|
v.Env = maps.Clone(o.Env)
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoString returns compound literal for the underlying value.
|
||||||
|
func (o *Object) GoString() string {
|
||||||
|
return fmt.Sprintf("&%#v", *o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge merges uninterpreted environment variable pairs from an [Event].
|
||||||
|
func (o *Object) merge(env map[string]string) {
|
||||||
|
for k, v := range env {
|
||||||
|
if v == "\x00" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch k {
|
||||||
|
case "MODALIAS":
|
||||||
|
o.ModAlias = v
|
||||||
|
continue
|
||||||
|
|
||||||
|
case "DRIVER":
|
||||||
|
o.Driver = v
|
||||||
|
continue
|
||||||
|
|
||||||
|
default:
|
||||||
|
if o.Env == nil {
|
||||||
|
o.Env = make(map[string]string)
|
||||||
|
}
|
||||||
|
o.Env[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update updates o with pairs from env, optionally stripping visited pairs.
|
||||||
|
func (o *Object) update(env map[string]string, strip bool) {
|
||||||
|
for k := range o.Env {
|
||||||
|
if v, ok := env[k]; ok {
|
||||||
|
if strip {
|
||||||
|
delete(env, k)
|
||||||
|
}
|
||||||
|
o.Env[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pendingIterator is a callback currently iterating through objects targeted
|
||||||
|
// by ongoing events.
|
||||||
|
type pendingIterator struct {
|
||||||
|
f func(o *Object, act uevent.KobjectAction) bool
|
||||||
|
done chan<- struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State processes a stream of [Event] populated from [uevent.Message] received
|
||||||
|
// from a NETLINK_KOBJECT_UEVENT socket and presents an efficient representation
|
||||||
|
// of kernel state.
|
||||||
|
type State struct {
|
||||||
|
// Next expected SEQNUM.
|
||||||
|
seq uint64
|
||||||
|
// DevPath to environment variables.
|
||||||
|
uevent map[string]*Object
|
||||||
|
// Synchronises access to uevent and its objects.
|
||||||
|
ueventMu sync.RWMutex
|
||||||
|
// Alive iterators.
|
||||||
|
iter []*pendingIterator
|
||||||
|
// Synchronises access to iter.
|
||||||
|
iterMu sync.Mutex
|
||||||
|
// UUID for synthetic [uevent.Coldboot] events.
|
||||||
|
coldboot uevent.UUID
|
||||||
|
// Called on [uevent.KOBJ_CHANGE] with stripped environment variables.
|
||||||
|
handleChange func(o *Object, env map[string]string)
|
||||||
|
// Reports errors populating [Event] from [uevent.Message]. A user-supplied
|
||||||
|
// nil value is replaced with a noop.
|
||||||
|
reportErr func(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns the address of a new [State].
|
||||||
|
func New(
|
||||||
|
coldboot uevent.UUID,
|
||||||
|
handleChange func(o *Object, env map[string]string),
|
||||||
|
reportErr func(error),
|
||||||
|
) *State {
|
||||||
|
return &State{
|
||||||
|
uevent: make(map[string]*Object),
|
||||||
|
coldboot: coldboot,
|
||||||
|
handleChange: handleChange,
|
||||||
|
reportErr: reportErr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteIter removes an iterator from s. Must be called after acquiring iterMu.
|
||||||
|
func (s *State) deleteIter(p *pendingIterator) {
|
||||||
|
s.iter = slices.DeleteFunc(s.iter, func(v *pendingIterator) bool {
|
||||||
|
return p == v
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchIter broadcasts an [Object] to all alive iterators.
|
||||||
|
func (s *State) dispatchIter(o *Object, act uevent.KobjectAction) {
|
||||||
|
s.iterMu.Lock()
|
||||||
|
defer s.iterMu.Unlock()
|
||||||
|
|
||||||
|
for _, p := range s.iter {
|
||||||
|
if !p.f(o, act) {
|
||||||
|
s.deleteIter(p)
|
||||||
|
close(p.done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range calls f on all current and upcoming [Object] values tracked by s until
|
||||||
|
// f returns false or the context is cancelled. f must not retain o or modify
|
||||||
|
// the value it points to.
|
||||||
|
func (s *State) Range(
|
||||||
|
ctx context.Context,
|
||||||
|
f func(o *Object, act uevent.KobjectAction) bool,
|
||||||
|
) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
p := pendingIterator{f, done}
|
||||||
|
|
||||||
|
s.iterMu.Lock()
|
||||||
|
s.ueventMu.RLock()
|
||||||
|
for _, o := range s.uevent {
|
||||||
|
if !f(o, uevent.KOBJ_ADD) {
|
||||||
|
s.ueventMu.RUnlock()
|
||||||
|
s.iterMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.ueventMu.RUnlock()
|
||||||
|
s.iter = append(s.iter, &p)
|
||||||
|
s.iterMu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
s.iterMu.Lock()
|
||||||
|
s.deleteIter(&p)
|
||||||
|
s.iterMu.Unlock()
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-done:
|
||||||
|
// deregistered by dispatchIter
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An EventError describes a malformed or inconsistent [Event].
|
||||||
|
type EventError struct {
|
||||||
|
Kind int `json:"fault"`
|
||||||
|
E Event `json:"event"`
|
||||||
|
O *Object `json:"object,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ report.RepresentableError = EventError{}
|
||||||
|
|
||||||
|
func (EventError) Representable() {}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EUnexpectedColdboot is reported for a coldboot event with action other
|
||||||
|
// than the expected [uevent.KOBJ_ADD].
|
||||||
|
EUnexpectedColdboot = iota
|
||||||
|
// EDuplicateAdd is reported for a [uevent.KOBJ_ADD] event on a
|
||||||
|
// still-existing entry that was not the result of a coldboot.
|
||||||
|
EDuplicateAdd
|
||||||
|
// EBadTarget is reported for an event on a nonexistent [Object]. This is
|
||||||
|
// generally only possible before coldboot completes.
|
||||||
|
EBadTarget
|
||||||
|
// ERemoveState is reported for a [uevent.KOBJ_REMOVE] event targeting an
|
||||||
|
// entry in a state other than [StateColdboot] and [StateNew].
|
||||||
|
ERemoveState
|
||||||
|
// EUnexpectedOffline is reported for a [uevent.KOBJ_OFFLINE] or
|
||||||
|
// [uevent.KOBJ_ONLINE] event targeting an already offline or online object.
|
||||||
|
EUnexpectedOffline
|
||||||
|
// EBindState is reported for a [uevent.KOBJ_BIND] event targeting an entry
|
||||||
|
// in a state other than [StateColdboot] and [StateNew].
|
||||||
|
EBindState
|
||||||
|
// EUnbindState is reported for a [uevent.KOBJ_UNBIND] event targeting an
|
||||||
|
// entry in a state other than [StateBound].
|
||||||
|
EUnbindState
|
||||||
|
// EMalformedMove is reported for a [uevent.KOBJ_MOVE] event missing the
|
||||||
|
// DEVPATH_OLD environment variable.
|
||||||
|
EMalformedMove
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e EventError) Error() string {
|
||||||
|
switch e.Kind {
|
||||||
|
case EUnexpectedColdboot:
|
||||||
|
return "unexpected " + e.E.Action.String() + " coldboot event"
|
||||||
|
case EDuplicateAdd:
|
||||||
|
return "duplicate add event on devpath " + strconv.Quote(e.E.DevPath)
|
||||||
|
case EBadTarget:
|
||||||
|
return "unexpected " + e.E.Action.String() + " event on devpath " +
|
||||||
|
strconv.Quote(e.E.DevPath)
|
||||||
|
case ERemoveState:
|
||||||
|
if e.O == nil {
|
||||||
|
return "invalid remove event error"
|
||||||
|
}
|
||||||
|
return "remove event targeting devpath " + strconv.Quote(e.E.DevPath) +
|
||||||
|
" in state " + strconv.Itoa(e.O.State)
|
||||||
|
case EUnexpectedOffline:
|
||||||
|
if e.O == nil {
|
||||||
|
return "invalid unexpected offline error"
|
||||||
|
}
|
||||||
|
if e.O.Offline {
|
||||||
|
return "offline event targeting devpath " + strconv.Quote(e.E.DevPath)
|
||||||
|
}
|
||||||
|
return "online event targeting devpath " + strconv.Quote(e.E.DevPath)
|
||||||
|
case EBindState:
|
||||||
|
if e.O == nil {
|
||||||
|
return "invalid bind state error"
|
||||||
|
}
|
||||||
|
return "bind event targeting devpath " + strconv.Quote(e.E.DevPath) +
|
||||||
|
" in state " + strconv.Itoa(e.O.State)
|
||||||
|
case EUnbindState:
|
||||||
|
if e.O == nil {
|
||||||
|
return "invalid unbind state error"
|
||||||
|
}
|
||||||
|
return "unbind event targeting devpath " + strconv.Quote(e.E.DevPath) +
|
||||||
|
" in state " + strconv.Itoa(e.O.State)
|
||||||
|
case EMalformedMove:
|
||||||
|
return "move event targeting devpath " + strconv.Quote(e.E.DevPath) +
|
||||||
|
" missing DEVPATH_OLD"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "invalid event error kind " + strconv.Itoa(e.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError returns a new [EventError] for e and o.
|
||||||
|
func (e *Event) NewError(kind int, o *Object) error {
|
||||||
|
if o != nil {
|
||||||
|
o = o.Clone()
|
||||||
|
}
|
||||||
|
return EventError{kind, e.Clone(), o}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processEvent merges an event into s.
|
||||||
|
func (s *State) processEvent(e *Event) {
|
||||||
|
s.ueventMu.Lock()
|
||||||
|
defer s.ueventMu.Unlock()
|
||||||
|
|
||||||
|
coldboot := e.Synth != nil
|
||||||
|
if e.Action != uevent.KOBJ_ADD && coldboot {
|
||||||
|
s.reportErr(e.NewError(EUnexpectedColdboot, nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch act := e.Action; act {
|
||||||
|
case uevent.KOBJ_ADD:
|
||||||
|
if e.Synth == nil {
|
||||||
|
if o, ok := s.uevent[e.DevPath]; ok {
|
||||||
|
s.reportErr(e.NewError(EDuplicateAdd, o))
|
||||||
|
o.merge(e.Env)
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o := e.makeColdboot()
|
||||||
|
if !coldboot {
|
||||||
|
o.State = StateNew
|
||||||
|
}
|
||||||
|
o.merge(e.Env)
|
||||||
|
s.uevent[e.DevPath] = o
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
|
||||||
|
case uevent.KOBJ_REMOVE:
|
||||||
|
if o, ok := s.uevent[e.DevPath]; !ok {
|
||||||
|
s.reportErr(e.NewError(EBadTarget, nil))
|
||||||
|
return
|
||||||
|
} else if o.State != StateColdboot && o.State != StateNew {
|
||||||
|
s.reportErr(e.NewError(ERemoveState, o))
|
||||||
|
}
|
||||||
|
delete(s.uevent, e.DevPath)
|
||||||
|
return
|
||||||
|
|
||||||
|
case uevent.KOBJ_CHANGE:
|
||||||
|
o, ok := s.uevent[e.DevPath]
|
||||||
|
if !ok {
|
||||||
|
s.reportErr(e.NewError(EBadTarget, nil))
|
||||||
|
// this suffers from the coldboot race window similar to KOBJ_MOVE,
|
||||||
|
// however this action combines driver-specific and change-specific
|
||||||
|
// environment variables and combines them with environment
|
||||||
|
// variables meant to convey state of the kobject, and it is not
|
||||||
|
// possible to reliably separate them, so this fallback avoids the
|
||||||
|
// race at the cost of including some garbage in tracked state
|
||||||
|
o = e.makeColdboot()
|
||||||
|
o.merge(e.Env)
|
||||||
|
s.uevent[e.DevPath] = o
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o.update(e.Env, true)
|
||||||
|
if s.handleChange != nil {
|
||||||
|
s.handleChange(o, e.Env)
|
||||||
|
}
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
|
||||||
|
case uevent.KOBJ_MOVE:
|
||||||
|
var o *Object
|
||||||
|
if old, ok := e.Env["DEVPATH_OLD"]; !ok {
|
||||||
|
s.reportErr(e.NewError(EMalformedMove, nil))
|
||||||
|
// not reached
|
||||||
|
o = e.makeColdboot()
|
||||||
|
} else if o, ok = s.uevent[old]; !ok {
|
||||||
|
s.reportErr(e.NewError(EBadTarget, nil))
|
||||||
|
// this generally happens during coldboot, dropping the event here
|
||||||
|
// may cause inconsistent state if the coldboot event for this
|
||||||
|
// object was generated before the bind event
|
||||||
|
delete(e.Env, "DEVPATH_OLD")
|
||||||
|
o = e.makeColdboot()
|
||||||
|
} else {
|
||||||
|
delete(s.uevent, old)
|
||||||
|
delete(e.Env, "DEVPATH_OLD")
|
||||||
|
}
|
||||||
|
o.merge(e.Env)
|
||||||
|
s.uevent[e.DevPath] = o
|
||||||
|
o.DevPath = e.DevPath
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
|
||||||
|
case uevent.KOBJ_ONLINE:
|
||||||
|
o, ok := s.uevent[e.DevPath]
|
||||||
|
if !ok {
|
||||||
|
s.reportErr(e.NewError(EBadTarget, nil))
|
||||||
|
// coldboot race window similar to an unexpected KOBJ_MOVE
|
||||||
|
o = e.makeColdboot()
|
||||||
|
s.uevent[e.DevPath] = o
|
||||||
|
o.merge(e.Env)
|
||||||
|
}
|
||||||
|
if !o.Offline {
|
||||||
|
s.reportErr(e.NewError(EUnexpectedOffline, o))
|
||||||
|
}
|
||||||
|
o.Offline = false
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
|
||||||
|
case uevent.KOBJ_OFFLINE:
|
||||||
|
o, ok := s.uevent[e.DevPath]
|
||||||
|
if !ok {
|
||||||
|
s.reportErr(e.NewError(EBadTarget, nil))
|
||||||
|
// coldboot race window similar to an unexpected KOBJ_MOVE
|
||||||
|
o = e.makeColdboot()
|
||||||
|
s.uevent[e.DevPath] = o
|
||||||
|
o.merge(e.Env)
|
||||||
|
}
|
||||||
|
if o.Offline {
|
||||||
|
s.reportErr(e.NewError(EUnexpectedOffline, o))
|
||||||
|
}
|
||||||
|
o.Offline = true
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
|
||||||
|
case uevent.KOBJ_BIND:
|
||||||
|
o, ok := s.uevent[e.DevPath]
|
||||||
|
if !ok {
|
||||||
|
s.reportErr(e.NewError(EBadTarget, nil))
|
||||||
|
// coldboot race window similar to an unexpected KOBJ_MOVE
|
||||||
|
o = e.makeColdboot()
|
||||||
|
s.uevent[e.DevPath] = o
|
||||||
|
}
|
||||||
|
if o.State != StateColdboot && o.State != StateNew {
|
||||||
|
s.reportErr(e.NewError(EBindState, o))
|
||||||
|
}
|
||||||
|
o.State = StateBound
|
||||||
|
o.merge(e.Env)
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
|
||||||
|
case uevent.KOBJ_UNBIND:
|
||||||
|
o, ok := s.uevent[e.DevPath]
|
||||||
|
if !ok {
|
||||||
|
s.reportErr(e.NewError(EBadTarget, nil))
|
||||||
|
// coldboot race window similar to an unexpected KOBJ_MOVE, but does
|
||||||
|
// not result in inconsistent state if dropped
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if o.State != StateBound {
|
||||||
|
s.reportErr(e.NewError(EUnbindState, o))
|
||||||
|
}
|
||||||
|
o.State = StateNew
|
||||||
|
o.Driver = ""
|
||||||
|
s.dispatchIter(o, act)
|
||||||
|
return
|
||||||
|
|
||||||
|
default: // not reached
|
||||||
|
s.reportErr(fmt.Errorf("invalid action %d", e.Action))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadSequenceError is reported for an unexpected SEQNUM.
|
||||||
|
type BadSequenceError struct{ Got, Want uint64 }
|
||||||
|
|
||||||
|
func (e BadSequenceError) Error() string {
|
||||||
|
return "SEQNUM=" + strconv.FormatUint(e.Got, 10) +
|
||||||
|
", want " + strconv.FormatUint(e.Want, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume receives uevent messages and updates s to reflect state of kernel.
|
||||||
|
func (s *State) Consume(ctx context.Context, events <-chan *uevent.Message) {
|
||||||
|
if s.uevent == nil {
|
||||||
|
s.uevent = make(map[string]*Object)
|
||||||
|
}
|
||||||
|
if s.reportErr == nil {
|
||||||
|
s.reportErr = func(error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var e Event
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
case m, ok := <-events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.Populate(s.reportErr, m)
|
||||||
|
|
||||||
|
// skip external synthetic event
|
||||||
|
if e.Synth != nil && *e.Synth != s.coldboot {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.seq == 0 {
|
||||||
|
s.seq = e.Sequence
|
||||||
|
}
|
||||||
|
if s.seq != e.Sequence {
|
||||||
|
s.reportErr(BadSequenceError{e.Sequence, s.seq})
|
||||||
|
}
|
||||||
|
s.seq++
|
||||||
|
s.processEvent(&e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user