util-sbx.lmt /size: 20 Kb    last modification: 2025-02-21 11:03
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,"r") -- already has flush ("rb" fails on unix)
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            return iopopen(command) -- already has flush
422        end
423    end,
424    command = function(...)
425        local command = validcommand(...)
426        if command then
427            if trace then
428                report("command: %s",command)
429            end
430            return command
431        end
432    end,
433}
434
435function sandbox.registerrunner(specification)
436    if type(specification) == "string" then
437        local wrapped = validrunners[specification]
438        inspect(table.sortedkeys(validrunners))
439        if wrapped then
440            return wrapped
441        else
442            report("unknown predefined runner %a",specification)
443            return
444        end
445    end
446    if type(specification) ~= "table" then
447        report("specification should be a table (or string)")
448        return
449    end
450    local name = specification.name
451    if type(name) ~= "string" then
452        report("invalid name, string expected",name)
453        return
454    end
455    if validrunners[name] then
456        report("invalid name, runner %a already defined",name)
457        return
458    end
459    local program = specification.program
460    if type(program) == "string" then
461        -- common for all platforms
462    elseif type(program) == "table" then
463        program = program[platform] or program.default or program.unix
464    end
465    if type(program) ~= "string" or program == "" then
466        report("invalid runner %a specified for platform %a",name,platform)
467        return
468    end
469    local template = specification.template
470    if not template then
471        report("missing template for runner %a",name)
472        return
473    end
474    local method   = specification.method   or "execute"
475    local checkers = specification.checkers or { }
476    local defaults = specification.defaults or { }
477    local internal = specification.internal
478    local runner   = runners[method]
479    if runner then
480        local finalized    = finalized -- so, the current situation is frozen
481        local internalized = false
482        local wrapped = function(variables)
483            if internal and not internalized then
484                -- So internal returns a function but can also just return false in which
485                -- case we fallback in the runner; this permits optional library support.
486                internal = internal(specification)
487                if type(internal) ~= "function" then
488                    internal = false
489                end
490                internalized = true
491            end
492            return (internal or runner)(name,program,template,checkers,defaults,variables,specification.reporter,finalized)
493        end
494        validrunners[name] = wrapped
495        return wrapped
496    else
497        validrunners[name] = nil
498        report("invalid method for runner %a",name)
499    end
500end
501
502function sandbox.getrunner(name)
503    return name and validrunners[name]
504end
505
506local function suspicious(str)
507    return (find(str,"[/\\]") or find(command,"..",1,true)) and true or false
508end
509
510local function binaryrunner(action,command,...)
511    if validbinaries == false then
512        -- nothing permitted
513        report("no binaries permitted, ignoring command: %s",command)
514        return
515    end
516    if type(command) ~= "string" then
517        -- we only handle strings, maybe some day tables
518        report("command should be a string")
519        return
520    end
521    local program = lpegmatch(p_split,command)
522    if not program or program == "" then
523        report("unable to filter binary from command: %s",command)
524        return
525    end
526    if validbinaries == true then
527        -- everything permitted
528    elseif not validbinaries[program] then
529        report("binary not permitted, ignoring command: %s",command)
530        return
531    elseif suspicious(command) then
532        report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command)
533        return
534    end
535    return action(command,...)
536end
537
538local function dummyrunner(action,command,...)
539    if type(command) == "table" then
540        command = concat(command," ",command[0] and 0 or 1)
541    end
542    report("ignoring command: %s",command)
543end
544
545sandbox.filehandlerone = filehandlerone
546sandbox.filehandlertwo = filehandlertwo
547sandbox.iohandler      = iohandler
548
549do
550
551    local library     = optional.library
552    local foreign     = optional.foreign
553    local reported    = { }
554    local libraryload = library.load
555    local foreignload = foreign.load
556
557    function library.load(name,...)
558        if validlibraries == false then
559            -- all blocked
560        elseif validlibraries == true then
561            -- all permitted
562            return libraryload(name,...)
563        elseif validlibraries[nameonly(name)] then
564            -- 'name' permitted
565            return libraryload(name,...)
566        else
567            -- 'name' not permitted
568        end
569        if not reported[name] then
570            report("using library %a is not permitted",name)
571            reported[name] = true
572        end
573        return nil
574    end
575
576    function foreign.load(name,...)
577        if validlibraries == false then
578            -- all blocked
579        elseif validlibraries == true then
580            -- all permitted
581            return foreignload(name,...)
582        elseif validlibraries[nameonly(name)] then
583            -- 'name' permitted
584            return foreignload(name,...)
585        else
586            -- 'name' not permitted
587        end
588        if not reported[name] then
589            report("using foreign %a is not permitted",name)
590            reported[name] = true
591        end
592        return nil
593    end
594
595 -- we can do this in the engine: a one time flag
596
597    function sandbox.disablelibraries()
598        validlibraries = false
599        library.load = function() end
600        foreign.load = function() end
601    end
602
603    function sandbox.disablerunners()
604        validbinaries = false
605    end
606
607end
608
609-------------------
610
611local overload = sandbox.overload
612local register = sandbox.register
613
614    overload(loadfile,             filehandlerone,"loadfile") -- todo
615
616if io then
617    overload(io.open,              filehandlerone,"io.open")
618    overload(io.popen,             binaryrunner,  "io.popen")
619    overload(io.input,             iohandler,     "io.input")
620    overload(io.output,            iohandler,     "io.output")
621    overload(io.lines,             filehandlerone,"io.lines")
622end
623
624if os then
625    overload(os.execute,           binaryrunner,  "os.execute")
626    overload(os.spawn,             dummyrunner,   "os.spawn")    -- no longer there
627    overload(os.exec,              dummyrunner,   "os.exec")     -- no longer there
628    overload(os.resultof,          binaryrunner,  "os.resultof")
629    overload(os.pipeto,            binaryrunner,  "os.pipeto")
630    overload(os.rename,            filehandlertwo,"os.rename")
631    overload(os.remove,            filehandlerone,"os.remove")
632end
633
634if lfs then
635    overload(lfs.chdir,            filehandlerone,"lfs.chdir")
636    overload(lfs.mkdir,            filehandlerone,"lfs.mkdir")
637    overload(lfs.rmdir,            filehandlerone,"lfs.rmdir")
638    overload(lfs.isfile,           filehandlerone,"lfs.isfile")
639    overload(lfs.isdir,            filehandlerone,"lfs.isdir")
640    overload(lfs.attributes,       filehandlerone,"lfs.attributes")
641    overload(lfs.dir,              filehandlerone,"lfs.dir")
642    overload(lfs.lock_dir,         filehandlerone,"lfs.lock_dir")
643    overload(lfs.touch,            filehandlerone,"lfs.touch")
644    overload(lfs.link,             filehandlertwo,"lfs.link")
645    overload(lfs.setmode,          filehandlerone,"lfs.setmode")
646    overload(lfs.readlink,         filehandlerone,"lfs.readlink")
647    overload(lfs.shortname,        filehandlerone,"lfs.shortname")
648    overload(lfs.symlinkattributes,filehandlerone,"lfs.symlinkattributes")
649end
650
651-- these are used later on
652
653if zip then
654    zip.open = register(zip.open, filehandlerone,"zip.open")
655end
656
657sandbox.registerroot    = registerroot
658sandbox.registerbinary  = registerbinary
659sandbox.registerlibrary = registerlibrary
660sandbox.validfilename   = validfilename
661
662-- not used in a normal mkiv run : os.spawn = os.execute
663-- not used in a normal mkiv run : os.exec  = os.exec
664
665-- print(io.open("test.log"))
666-- sandbox.enable()
667-- print(io.open("test.log"))
668-- print(io.open("t:/test.log"))
669