-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
1409 lines (1347 loc) · 61 KB
/
Copy pathinit.lua
File metadata and controls
1409 lines (1347 loc) · 61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- ╔══════════════════════════════════════╗
-- ║ TOS init.lua - System Bootstrap ║
-- ║ Terminal Operating System v1.5.0 ║
-- ╚══════════════════════════════════════╝
-- Works with TOS BIOS (receives bootFS as arg)
-- AND with standard Lua BIOS (finds bootFS itself)
-- bootFS may be passed by TOS BIOS, or nil if standard BIOS
local bootFS = ...
-- In OC, component and computer are machine globals.
-- Do NOT use require() here - it doesn't exist yet.
local component = component
local computer = computer
-- ============================================================
-- Lua architecture guard
-- ============================================================
-- Several kernel modules (crypto, net, display, jbod, vault, compress,
-- backup, launcher) use Lua 5.3 bitwise-operator SYNTAX, and the boot
-- chain (BIOS TBFS reader, blockfs) needs string.pack. OC lets players
-- switch a CPU to the Lua 5.2 architecture; there, display.lua — on the
-- mandatory boot path — fails to PARSE, so without this probe the
-- failure mode is a kernel panic showing a raw syntax error from a
-- perfectly healthy disk. Probe the parser itself (this file contains
-- no 5.3 syntax, so it always gets far enough to run) and halt with
-- the actual fix. NOTE: keep this file 5.2-parseable.
-- Lua 5.4 is SUPPORTED on purpose: it parses 5.3 syntax and provides
-- every stdlib feature TOS uses (integers are 64-bit signed in both;
-- code that cares — crypto's unsigned compares — already uses
-- math.ult). A parser-feature probe passes there; keep it that way —
-- never replace this with a version-string or architecture-name
-- compare (test_bios pins the probe in both boot files).
if not load("return 1<<1") then
local g
for a in component.list("gpu") do
local ok, px = pcall(component.proxy, a)
if ok and px then g = px break end
end
local scr
for a in component.list("screen") do scr = a break end
if g and scr then
pcall(g.bind, scr)
pcall(g.setBackground, 0x000000)
pcall(g.setForeground, 0xFFFFFF)
pcall(g.fill, 1, 1, 80, 25, " ")
pcall(g.set, 2, 2, "TOS requires the Lua 5.3 or 5.4 CPU architecture.")
pcall(g.set, 2, 3, "This CPU is running an older one (Lua 5.2).")
pcall(g.set, 2, 5, "Fix: sneak-click (crouch + right-click) the CPU in its")
pcall(g.set, 2, 6, "computer-case slot to cycle architectures, then reboot.")
pcall(g.set, 2, 8, "Press any key to power off.")
end
pcall(computer.beep, 800, 0.3)
while true do
local ev = computer.pullSignal(math.huge)
if ev == "key_down" then computer.shutdown() end
end
end
-- ============================================================
-- Stage 0: Find boot filesystem
-- ============================================================
-- TBFS unmanaged boot: the stage-2 bootstrap (blockfs.BOOTSTRAP, run by
-- the TOS BIOS from a raw drive's boot region) already mounted the volume
-- and left the root proxy here. Prefer it — on that path getBootAddress
-- points at a `drive` component, which must NOT be proxied as a
-- filesystem (no exists/open; every fallback below would misfire).
if not bootFS and _G._TOS_UNMANAGED_ROOT then
bootFS = _G._TOS_UNMANAGED_ROOT
end
-- If TOS BIOS passed it, great. Otherwise find it ourselves.
if not bootFS then
-- Try computer.getBootAddress (added by standard Lua BIOS or TOS BIOS)
local getBA = computer.getBootAddress
if getBA then
local addr = getBA()
if addr and addr ~= "" then
local ok, px = pcall(component.proxy, addr)
if ok and px then bootFS = px end
end
end
end
-- exists() is pcall'd in both scans (#REV review finding #6): a floppy
-- yanked between the proxy and the invoke makes the component call
-- RAISE, which otherwise kills the whole scan instead of skipping the
-- dead device.
local function safeExists(px, path)
local ok, r = pcall(px.exists, path)
return ok and r
end
--! #FIX (in-game, 2026-08-11) — TOS needs a WRITABLE root. It writes
--! /etc/users.dat before an operator has even finished the First Boot
--! password prompt, plus /var/log, /home and the whole package tree.
--! Nothing checked, so a read-only device booted "successfully" and the
--! first symptom was `Persist failed: read-only filesystem` in the
--! middle of setting the root password — with no hint of which disk was
--! at fault or that the disk was the fault at all.
local function safeReadOnly(px)
if type(px) ~= "table" or type(px.isReadOnly) ~= "function" then return false end
local ok, ro = pcall(px.isReadOnly)
return ok and ro == true
end
--! Both scans below now run TWICE: once accepting only writable
--! filesystems, then once accepting anything. A read-only disk carrying
--! /tos is a fine thing to READ an OS from and a hopeless thing to run
--! one on, so it is the last choice rather than the first one that
--! happens to enumerate — but it is still better than not booting.
local function scanFor(marker)
for _, requireWritable in ipairs({ true, false }) do
for addr in component.list("filesystem") do
local ok, px = pcall(component.proxy, addr)
if ok and px and px.exists and safeExists(px, marker) then
if not (requireWritable and safeReadOnly(px)) then return px end
end
end
end
end
if not bootFS then bootFS = scanFor("/tos/kernel/init.lua") end
if not bootFS then bootFS = scanFor("/init.lua") end
if not bootFS then
error("FATAL: Cannot find boot filesystem!")
end
-- ============================================================
-- Stage 0a: is the root writable?
-- ============================================================
--! Said HERE, at boot, rather than discovered later by whichever write
--! happened to come first. A machine that cannot write its own user
--! database cannot be set up, and the operator needs to know that before
--! they type a password into a prompt that is going to throw it away.
--!
--! Not fatal: a read-only root still boots to a usable read-only system,
--! and being able to look around is exactly what you want when
--! diagnosing this. But it is announced, loudly, once.
_G._TOS_ROOT_READONLY = safeReadOnly(bootFS)
if _G._TOS_ROOT_READONLY then
local gpuAddr = component.list("gpu")()
local scrAddr = component.list("screen")()
if gpuAddr and scrAddr then
local okG, gpu = pcall(component.proxy, gpuAddr)
if okG and gpu then
pcall(gpu.bind, scrAddr)
pcall(gpu.setBackground, 0x000000)
pcall(gpu.setForeground, 0xFF5555)
local okR, w = pcall(gpu.getResolution)
pcall(gpu.fill, 1, 1, (okR and w) or 80, 6, " ")
pcall(gpu.set, 2, 2, "WARNING: the boot filesystem is READ-ONLY.")
pcall(gpu.setForeground, 0xAAAAAA)
pcall(gpu.set, 2, 3, "Disk " .. tostring(bootFS.address or "?"):sub(1, 8)
.. "... Nothing TOS writes will survive, and First Boot Setup")
pcall(gpu.set, 2, 4, "cannot set a root password at all.")
pcall(gpu.set, 2, 5, "Install to a writable drive (run install.lua), or unprotect this one.")
pcall(computer.pullSignal, 5)
end
end
end
-- ============================================================
-- Floppy detection: If booting from a small/removable disk
-- while a larger drive exists, offer to install there instead.
-- ============================================================
do
local bootTotal = bootFS.spaceTotal()
local FLOPPY_THRESHOLD = 524288 -- 512KB (floppies are ~512KB)
--! #FIX (loot disk, 2026-09-13) — a READ-ONLY boot disk gets the offer
--! at any size. OpenComputers mounts a loot disk (the TOS floppy from a
--! dungeon chest) through ReadOnlyWrapper, whose spaceTotal() returns
--! spaceUsed() — the size of its contents, ~1.8 MB for TOS (verified in
--! OC 1.8.10). So the disk that most needed this offer never passed the
--! size test, and booted into a root that cannot keep a password
--! (Stage 0a). test_init_readonly_install pins this.
local bootReadOnly = _G._TOS_ROOT_READONLY
-- #SEC H1 — a one-time boot (operator chose Shift+Enter at the BIOS for a
-- changed/fallback drive) explicitly declined to touch boot config. The
-- migration flow below can re-flash the EEPROM via setBootAddress, so honour
-- that intent: skip the offer entirely on a one-time boot. The operator can
-- still migrate deliberately on a normal (EEPROM-committed) boot.
if _G._BIOS_ONETIME then
bootTotal = nil
end
if bootTotal and (bootTotal <= FLOPPY_THRESHOLD or bootReadOnly) then
-- Look for a larger disk to install onto
local largerDisk = nil
local largerTotal = 0
for addr in component.list("filesystem") do
if addr ~= bootFS.address then
local ok2, px = pcall(component.proxy, addr)
--! A read-only disk is never a target, however large: a second loot
--! disk would otherwise beat the hard drive and fail the copy on its
--! first write.
if ok2 and px and not safeReadOnly(px) then
local t2 = px.spaceTotal()
if t2 and t2 > bootTotal and t2 > largerTotal then
largerDisk = px
largerTotal = t2
end
end
end
end
if largerDisk then
-- We're booting from a floppy but a hard drive exists.
-- Show a choice to the user.
local g2
for addr in component.list("gpu") do g2 = component.proxy(addr) break end
local s2
for addr in component.list("screen") do s2 = addr break end
if g2 and s2 then
pcall(g2.bind, s2)
g2.setBackground(0x000000)
g2.setForeground(0xFFFFFF)
local sw, sh = g2.getResolution()
g2.fill(1, 1, sw, sh, " ")
local function gp(y, text, fg)
g2.setForeground(fg or 0xFFFFFF)
g2.set(2, y, tostring(text))
end
local bKB = math.floor(bootTotal / 1024)
local dKB = math.floor(largerTotal / 1024)
-- Every line here fits a Tier 1 screen (50 columns).
gp(2, (bootReadOnly and "TOS is running from a read-only disk ("
or "TOS is running from a floppy disk (") .. bKB .. "KB)", 0xFFFF00)
gp(3, "A larger drive was detected (" .. dKB .. "KB)", 0xFFFF00)
gp(4, " source: " .. (bootFS.address or "?"):sub(1, 12) .. "...", 0xAAAAAA)
gp(5, " target: " .. (largerDisk.address or "?"):sub(1, 12) .. "...", 0xAAAAAA)
-- #SEC H2 — surface both component addresses so the operator
-- can confirm what's about to happen before approving an
-- irreversible disk overwrite + EEPROM reflash.
gp(7, "1. Install TOS to the hard drive (recommended)", 0x00FF00)
gp(8, bootReadOnly and "2. Continue read-only (nothing is saved)"
or "2. Continue booting from floppy (limited space)", 0xAAAAAA)
gp(10, "Press 1 or 2:", 0xFFFFFF)
-- Wait for keypress
while true do
local sig, _, char = computer.pullSignal(60)
if sig == "key_down" then
if char == 49 then -- "1"
-- Run install.lua if available on the floppy
if bootFS.exists("/install.lua") then
-- Load and execute install.lua, passing the floppy's bootFS
g2.fill(1, 1, sw, sh, " ")
gp(2, "Starting installer...", 0x00AAFF)
-- install.lua needs OpenOS require which we don't have
-- yet. Instead of loading it here (the source is never
-- actually executed — the old code read ~17 KB into a
-- buffer and then threw it away), we copy the TOS tree
-- directly to the target disk and chain-load from there.
g2.fill(1, 1, sw, sh, " ")
gp(2, "Copying TOS to hard drive...", 0x00AAFF)
local y2 = 4
local copied2 = 0
-- Recursive copy from floppy to target disk.
-- #107 — previously this silently ignored write failures,
-- truncated reads, and partial files. We now surface the
-- first failure up to the caller so the installer can
-- refuse to flash the BIOS when the copy didn't complete.
local copyFailed = nil
-- Fail FAST on space (#REV review finding #5): sum the
-- source tree before copying instead of half-copying and
-- then failing verification.
local function treeSize(srcFS, dir)
local total = 0
local iter = srcFS.list(dir)
if not iter then return total end
for _, name in ipairs(iter) do
local p = dir .. name
if srcFS.isDirectory(p) then
total = total + treeSize(srcFS, p)
else
total = total + (srcFS.size(p) or 0)
end
end
return total
end
local function copyDir(srcFS, dstFS, dir)
if copyFailed then return end
local iter = srcFS.list(dir)
if not iter then return end
for _, name in ipairs(iter) do
if copyFailed then return end
local srcPath = dir .. name
if srcFS.isDirectory(srcPath) then
dstFS.makeDirectory(srcPath)
copyDir(srcFS, dstFS, srcPath)
else
local fh = srcFS.open(srcPath, "r")
if not fh then
copyFailed = "open-src " .. srcPath; return
end
local wh = dstFS.open(srcPath, "w")
if not wh then
srcFS.close(fh)
copyFailed = "open-dst " .. srcPath; return
end
-- Stream chunk-by-chunk (#REV finding #5): the old
-- whole-file write buffered everything in RAM and a
-- mid-file out-of-space surfaced only at the end —
-- and its `wOk == false` check let a nil-shaped
-- write failure through as success. `not wOk`
-- catches both failure shapes at the failing chunk.
while true do
local chunk = srcFS.read(fh, 4096)
if not chunk then break end
local wOk, wErr = dstFS.write(wh, chunk)
if not wOk then
copyFailed = "write " .. srcPath .. ": " .. tostring(wErr)
break
end
end
srcFS.close(fh)
dstFS.close(wh)
if copyFailed then return end
copied2 = copied2 + 1
end
end
end
-- Space fail-fast: refuse before the first write, not
-- after a half-copy fails verification. (+8KB slack for
-- directory/metadata overhead.)
local needed = treeSize(bootFS, "/") + 8192
local dstFree = (largerDisk.spaceTotal() or 0)
- (largerDisk.spaceUsed() or 0)
if dstFree < needed then
gp(y2, "Target too full: need " .. math.floor(needed / 1024)
.. "KB, only " .. math.floor(dstFree / 1024) .. "KB free", 0xFF0000)
y2 = y2 + 1
gp(y2, "Nothing was written. Free up the disk and retry.", 0xFFAA00)
while true do local ev = computer.pullSignal(1e9)
if ev == "key_down" then computer.shutdown(true) end
end
end
-- Create core directories on target
local coreDirs = {
"/tos/", "/tos/kernel/", "/tos/kernel/net/", "/tos/shell/",
"/tos/compat/", "/tos/peripheral/",
"/etc/", "/etc/rc.d/", "/home/", "/root/", "/public/",
"/usr/", "/usr/bin/", "/usr/lib/", "/usr/modules/",
"/var/", "/var/log/", "/var/run/", "/tmp/",
}
for _, d in ipairs(coreDirs) do
largerDisk.makeDirectory(d)
end
copyDir(bootFS, largerDisk, "/")
if copyFailed then
gp(y2, "Copy FAILED: " .. copyFailed, 0xFF0000)
y2 = y2 + 1
gp(y2, "BIOS not updated. Remove disk to try again.", 0xFFAA00)
while true do local ev = computer.pullSignal(1e9)
if ev == "key_down" then computer.shutdown(true) end
end
end
gp(y2, "Copied " .. copied2 .. " files", 0x00FF00)
y2 = y2 + 1
-- #107 — verify critical boot files before flashing BIOS.
-- If the copy truncated mid-file, the machine would otherwise
-- brick on next boot.
local ok1 = largerDisk.exists("/init.lua")
local ok2 = largerDisk.exists("/tos/kernel/init.lua")
local sz1 = ok1 and largerDisk.size("/init.lua") or 0
local sz2 = ok2 and largerDisk.size("/tos/kernel/init.lua") or 0
if not (ok1 and ok2 and sz1 > 0 and sz2 > 0) then
gp(y2, "Verification FAILED — BIOS not updated", 0xFF0000)
y2 = y2 + 1
gp(y2, " init.lua=" .. tostring(sz1) .. " kernel=" .. tostring(sz2), 0xFF6600)
while true do local ev = computer.pullSignal(1e9)
if ev == "key_down" then computer.shutdown(true) end
end
end
-- #SEC H2 — require an explicit second confirmation
-- before re-flashing the EEPROM. Auto-setBootAddress
-- was the audit's main floppy-bootkit lever: any floppy
-- left in the drive could silently permanently rebind
-- the boot device. Operator must type Y now; otherwise
-- the install stays on disk but the EEPROM still points
-- at the floppy and the operator can reboot with the
-- floppy removed to switch drives manually.
gp(y2, "Files copied. Update EEPROM to boot from hard drive?", 0xFFAA00)
y2 = y2 + 1
gp(y2, " [Y] update EEPROM [N] leave EEPROM alone", 0xAAAAAA)
y2 = y2 + 1
gp(y2, " Target: " .. (largerDisk.address or "?"):sub(1, 12) .. "...", 0xAAAAAA)
y2 = y2 + 1
local doFlash = false
local t0 = computer.uptime()
while computer.uptime() - t0 < 30 do
local s3, _, ch3 = computer.pullSignal(30)
if s3 == "key_down" then
if ch3 == 121 or ch3 == 89 then doFlash = true end
break
end
end
if doFlash then
gp(y2, "Setting boot drive...", 0x00AAFF)
computer.setBootAddress(largerDisk.address)
y2 = y2 + 1
else
gp(y2, "EEPROM unchanged — remove floppy and reboot to use HD", 0xFFAA00)
y2 = y2 + 1
end
gp(y2, "", 0xFFFFFF)
y2 = y2 + 1
gp(y2, "TOS installed to hard drive!", 0x00FF00)
y2 = y2 + 1
gp(y2, "Rebooting in 3 seconds...", 0xAAAAAA)
computer.pullSignal(3)
computer.shutdown(true)
else
gp(10, "install.lua not found on floppy!", 0xFF0000)
gp(11, "Booting from floppy instead...", 0xAAAAAA)
computer.pullSignal(2)
end
break
elseif char == 50 then -- "2"
break -- Continue booting from floppy
end
elseif not sig then
break -- Timeout: continue booting from floppy
end
end
end
end
end
end
-- ============================================================
-- Global TOS state
-- ============================================================
_G._TOS = {
version = "1.5.0",
codename = "Aletheia",
-- Which tree this is. The release build (build/strip.lua) rewrites both
-- placeholders: build = the commit it was built from ("-dirty" if the
-- source had uncommitted changes), variant = how it was stripped. Run
-- from source, they stay "source". Must appear exactly once each.
build = "source",
variant = "source",
bootFS = bootFS,
-- #REV (#8) — the boot filesystem's component address. The shell's
-- auto-mount gate (shell.panels autoMount, #SEC H26) refuses to mount
-- anything until this is set (fail-closed so an inserted disk can't
-- shadow the boot FS). It was NEVER set, so EVERY inserted floppy was
-- silently refused ("inserting a disk does nothing"). Set it here from
-- the boot proxy so removable-media auto-mount works.
bootAddr = bootFS and bootFS.address or nil,
bootTime = computer.uptime(),
startMem = computer.freeMemory(),
totalMem = computer.totalMemory(),
}
-- ============================================================
-- Stage 1: Build require() system
-- ============================================================
local loaded = {}
local loading = {}
-- Kernel /tos first so a package can never shadow a kernel module by name;
-- user-installed roots after it, in the order pkg uses them.
--
-- /usr/modules MUST be here: kernel.sandbox's USER_LIB_ROOTS already lists
-- it as a place package code may require from, and its resolver checks the
-- file exists there before handing off to THIS require. Without the entry
-- the two disagreed — the sandbox authorized the require, then the load
-- failed with "Module not found" (emulator round: a package with a
-- multi-file module under /usr/modules, e.g. `calc` requiring calc.sheet,
-- could not start at all). Single-file packages never noticed because pkg
-- loads a command's entry by absolute PATH, not by module name.
-- test_require_roots pins the two lists together.
--! #SEC (pentest, Sep 2026) — /usr/bin is NOT a library root. It held
--! package COMMAND files, which pkg runs by path inside the package's
--! sandbox; on this list the same file was also a module this require
--! loads into the kernel's own _G. The shell pcall-requires add-on names
--! in kernel context (`require("mail")`), so any package shipping a
--! command called mail.lua ran it unsandboxed at the next `mail`. The
--! sandbox's USER_LIB_ROOTS never listed /usr/bin, so nothing sandboxed
--! lost anything. test_pkg_protected_targets pins this.
local searchPaths = {
"/tos/?.lua",
"/tos/?/init.lua",
"/lib/?.lua",
"/usr/lib/?.lua",
"/usr/modules/?.lua",
"/usr/modules/?/init.lua",
}
-- Package compat: loaded table accessible as package.loaded
-- (needed by compat/init.lua to register OpenOS shims)
_G.package = { loaded = nil } -- Set after loaded table creation
local function readFile(path)
if not bootFS.exists(path) then return nil end
local h = bootFS.open(path, "r")
if not h then return nil end
local parts = {}
while true do
local chunk = bootFS.read(h, 4096)
if not chunk then break end
parts[#parts + 1] = chunk
end
bootFS.close(h)
return table.concat(parts)
end
-- #MEM — compile a module without ever holding TWO full copies of it.
-- The original path built a table of 4KB chunks, table.concat'd them into
-- one string, and load()ed that: source-in-pieces + joined copy + compiled
-- chunk all live at once (~2x the file plus the chunk). On low-RAM boxes
-- that peak was the "not enough memory for buffer allocation" shell-load
-- crash. We drop the joined copy by feeding load() from the chunk table and
-- releasing each piece as it is consumed.
--
-- #BUG (emulator round, 2026-07-24) — the reader must NOT touch the
-- filesystem. An earlier version read 4KB per reader call, which is the
-- textbook streaming form and would have kept the peak at one chunk; in
-- OpenComputers it panics the kernel outright:
--
-- Syntax error in 'kernel': attempt to yield across a C-call boundary
--
-- Component calls can YIELD — OC gives each machine a direct-call budget
-- and forces a yield when it runs out — and load() is a C function, so a
-- yield inside its reader crosses a C-call boundary and is fatal. It is not
-- RAM-dependent or intermittent: it killed boot on every machine. So do all
-- I/O FIRST (yielding there is fine, it's ordinary Lua), then compile from
-- memory, where the reader can only do table lookups.
--
-- Returns (found, fn, err, mode): found=false → no such file, keep
-- searching; found=true with fn=nil → the file exists but could not be
-- turned into a chunk, with `mode` saying which half failed ("read" or
-- "compile"). The caller words the error from that: reporting a failed
-- READ as a syntax error is how this very bug wasted diagnosis time — the
-- boot panic read "Syntax error in 'kernel'" when the file was fine and
-- the real fault was a yield during loading.
local function loadModuleFile(path)
if not bootFS.exists(path) then return false end
local h = bootFS.open(path, "r")
if not h then return false end
local chunks, n = {}, 0
local readOk, readErr = pcall(function()
while true do
local chunk = bootFS.read(h, 4096)
if chunk == nil then break end
-- Lua treats "" from a reader as end-of-chunk, so a short read must
-- never reach load() — it would compile a silent PREFIX of the module.
if #chunk > 0 then n = n + 1; chunks[n] = chunk end
end
end)
bootFS.close(h)
if not readOk then return true, nil, readErr, "read" end
-- Force text-only mode ("t"). Bytecode (mode "b" or "bt") is accepted
-- by load() if bit5.band was ever compiled in, and loading
-- attacker-crafted bytecode bypasses all Lua validity checks
-- (arbitrary memory access, type-confusion, etc). Only source
-- text may enter the kernel boot path.
local i = 0
local okL, fn, err = pcall(load, function()
i = i + 1
local c = chunks[i]
chunks[i] = nil -- release as we go; source frees while compiling
return c -- nil past the end ends the chunk
end, "=" .. path, "t")
if not okL then return true, nil, fn, "compile" end -- compile RAISED (e.g. OOM)
return true, fn, err, "compile"
end
-- OpenOS library names served by the compat layer (tos/compat/init.lua).
-- The layer used to be loaded whole at boot; now it loads on the FIRST
-- require() of one of these names (see the fallback in tosRequireBody).
-- Checked BEFORE the search paths so precedence matches the old behavior,
-- where compat pre-registered these names in the require cache at boot
-- (e.g. a /usr/lib/text.lua add-on must not shadow the OpenOS shim).
--! MUST list every name compat.init registers, or that name's first
--! require falls past this hook into the search path.
--!
--! `internet` was missing. compat.init registers it, but nothing here
--! triggered on it, so require("internet") went to the path search and
--! found OpenOS's /lib/internet.lua -- TOS quietly running OpenOS's
--! library instead of its own shim. Worse, it was ORDER-DEPENDENT: touch
--! any other shim name first and compat is already initialized, so
--! package.loaded has "internet" and it resolves correctly. Touch
--! `internet` first, on a disk with no OpenOS underneath it, and the
--! require fails outright.
--!
--! Two lists that have to agree, with nothing checking. Now checked:
--! test_openos_compat.lua compares them and fails on any divergence.
local OPENOS_SHIMS = {
sides = true, colors = true, keyboard = true, text = true,
serialization = true, buffer = true, term = true, filesystem = true,
event = true, shell = true, io = true, internet = true,
robot = true, process = true, note = true,
}
local function tosRequireBody(name)
-- Lazy OpenOS compat: first touch of a shim name loads + initializes the
-- whole layer (the shims cross-register each other, so per-module lazy
-- loading isn't meaningful). The kernel sets _TOS.compatDisabled when the
-- boot profile gates compat off — honor it so Safe Mode still refuses to
-- run OpenOS code, exactly as when the layer wasn't loaded at boot.
if OPENOS_SHIMS[name] and not loaded[name] then
local T = _G._TOS
if not (T and T.compatDisabled) then
-- _G.require is tosRequire (assigned right after these definitions);
-- called through the global because tosRequire's local isn't in
-- scope yet at this point in the file.
local okC, compatMod = pcall(_G.require, "compat")
if okC and compatMod and compatMod.init then
pcall(compatMod.init, { procSleep = T and T.proc and T.proc.sleep })
end
if loaded[name] then return loaded[name] end
end
end
local modName = name:gsub("%.", "/")
local tried = {}
for _, pattern in ipairs(searchPaths) do
local path = pattern:gsub("%?", modName)
local found, fn, err, mode = loadModuleFile(path)
if found then
if not fn then
error((mode == "read" and "Read error in '" or "Syntax error in '")
.. name .. "' (" .. path .. "): " .. tostring(err), 2)
end
local ok, result = pcall(fn, name)
if not ok then
error("Runtime error in '" .. name .. "': " .. tostring(result), 2)
end
if result == nil then result = true end
loaded[name] = result
return result
end
tried[#tried + 1] = path
end
error("Module not found: " .. name .. "\nSearched:\n " .. table.concat(tried, "\n "), 2)
end
local function tosRequire(name)
if loaded[name] then return loaded[name] end
if loading[name] then
error("Circular dependency: " .. name, 2)
end
loading[name] = true
-- #REV (v1.4.0 emulator round) — the body can RAISE from places the
-- old inline code didn't guard: on a low-RAM box, bootFS.read inside
-- readFile (or load() itself) propagates "not enough memory" BEFORE
-- any of the explicit `loading[name] = nil` lines ran. The marker
-- then stayed set, so the shell's OOM-nudge-GC-and-retry hit a bogus
-- "Circular dependency: shell.panels.commands.core" — which the
-- command loader rightly treats as a permanent code error and caches,
-- walling off every core command for the session. Run the body under
-- pcall and ALWAYS clear the marker, so a transient failure stays
-- transient and "Circular dependency" again means only actual cycles.
local ok, result = pcall(tosRequireBody, name)
loading[name] = nil
if not ok then error(result, 0) end
return result
end
_G.require = tosRequire
-- Pre-register machine globals as modules so require("computer") etc. works
loaded["component"] = component
loaded["computer"] = computer
if unicode then loaded["unicode"] = unicode end
-- Expose loaded table as package.loaded for OpenOS compat
_G.package.loaded = loaded
-- ============================================================
-- Stage 1b: Boot configuration (the everything → nothing spectrum)
-- ============================================================
-- Read VERY early and FAIL-SAFE: a missing/corrupt /etc/boot.cfg yields the
-- 'normal' profile so boot always proceeds. Drives the verbosity "muter",
-- the System Configuration POST screen, and the optional-stage gates (#4).
local bootcfgMod, bootCfg, verbosity
do
local okB, m = pcall(require, "kernel.bootcfg")
if okB and m then
bootcfgMod = m
local ok2, cfg = pcall(m.load, {
exists = function(p) return bootFS.exists(p) end,
readFile = function(p) return readFile(p) end,
})
bootCfg = ok2 and cfg or nil
end
verbosity = (bootcfgMod and bootCfg) and bootcfgMod.verbosity(bootCfg) or "text"
_G._TOS.bootcfg = bootCfg
_G._TOS.bootcfgMod = bootcfgMod
end
-- Verbosity → log early-echo threshold (DEBUG=0 … FATAL=4). Canonical mapping
-- lives in kernel.bootcfg (single source of truth, unit-tested); fall back to
-- a local copy if bootcfg didn't load.
local earlyMinLevel
if bootcfgMod and bootcfgMod.echoMinLevel then
earlyMinLevel = bootcfgMod.echoMinLevel(verbosity)
else
earlyMinLevel = ({ silent = 4, splash = 2, text = 1, verbose = 0 })[verbosity] or 1
end
-- ============================================================
-- Stage 2: GPU + Early Display
-- ============================================================
local gpuAddr
for addr in component.list("gpu") do gpuAddr = addr break end
local screenAddr
for addr in component.list("screen") do screenAddr = addr break end
local gpu
if gpuAddr and screenAddr then
gpu = component.proxy(gpuAddr)
-- Skip bind() if TOS BIOS already did it: gpu.bind() resets the GPU buffer
-- (clears screen) in OC even when re-binding the same screen.
if not _G._BIOS_CY then
gpu.bind(screenAddr)
end
_G._TOS.gpu = gpu
end
local screenW, screenH = 50, 16
if gpu then
screenW, screenH = gpu.getResolution()
end
-- Detect GPU color depth for tier-safe rendering
local gpuDepth = _G._BIOS_DEPTH or 1
if gpu then
local ok, d = pcall(gpu.getDepth)
if ok and d then gpuDepth = d end
end
local isMonochrome = gpuDepth <= 1
--- Map a color to a tier-safe value (T1 → white only)
local function tc(color)
if isMonochrome then return 0xFFFFFF end
return color
end
local cursorY = 1
-- Boot reference time for the verbose timing prefix. _G._TOS.bootTime was
-- captured at the top of this file; fall back to "now" if it's missing.
local bootT0 = (_G._TOS and _G._TOS.bootTime) or computer.uptime()
local function earlyPrint(text, color)
if not gpu then return end
text = tostring(text)
-- "verbose" promises timings (see kernel.bootcfg's VERBOSITY note): on a
-- verbose boot, stamp every early line with ms-since-boot so verbose is
-- visibly distinct from text instead of identical to it.
if verbosity == "verbose" then
local ms = math.floor((computer.uptime() - bootT0) * 1000)
text = string.format("[%6dms] %s", ms, text)
end
if color then gpu.setForeground(tc(color)) end
if cursorY > screenH then
gpu.copy(1, 2, screenW, screenH - 1, 0, -1)
cursorY = screenH
end
-- Clear full line then write text (prevents remnant chars from longer lines)
gpu.fill(1, cursorY, screenW, 1, " ")
gpu.set(1, cursorY, text)
cursorY = cursorY + 1
end
local function earlyClear()
if not gpu then return end
gpu.setBackground(0x000000)
gpu.setForeground(tc(0x00FF00))
gpu.fill(1, 1, screenW, screenH, " ")
cursorY = 1
end
-- Splash composition helper: on a "splash" boot the wordmark + status lines
-- + progress bar are CENTRED so the screen reads as a designed splash, not a
-- left-aligned boot log. Pads an ASCII line to the screen centre; the UTF-8
-- wordmark centres via logo.banner's width option instead (#string would
-- over-count its 3-byte block glyphs). Every other verbosity keeps the
-- classic indented log look.
local function splashPad(s)
if verbosity ~= "splash" then return " " .. s end
return string.rep(" ", math.max(0, math.floor((screenW - #s) / 2))) .. s
end
-- ============================================================
-- Stage 3: Boot splash
-- ============================================================
if _G._BIOS_CY then
cursorY = _G._BIOS_CY
if gpu then
gpu.setBackground(0x000000)
gpu.setForeground(tc(0x00FF00))
end
else
earlyClear()
end
local totalKB = math.floor(_G._TOS.totalMem / 1024)
local freeKB = math.floor(computer.freeMemory() / 1024)
-- The splash banner is "splash"-level output — suppressed only on a fully
-- silent boot. (Warnings below still show regardless.)
--
-- #FIX (emulator round 7) — this is a FUNCTION, not a straight-line block,
-- because the System Configuration POST screen below owns the whole screen
-- and earlyClear()s on the way out. The wordmark used to be painted once,
-- here, and then wiped: a splash boot showed a bare progress bar with no
-- branding at all (the operator: "the System Configuration screen removes
-- the logo"). The header is re-drawn after the POST screen closes so the
-- composition the splash is supposed to be actually survives to the
-- kernel hand-off.
local function drawBootHeader()
if verbosity ~= "silent" then
-- Branded splash: the shared kernel.logo wordmark. On a "splash" boot the
-- whole composition (wordmark, status lines, progress bar below) is
-- CENTRED; every other verbosity keeps the left-aligned boot-log look.
-- pcall-required with a graceful fallback to the old thin banner so a
-- logo-load hiccup can never block boot.
local okLogo, logo = pcall(require, "kernel.logo")
if okLogo and logo and logo.banner then
local bopts = { ascii = isMonochrome, compact = screenW < 34 }
if verbosity == "splash" then bopts.width = screenW
else bopts.indent = 2 end
for _, ln in ipairs(logo.banner(bopts)) do
earlyPrint(ln[1], tc((logo.COLORS and logo.COLORS[ln[2]]) or 0x00AAFF))
end
else
local bar = string.rep(isMonochrome and "=" or "═", math.min(38, screenW))
earlyPrint(bar, tc(0x00AAFF))
earlyPrint(" TOS", tc(0x00AAFF))
earlyPrint(bar, tc(0x00AAFF))
end
earlyPrint(splashPad("TOS v" .. _G._TOS.version .. " [" .. _G._TOS.codename .. "]"), tc(0x00AAFF))
earlyPrint(splashPad("Memory: " .. freeKB .. "K free / " .. totalKB .. "K total"), tc(0xAAAAAA))
end
-- #SEC H1 — visible confirmation that the operator's Shift+Enter one-time
-- boot took effect: this session runs from the fallback drive but the EEPROM
-- was left untouched, and the floppy→HDD migration offer is suppressed.
if _G._BIOS_ONETIME then
earlyPrint(" ONE-TIME BOOT: EEPROM unchanged, boot config preserved", tc(0xFFAA00))
end
-- Safe Mode is LOUD on purpose: the operator must never wonder why their
-- services/packages aren't running. (A one-time safe boot chosen at the
-- POST screen prints its own notice there — this covers the saved profile.)
if bootCfg and bootCfg.profile == "safe" then
earlyPrint(" SAFE MODE: services, cron, packages, net, themes are OFF", tc(0xFFAA00))
earlyPrint(" (Boot Settings -> Profile to leave Safe Mode)", tc(0xAAAAAA))
end
if totalKB < 128 then
earlyPrint(" WARNING: Low memory - features limited!", tc(0xFF0000))
elseif totalKB < 256 then
earlyPrint(" NOTE: Limited memory - some features may be skipped", tc(0xFFFF00))
end
end -- drawBootHeader
drawBootHeader()
-- ── System Configuration POST screen + DEL-to-setup (#3/#5) ──
-- Shown briefly when showConfig is on (default) — the TOS-ified AMIBIOS
-- screen: installed hardware + tiers. Press DEL during the window to enter
-- Boot Settings; any other key skips ahead. Fully guarded so a detection
-- hiccup or setup error never blocks boot.
--
-- showConfig — NOT verbosity — gates this screen. The old code also
-- required verbosity ~= "silent", which meant a silent boot hid the only
-- DEL-to-setup entry point: an operator who set "silent" could no longer
-- reach Boot Settings at all (recoverable only via the shell `bootsettings`
-- command, if they could still log in). verbosity is the boot-LOG muter;
-- showConfig is the master switch for this interactive screen. For a truly
-- silent boot, turn showConfig off as well.
-- showConfig is the master switch for this screen; a "verbose" boot ALSO
-- forces it, since the bootcfg contract lists "the hardware table" as part of
-- verbose (and a diagnostic boot should show diagnostics). Either trigger,
-- plus a GPU, opens it.
if ((bootCfg and bootCfg.showConfig) or verbosity == "verbose") and gpu then
pcall(function()
local sysinfo = require("kernel.sysinfo")
-- Shared palette + GPU draw primitives (raw GPU; pre-kernel).
local function postColor(role)
local map = {
bg = 0x000000, border = tc(0x00AAFF), title = tc(0x00FFFF),
dim = tc(0xAAAAAA), value = tc(0xFFFFFF), ok = tc(0x00FF00),
warn = tc(0xFFAA00), section = tc(0x00FFFF),
}
return map[role] or tc(0xFFFFFF)
end
local function gset(x, y, text, fgc, bgc)
if bgc then gpu.setBackground(bgc) end
gpu.setForeground(fgc or 0xFFFFFF)
gpu.set(x, y, text)
end
-- Pass BOTH operator tier overrides so the POST screen honours a manually
-- set Data Card tier (Boot Settings) instead of reporting "unknown tier".
local inv = sysinfo.gather(nil,
{ cpuTier = bootCfg.cpuTier, dataTier = bootCfg.dataTier },
function(msg) earlyPrint(" Standby - " .. msg .. "...", tc(0xAAAAAA)) end)
earlyClear()
-- Title line. sysinfo.render lays the screen out as a rigid two-column
-- config table showing only REAL hardware (no PC-BIOS flavor). Vendor
-- comes from kernel.logo (one source of truth); render clips to the screen.
local cfgTitle = "TOS System Configuration"
if screenW >= 60 then
local okLogo, logo = pcall(require, "kernel.logo")
if okLogo and logo and logo.VENDOR then
cfgTitle = logo.VENDOR .. " - System Configuration"
end
end
local after = sysinfo.render(inv, { W = screenW, H = screenH, color = postColor,
set = gset, title = cfgTitle })
gset(2, math.min(screenH, (after or screenH) + 1),
"Press DEL for Boot Settings, S for Safe Mode (once)...", tc(0xFFAA00))
-- Wait up to 3s for a key. DEL (code 211) opens setup; S boots Safe
-- Mode for THIS session only (config untouched — the operator's way
-- to get a trustworthy shell after installing something that breaks
-- boot, without editing anything first); any other key skips ahead;
-- non-key signals are ignored until the deadline.
local enterSetup = false
local safeOnce = false
local deadline = computer.uptime() + 3
while computer.uptime() < deadline do
local ev, _, ch, code = computer.pullSignal(deadline - computer.uptime())
if ev == "key_down" then
if code == 211 then enterSetup = true
elseif ch == 115 or ch == 83 then safeOnce = true end
break
end
end
earlyClear()
-- The POST screen owned the whole display; put the branded header back
-- so the rest of the boot (verify / load / progress bar) composes with
-- it instead of floating on a blank screen.
drawBootHeader()
if safeOnce and bootCfg then
bootCfg.profile = "safe" -- in-memory only; NOT saved
earlyPrint(" SAFE MODE (one-time): services, cron, packages, net, themes OFF",
tc(0xFFAA00))
earlyPrint(" Boot config untouched - the next boot is normal.", tc(0xAAAAAA))
end
if enterSetup then
local okBS, bootsettings = pcall(require, "kernel.bootsettings")
if okBS and bootsettings then
local ctx = {
W = screenW, H = screenH, color = postColor, set = gset,
sysinfo = sysinfo, clear = earlyClear,
readKey = function()
while true do
local e, _, ch, code = computer.pullSignal()
if e == "key_down" then return e, ch, code end
end
end,
}
local action, newCfg = bootsettings.run(bootCfg, ctx)
if action == "save" or action == "reboot" then
local cfgFS = {
exists = function(p) return bootFS.exists(p) end,
readFile = readFile,
writeFile = function(p, d)
if not bootFS.exists("/etc") then pcall(bootFS.makeDirectory, "/etc") end
local h = bootFS.open(p, "w"); if not h then return false end
bootFS.write(h, d); bootFS.close(h); return true
end,
}
pcall(function() require("kernel.bootcfg").save(cfgFS, newCfg) end)
earlyClear()
if action == "reboot" then
gset(2, 2, "Saved. Rebooting to apply...", tc(0x00FF00))
computer.pullSignal(1)
computer.shutdown(true)
else
gset(2, 2, "Saved. Changes apply on next boot. Continuing...", tc(0x00FF00))
computer.pullSignal(1)
earlyClear()
drawBootHeader() -- Boot Settings owned the screen too
end
end
end
end
end)
end
-- ============================================================
-- Stage 4: Verify & load kernel
-- ============================================================
-- Boot-critical file list resolution. Three layers, tried in order:
--
-- 1. /var/pkg/installed/tos-core/package.lua
-- Primary source post-migration (commit 3). The kernel pkg
-- manager keeps this in sync with the source tree on every
-- install/uninstall.
--
-- 2. /etc/critical.bak
-- Mirror written by pkg.syncCriticalBackup() after every
-- critical-set change. Lives under a different parent so
-- griefing or corruption that takes out /var/pkg/ doesn't
-- also wipe the backup.
--
-- 3. /tos/system_manifest.lua (legacy)
-- The pre-pkg-manager source of truth. Still consulted in
-- commit 1; will be deleted in commit 3 once tos-core is the
-- canonical home.