util-sbx.lmt /size: 20 Kb    last modification: 2024-01-16 10:22
1if not modules then modules = { } end modules ['util-sbx'] = {
2    version   = 1.001,
3    comment   = "companion to luat-lib.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Note: we use expandname and collapsepath and these use chdir which is overloaded
10-- so we need to use originals there. Just something to keep in mind. This is old
11-- code that I might need to upgrade to fit lmtx better. I'll stepwise also try to
12-- integrate e.g. curl and graphicmagick a bit more natural. Some code below can
13-- be simplified because we don't share any longer with luatex.
14
15if not sandbox then require("l-sandbox") end -- for testing
16
17local next, type = next, type
18
19local replace        = utilities.templates.replace
20local collapsepath   = file.collapsepath
21local expandname     = dir.expandname
22local sortedhash     = table.sortedhash
23local lpegmatch      = lpeg.match
24local platform       = os.type
25local P, S, C        = lpeg.P, lpeg.S, lpeg.C
26local gsub           = string.gsub
27local lower          = string.lower
28local find           = string.find
29local concat         = string.concat
30local unquoted       = string.unquoted
31local optionalquoted = string.optionalquoted
32local basename       = file.basename
33local nameonly       = file.nameonly
34
35local sandbox        = sandbox
36local validroots     = { }
37local validrunners   = { }
38local validbinaries  = true -- all permitted
39local validlibraries = true -- all permitted
40local validators     = { }
41local finalized      = nil
42local trace          = false
43
44local p_validroot    = nil
45local p_split        = lpeg.firstofsplit(" ")
46
47local report         = logs.reporter("sandbox")
48
49trackers.register("sandbox",function(v) trace = v end) -- often too late anyway
50
51sandbox.setreporter(report)
52
53sandbox.finalizer {
54    category = "files",
55    action   = function()
56        finalized = true
57    end
58}
59
60local function registerroot(root,what) -- what == read|write
61    if finalized then
62        report("roots are already finalized")
63    else
64        if type(root) == "table" then
65            root, what = root[1], root[2]
66        end
67        if type(root) == "string" and root ~= "" then
68            root = collapsepath(expandname(root))
69            if what == "r" or what == "ro" or what == "readable" then
70                what = "read"
71            elseif what == "w" or what == "wo" or what == "writable" then
72                what = "write"
73            end
74            -- true: read & write | false: read
75            validroots[root] = what == "write" or false
76        end
77    end
78end
79
80sandbox.finalizer {
81    category = "files",
82    action   = function() -- initializers can set the path
83        if p_validroot then
84            report("roots are already initialized")
85        else
86            sandbox.registerroot(".","write") -- always ok
87            -- also register texmf as read
88            for name in sortedhash(validroots) do
89                if p_validroot then
90                    p_validroot = P(name) + p_validroot
91                else
92                    p_validroot = P(name)
93                end
94            end
95            p_validroot = p_validroot / validroots
96        end
97    end
98}
99
100local function registerbinary(name)
101    if finalized then
102        report("binaries are already finalized")
103    elseif type(name) == "string" and name ~= "" then
104        if not validbinaries then
105            return
106        end
107        if validbinaries == true then
108            validbinaries = { [name] = true }
109        else
110            validbinaries[name] = true
111        end
112    elseif name == true then
113        validbinaries = { }
114    end
115end
116
117local function registerlibrary(name)
118    if finalized then
119        report("libraries are already finalized")
120    elseif type(name) == "string" and name ~= "" then
121        if not validlibraries then
122            return
123        end
124        if validlibraries == true then
125            validlibraries = { [nameonly(name)] = true }
126        else
127            validlibraries[nameonly(name)] = true
128        end
129    elseif name == true then
130        validlibraries = { }
131    end
132end
133
134-- begin of validators
135
136local p_write = S("wa")       p_write = (1 - p_write)^0 * p_write
137local p_path  = S("\\/~$%:")  p_path  = (1 - p_path )^0 * p_path  -- be easy on other arguments
138
139local function normalized(name) -- only used in executers
140    if platform == "windows" then
141        name = gsub(name,"/","\\")
142    end
143    return name
144end
145
146function sandbox.possiblepath(name)
147    return lpegmatch(p_path,name) and true or false
148end
149
150local filenamelogger = false
151
152function sandbox.setfilenamelogger(l)
153    filenamelogger = type(l) == "function" and l or false
154end
155
156local function validfilename(name,what)
157    if p_validroot and type(name) == "string" and lpegmatch(p_path,name) then
158        local asked = collapsepath(expandname(name))
159     -- if platform == "windows" then
160     --     asked = lower(asked) -- we assume ascii names
161     -- end
162        local okay = lpegmatch(p_validroot,asked)
163        if okay == true then
164            -- read and write access
165            if filenamelogger then
166                filenamelogger(name,"w",asked,true)
167            end
168            return name
169        elseif okay == false then
170            -- read only access
171            if not what then
172                -- no further argument to io.open so a readonly case
173                if filenamelogger then
174                    filenamelogger(name,"r",asked,true)
175                end
176                return name
177            elseif lpegmatch(p_write,what) then
178                if filenamelogger then
179                    filenamelogger(name,"w",asked,false)
180                end
181                return -- we want write access
182            else
183                if filenamelogger then
184                    filenamelogger(name,"r",asked,true)
185                end
186                return name
187            end
188        elseif filenamelogger then
189            filenamelogger(name,"*",name,false)
190        end
191    else
192        return name
193    end
194end
195
196local function readable(name,finalized)
197--     if platform == "windows" then -- yes or no
198--         name = lower(name) -- we assume ascii names
199--     end
200    return validfilename(name,"r")
201end
202
203local function normalizedreadable(name,finalized)
204--     if platform == "windows" then -- yes or no
205--         name = lower(name) -- we assume ascii names
206--     end
207    local valid = validfilename(name,"r")
208    if valid then
209        return normalized(valid)
210    end
211end
212
213local function writeable(name,finalized)
214--     if platform == "windows" then
215--         name = lower(name) -- we assume ascii names
216--     end
217    return validfilename(name,"w")
218end
219
220local function normalizedwriteable(name,finalized)
221--     if platform == "windows" then
222--         name = lower(name) -- we assume ascii names
223--     end
224    local valid = validfilename(name,"w")
225    if valid then
226        return normalized(valid)
227    end
228end
229
230validators.readable            = readable
231validators.writeable           = normalizedwriteable
232validators.normalizedreadable  = normalizedreadable
233validators.normalizedwriteable = writeable
234validators.filename            = readable
235
236table.setmetatableindex(validators,function(t,k)
237    if k then
238        t[k] = readable
239    end
240    return readable
241end)
242
243-- function validators.verbose(s)
244--     return s
245-- end
246
247function validators.string(s,finalized)
248    -- can be used to prevent filename checking (todo: only when registered)
249    if finalized and suspicious(s) then
250        return ""
251    else
252        return s
253    end
254end
255
256function validators.cache(s)
257    if finalized then
258        return basename(s)
259    else
260        return s
261    end
262end
263
264function validators.url(s)
265    if finalized and find("^file:") then
266        return ""
267    else
268        return s
269    end
270end
271
272-- end of validators
273
274local function filehandlerone(action,one,...)
275    local checkedone = validfilename(one)
276    if checkedone then
277        return action(one,...)
278    else
279     -- report("file %a is unreachable",one)
280    end
281end
282
283local function filehandlertwo(action,one,two,...)
284    local checkedone = validfilename(one)
285    if checkedone then
286        local checkedtwo = validfilename(two)
287        if checkedtwo then
288            return action(one,two,...)
289        else
290         -- report("file %a is unreachable",two)
291        end
292    else
293     -- report("file %a is unreachable",one)
294    end
295end
296
297local function iohandler(action,one,...)
298    if type(one) == "string" then
299        local checkedone = validfilename(one)
300        if checkedone then
301            return action(one,...)
302        end
303    elseif one then
304        return action(one,...)
305    else
306        return action()
307    end
308end
309
310-- runners can be strings or tables
311--
312-- os.execute : string
313-- os.exec    : string or table with program in [0|1] -- no longer there
314-- os.spawn   : string or table with program in [0|1] -- no longer there
315--
316-- our execute: registered program with specification
317
318local osexecute = sandbox.original(os.execute)
319local iopopen   = sandbox.original(io.popen)
320local reported  = { }
321
322local function validcommand(name,program,template,checkers,defaults,variables,reporter,strict)
323    if validbinaries ~= false and (validbinaries == true or validbinaries[program]) then
324        local binpath = nil
325        if variables then
326            for variable, value in next, variables do
327                local chktype = checkers[variable]
328                if chktype == "verbose" then
329                    -- for now, we will have a "flags" checker
330                else
331                    local checker = validators[chktype]
332                    if checker and type(value) == "string" then
333                        value = checker(unquoted(value),strict)
334                        if value then
335                            variables[variable] = optionalquoted(value)
336                        else
337                            report("variable %a with value %a fails the check",variable,value)
338                            return
339                        end
340                    else
341                        report("variable %a has no checker",variable)
342                        return
343                    end
344                end
345            end
346            for variable, default in next, defaults do
347                local value = variables[variable]
348                if not value or value == "" then
349                    local chktype = checkers[variable]
350                    if chktype == "verbose" then
351                        -- for now, we will have a "flags" checker
352                    elseif type(default) == "string" then
353                        local checker = validators[chktype]
354                        if checker then
355                            default = checker(unquoted(default),strict)
356                            if default then
357                                variables[variable] = optionalquoted(default)
358                            else
359                                report("variable %a with default %a fails the check",variable,default)
360                                return
361                            end
362                        end
363                    end
364                end
365            end
366            binpath = variables.binarypath
367        end
368        if type(binpath) == "string" and binpath ~= "" then
369            -- this works on the console but not from e.g. scite
370         -- program = '"' .. binpath .. "/" .. program .. '"'
371            program = binpath .. "/" .. program
372        end
373        local command = program .. " " .. replace(template,variables)
374        if reporter then
375            reporter("executing runner %a: %s",name,command)
376        elseif trace then
377            report("executing runner %a: %s",name,command)
378        end
379        return command
380    elseif not reported[name] then
381        report("executing program %a of runner %a is not permitted",program,name)
382        reported[name] = true
383    end
384end
385
386local runners = {
387    --
388    -- name,program,template,checkers,variables,reporter
389    --
390    resultof = function(...)
391        local command = validcommand(...)
392        if command then
393            if trace then
394                report("resultof: %s",command)
395            end
396            local handle = iopopen(command,"rb") -- already has flush
397            if handle then
398                local result = handle:read("*all") or ""
399                handle:close()
400                return result
401            end
402        end
403    end,
404    execute = function(...)
405        local command = validcommand(...)
406        if command then
407            if trace then
408                report("execute: %s",command)
409            end
410            local okay = osexecute(command)
411            return okay
412        end
413    end,
414    pipeto = function(...)
415        local command = validcommand(...)
416        if command then
417            if trace then
418                report("pipeto: %s",command)
419            end
420            return iopopen(command,"w") -- already has flush
421        end
422    end,
423}
424
425function sandbox.registerrunner(specification)
426    if type(specification) == "string" then
427        local wrapped = validrunners[specification]
428        inspect(table.sortedkeys(validrunners))
429        if wrapped then
430            return wrapped
431        else
432            report("unknown predefined runner %a",specification)
433            return
434        end
435    end
436    if type(specification) ~= "table" then
437        report("specification should be a table (or string)")
438        return
439    end
440    local name = specification.name
441    if type(name) ~= "string" then
442        report("invalid name, string expected",name)
443        return
444    end
445    if validrunners[name] then
446        report("invalid name, runner %a already defined",name)
447        return
448    end
449    local program = specification.program
450    if type(program) == "string" then
451        -- common for all platforms
452    elseif type(program) == "table" then
453        program = program[platform] or program.default or program.unix
454    end
455    if type(program) ~= "string" or program == "" then
456        report("invalid runner %a specified for platform %a",name,platform)
457        return
458    end
459    local template = specification.template
460    if not template then
461        report("missing template for runner %a",name)
462        return
463    end
464    local method   = specification.method   or "execute"
465    local checkers = specification.checkers or { }
466    local defaults = specification.defaults or { }
467    local internal = specification.internal
468    local runner   = runners[method]
469    if runner then
470        local finalized    = finalized -- so, the current situation is frozen
471        local internalized = false
472        local wrapped = function(variables)
473            if internal and not internalized then
474                -- So internal returns a function but can also just return false in which
475                -- case we fallback in the runner; this permits optional library support.
476                internal = internal(specification)
477                if type(internal) ~= "function" then
478                    internal = false
479                end
480                internalized = true
481            end
482            return (internal or runner)(name,program,template,checkers,defaults,variables,specification.reporter,finalized)
483        end
484        validrunners[name] = wrapped
485        return wrapped
486    else
487        validrunners[name] = nil
488        report("invalid method for runner %a",name)
489    end
490end
491
492function sandbox.getrunner(name)
493    return name and validrunners[name]
494end
495
496local function suspicious(str)
497    return (find(str,"[/\\]") or find(command,"..",1,true)) and true or false
498end
499
500local function binaryrunner(action,command,...)
501    if validbinaries == false then
502        -- nothing permitted
503        report("no binaries permitted, ignoring command: %s",command)
504        return
505    end
506    if type(command) ~= "string" then
507        -- we only handle strings, maybe some day tables
508        report("command should be a string")
509        return
510    end
511    local program = lpegmatch(p_split,command)
512    if not program or program == "" then
513        report("unable to filter binary from command: %s",command)
514        return
515    end
516    if validbinaries == true then
517        -- everything permitted
518    elseif not validbinaries[program] then
519        report("binary not permitted, ignoring command: %s",command)
520        return
521    elseif suspicious(command) then
522        report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command)
523        return
524    end
525    return action(command,...)
526end
527
528local function dummyrunner(action,command,...)
529    if type(command) == "table" then
530        command = concat(command," ",command[0] and 0 or 1)
531    end
532    report("ignoring command: %s",command)
533end
534
535sandbox.filehandlerone = filehandlerone
536sandbox.filehandlertwo = filehandlertwo
537sandbox.iohandler      = iohandler
538
539do
540
541    local library     = optional.library
542    local foreign     = optional.foreign
543    local reported    = { }
544    local libraryload = library.load
545    local foreignload = foreign.load
546
547    function library.load(name,...)
548        if validlibraries == false then
549            -- all blocked
550        elseif validlibraries == true then
551            -- all permitted
552            return libraryload(name,...)
553        elseif validlibraries[nameonly(name)] then
554            -- 'name' permitted
555            return libraryload(name,...)
556        else
557            -- 'name' not permitted
558        end
559        if not reported[name] then
560            report("using library %a is not permitted",name)
561            reported[name] = true
562        end
563        return nil
564    end
565
566    function foreign.load(name,...)
567        if validlibraries == false then
568            -- all blocked
569        elseif validlibraries == true then
570            -- all permitted
571            return foreignload(name,...)
572        elseif validlibraries[nameonly(name)] then
573            -- 'name' permitted
574            return foreignload(name,...)
575        else
576            -- 'name' not permitted
577        end
578        if not reported[name] then
579            report("using foreign %a is not permitted",name)
580            reported[name] = true
581        end
582        return nil
583    end
584
585 -- we can do this in the engine: a one time flag
586
587    function sandbox.disablelibraries()
588        validlibraries = false
589        library.load = function() end
590        foreign.load = function() end
591    end
592
593    function sandbox.disablerunners()
594        validbinaries = false
595    end
596
597end
598
599-------------------
600
601local overload = sandbox.overload
602local register = sandbox.register
603
604    overload(loadfile,             filehandlerone,"loadfile") -- todo
605
606if io then
607    overload(io.open,              filehandlerone,"io.open")
608    overload(io.popen,             binaryrunner,  "io.popen")
609    overload(io.input,             iohandler,     "io.input")
610    overload(io.output,            iohandler,     "io.output")
611    overload(io.lines,             filehandlerone,"io.lines")
612end
613
614if os then
615    overload(os.execute,           binaryrunner,  "os.execute")
616    overload(os.spawn,             dummyrunner,   "os.spawn")    -- no longer there
617    overload(os.exec,              dummyrunner,   "os.exec")     -- no longer there
618    overload(os.resultof,          binaryrunner,  "os.resultof")
619    overload(os.pipeto,            binaryrunner,  "os.pipeto")
620    overload(os.rename,            filehandlertwo,"os.rename")
621    overload(os.remove,            filehandlerone,"os.remove")
622end
623
624if lfs then
625    overload(lfs.chdir,            filehandlerone,"lfs.chdir")
626    overload(lfs.mkdir,            filehandlerone,"lfs.mkdir")
627    overload(lfs.rmdir,            filehandlerone,"lfs.rmdir")
628    overload(lfs.isfile,           filehandlerone,"lfs.isfile")
629    overload(lfs.isdir,            filehandlerone,"lfs.isdir")
630    overload(lfs.attributes,       filehandlerone,"lfs.attributes")
631    overload(lfs.dir,              filehandlerone,"lfs.dir")
632    overload(lfs.lock_dir,         filehandlerone,"lfs.lock_dir")
633    overload(lfs.touch,            filehandlerone,"lfs.touch")
634    overload(lfs.link,             filehandlertwo,"lfs.link")
635    overload(lfs.setmode,          filehandlerone,"lfs.setmode")
636    overload(lfs.readlink,         filehandlerone,"lfs.readlink")
637    overload(lfs.shortname,        filehandlerone,"lfs.shortname")
638    overload(lfs.symlinkattributes,filehandlerone,"lfs.symlinkattributes")
639end
640
641-- these are used later on
642
643if zip then
644    zip.open = register(zip.open, filehandlerone,"zip.open")
645end
646
647sandbox.registerroot    = registerroot
648sandbox.registerbinary  = registerbinary
649sandbox.registerlibrary = registerlibrary
650sandbox.validfilename   = validfilename
651
652-- not used in a normal mkiv run : os.spawn = os.execute
653-- not used in a normal mkiv run : os.exec  = os.exec
654
655-- print(io.open("test.log"))
656-- sandbox.enable()
657-- print(io.open("test.log"))
658-- print(io.open("t:/test.log"))
659