From f7dd4f74d0a6b3399fe694f80911657aec351ffd Mon Sep 17 00:00:00 2001 From: ian Date: Tue, 14 Oct 1997 01:05:13 +0000 Subject: [PATCH] 0.57 --- Changelog | 15 ++++++ INSTALL | 103 ++++++++++++++++++++++++++++++++++- Makefile.in | 3 +- client.c | 3 +- common.h | 4 +- daemon.h | 3 +- overlord.c | 150 +++++++++++++++++++++++++++++++++++++++++---------- process.c | 24 +++++---- servexec.c | 2 +- spec.sgml.in | 2 +- 10 files changed, 264 insertions(+), 45 deletions(-) diff --git a/Changelog b/Changelog index 9af7e5c..9ea9b1c 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,18 @@ +userv (0.57); urgency=high + + * Services provided by root work ! + * uservd can now go into background itself (-daemon option). + + * spec now has default syslog facility for rcfile messages as `user'. + * Better prioritisation of syslog messages. + * Startup error messages now go to stderr instead. + * SIGTERM and SIGINT now produce a syslog message. + + * Version number has VEREXT component, settable via make args &c. + * New sections in INSTALL about exit statuses and -daemon. + + -- Ian Jackson Tue, 14 Oct 1997 02:04:18 +0100 + userv (0.56); urgency=medium * Server now checks itself every hour to see if its socket has been diff --git a/INSTALL b/INSTALL index dee9597..4b5d665 100644 --- a/INSTALL +++ b/INSTALL @@ -55,8 +55,9 @@ System interfaces: open(O_WRONLY), close(), dup2, EPIPE, SIGPIPE, &c. (ie, opening pipes with O_RDWR never blocks; EPIPE happens if you write with no readers; EOF happens if you read with - no buffered data and writers) + no buffered data and writers); * POSIX signal handling - sigaction(2), sigprocmask(2), sigsuspend(2); +* POSIX sessions - setsid(2) (for -daemon flag). To format the documentation: @@ -68,6 +69,106 @@ For debugging version (./configure --enable-debug): * initgroups(3) must use setgroups(2) and dynamic linking must allow overriding setgroups(2) for initgroups(3); +DAEMON INVOCATION: + +The daemon can be invoked with no arguments, in which case it will not +fork or detach itself. This is suitable for running from init and +similar arrangements. + +With -daemon it will attempt to detach itself from the controlling +terminal and fork/exit so that control returns at startup. + +In both cases diagnostics which prevent correct startup will appear on +stderr. + +SYSLOG MESSAGES: + +The daemon issues diagnostics of various kinds to syslog, usually with +facility LOG_DAEMON (though this can be changed in daemon.h if you want). +The syslog levels used are: + debug - verbose messages about the activity of the userv daemon. + info - two log message about the nature and outcome of each request. + notice - messages about the status of the daemon, including the + startup message and the hourly socket check messages. + warning - if the uservd exits because it believes that it no longer + controls the rendezvous socket (ie, its socket has become + orphaned), this level will receive messages indicating why + the daemon believes this and notifying of its shutdown. + err - a believed-recoverable error condition was detected by the + userv server in itself, the client or the operating system + (this includes resource shortages). The uservd will try to + continue. + crit - the uservd detected a non-recoverable error condition + after startup and will exit. + alert - not used. + emerg - not used. + +The service configuration language has the facility to direct error +and warning messages to syslog. The default facility and level is +user.err, but the author of the configuration file(s) can override +this. + +DAEMON EXIT STATUS: + +The daemon's exit code will reflect how well things went: + + 0 - The daemon was asked to detach itself from the controlling + terminal and this appears to have been done successfully. + 1* - The daemon got a SIGTERM or SIGINT and shut itself down. + 2* - The daemon believes that it was no longer the uservd and so has + exited to clean itself up. + 3 - uservd was started with incorrect arguments. + 4 - A system call failure or other environmental problem occurred + during startup. + 5* - There was a non-recoverable error after startup; the uservd had + to exit. + 6 - The daemon was asked to detach itself, but its detaching child + died for some unexpected reason. + + SIGABRT/SIGIOT* - an unexpected internal error, usually caused by a + bug in uservd. This can also occur if an attempt to block signals + using sigprocmask fails. + +Outcomes marked * are not possible if the daemon is asked to detach +itself - these exit statuses will be reaped by init instead. + +The daemon's per-request children will note the success level of its +request in its exit status. This will not usually be logged unless it +is higher than those listed below; they are presented here for +completeness and as programming documentation. + + 2 - The connection was just an internal version check. + + 4 - The client requested that the service be disconnected. This + includes normal termination, which is achieved by having the + server tell the client that the service has completed and waiting + for the client to tell it to disconnect. + + 8 - The client closed its end of the socket when this would not + usually have been expected, causing an EPIPE or unexpected EOF in + the server. This is not an error condition - it can happen, for + example, if the client receives a fatal signal of some kind from + its execution environment (eg its controlling terminal). + + 12 - The service failed onm the service side in an expected and + controlled manner, for example because it was rejected in the + configuration files. + + 16 - A fatal system call failure or other general error occurred, + which ought not to have happened at all, barring system resource + shortages. + + 20 - The client sent invalid data to the server, after the client + dropped all its system privilege. On some systems this can be + caused by a malicious calling user. + + SIGABRT/SIGIOT - The client sent invalid data to the server before it + dropped all its system privileges, or some other unexpected + internal error occurred. This can also occur if an attempt to + block signals using sigprocmask fails. + + 0-3,5-7,9-11,13-15,17-19 are not currently used. + REENTRANCY IN THE LIBC: We assume, both in the client and server, that it is safe to use one diff --git a/Makefile.in b/Makefile.in index 03e99ed..4339481 100644 --- a/Makefile.in +++ b/Makefile.in @@ -17,9 +17,10 @@ # Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. VERSION=@VERSION@ +VEREXT=std CC=@CC@ -CFLAGS=@CFLAGS@ $(XCFLAGS) -DVERSION='"$(VERSION)"' +CFLAGS=@CFLAGS@ $(XCFLAGS) -DVERSION='"$(VERSION)"' -DVEREXT='"$(VEREXT)"' OPTIMISE=@OPTIMISE@ CPPFLAGS=@DEBUGDEFS@ $(XCPPFLAGS) LDLIBS=@DEBUGLIBS@ $(XLDLIBS) diff --git a/client.c b/client.c index 4fbb710..bc55412 100644 --- a/client.c +++ b/client.c @@ -434,7 +434,7 @@ static void usage(void) { " --spoof-user } or same user\n" "fdmodifiers: read write overwrite trunc[ate]\n" "(separate with commas) append sync excl[usive] creat[e] fd\n\n" - "userv and uservd version " VERSION "; copyright (C)1996-1997 Ian Jackson.\n" + "userv and uservd version " VERSION VEREXT "; copyright (C)1996-1997 Ian Jackson.\n" "there is NO WARRANTY; type `userv --copyright' for details.\n", stderr) < 0) syscallerror("write usage to stderr"); @@ -1047,6 +1047,7 @@ static void server_sendrequest(int argc, char *const *argv) { request_mbuf.serviceuserlen= strlen(serviceuser); request_mbuf.servicelen= strlen(argv[0]); request_mbuf.lognamelen= strlen(logname); + request_mbuf.spoofed= spoofuser ? 1 : 0; request_mbuf.cwdlen= cwdbufsize; request_mbuf.callinguid= spoofuid; request_mbuf.ngids= ngids+1; diff --git a/common.h b/common.h index ee243b1..f385fc9 100644 --- a/common.h +++ b/common.h @@ -82,10 +82,10 @@ struct opening_msg { struct request_msg { unsigned long magic; - pid_t clientpid; + pid_t clientpid; /* or -1 if no service is required and this was a version check */ int serviceuserlen; int servicelen; - int lognamelen; + int lognamelen, spoofed; /* spoofed is 0 or 1 */ int cwdlen, overridelen; uid_t callinguid; int ngids, nreadfds, nwritefds, nargs, nvars; diff --git a/daemon.h b/daemon.h index 9efd982..945d651 100644 --- a/daemon.h +++ b/daemon.h @@ -77,7 +77,7 @@ #define USERVD_LOGIDENT "uservd" #define USERVDCHECK_LOGIDENT "uservd/check" #define USERVD_LOGFACILITY LOG_DAEMON -#define DEFUSERLOGFACILITY LOG_DAEMON +#define DEFUSERLOGFACILITY LOG_USER #define DEFUSERLOGLEVEL LOG_ERR #define TOPLEVEL_CONFIGURATION " \n\ @@ -105,6 +105,7 @@ #define USERVD_MYSELF_CHECK 3600 #define USERVD_MYSELF_TIMEOUT 60 +#define USERVD_CHECKFORK_RETRY 60 #define MAX_INCLUDE_NEST 40 #define MAX_ERRMSG_LEN (MAX_ERRMSG_STRING-1024) #define ERRMSG_RESERVE_ERRNO 128 diff --git a/overlord.c b/overlord.c index e32c9a1..b18102f 100644 --- a/overlord.c +++ b/overlord.c @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -41,7 +42,7 @@ pid_t overlordpid; -static pid_t checkpid= -1; +static pid_t checkpid= -1, detachpid= -1; static sig_atomic_t needcheck= 1; static void checkstalepipes(void) { @@ -87,19 +88,24 @@ static void sighandler_chld(int x) { r= waitpid((pid_t)-1,&status,WNOHANG); if (!r || (r==-1 && errno==ECHILD)) break; if (r==-1) { syslog(LOG_ERR,"wait in sigchild handler gave error: %m"); break; } + if (r==detachpid) { + if (WIFEXITED(status) && WEXITSTATUS(status)==4) _exit(4); + fprintf(stderr,"uservd: detaching child failed with unexpected code %d\n",status); + exit(6); + } if (r==checkpid) { if (WIFEXITED(status)) { if (!WEXITSTATUS(status)) { - syslog(LOG_NOTICE,"no longer the uservd - exiting"); - _exit(0); + syslog(LOG_WARNING,"no longer the uservd - exiting"); + _exit(2); } else if (WEXITSTATUS(status)!=1) { syslog(LOG_ERR,"check pid %ld exited with status %d", (long)checkpid,WEXITSTATUS(status)); } } else if (WIFSIGNALED(status)) { if (WTERMSIG(status) == SIGALRM && !WCOREDUMP(status)) { - syslog(LOG_NOTICE,"check timed out; no longer the uservd - exiting"); - _exit(0); + syslog(LOG_WARNING,"check timed out; no longer the uservd - exiting"); + _exit(2); } else { syslog(LOG_ERR,"check pid %ld %s due to signal %s", (long)checkpid, @@ -134,10 +140,19 @@ static void sighandler_chld(int x) { return; } +static void sighandler_usr1(int x) { + _exit(0); +} + static void sighandler_alrm(int x) { needcheck= 1; } +static void sighandler_termint(int sig) { + syslog(LOG_NOTICE,"terminating due to signal %s",strsignal(sig)); + _exit(1); +} + static void blocksignals(int how) { int r; sigset_t set; @@ -145,6 +160,8 @@ static void blocksignals(int how) { sigemptyset(&set); sigaddset(&set,SIGCHLD); sigaddset(&set,SIGALRM); + sigaddset(&set,SIGTERM); + sigaddset(&set,SIGINT); r= sigprocmask(how,&set,0); assert(!r); } @@ -156,6 +173,8 @@ static void NONRETURNING docheck(void) { int sfd, r, remain; unsigned char *p; struct opening_msg opening_mbuf; + struct request_msg request_mbuf; + unsigned long endmagic; struct sigaction sig; struct sockaddr_un ssockname; @@ -179,7 +198,7 @@ static void NONRETURNING docheck(void) { r= connect(sfd,(struct sockaddr*)&ssockname,sizeof(ssockname)); if (r) { if (errno == ECONNREFUSED || errno == ENOENT) - { syslog(LOG_NOTICE,"uservd daemon is not running: %m"); exit(0); } + { syslog(LOG_WARNING,"real uservd daemon is not running: %m"); exit(0); } syslog(LOG_ERR,"unable to connect to uservd daemon: %m"); exit(1); } @@ -193,53 +212,86 @@ static void NONRETURNING docheck(void) { remain-= r; p+= r; } if (opening_mbuf.magic != OPENING_MAGIC) { - syslog(LOG_NOTICE,"magic number mismatch"); + syslog(LOG_WARNING,"magic number mismatch"); exit(0); } if (memcmp(opening_mbuf.protocolchecksumversion,protocolchecksumversion,PCSUMSIZE)) { - syslog(LOG_NOTICE,"protocol checksum mismatch"); + syslog(LOG_WARNING,"protocol checksum mismatch"); exit(0); } if (opening_mbuf.overlordpid != overlordpid) { - syslog(LOG_NOTICE,"overlord pid mismatch"); + syslog(LOG_WARNING,"overlord pid mismatch"); exit(0); } - syslog(LOG_NOTICE,"check - same daemon still running"); + memset(&request_mbuf,0,sizeof(request_mbuf)); + request_mbuf.magic= REQUEST_MAGIC; + request_mbuf.clientpid= -1; + request_mbuf.serviceuserlen= 0; + request_mbuf.servicelen= 0; + request_mbuf.lognamelen= 0; + request_mbuf.spoofed= 0; + request_mbuf.cwdlen= 0; + request_mbuf.overridelen= -1; + request_mbuf.callinguid= -1; + request_mbuf.ngids= 0; + request_mbuf.nreadfds= 0; + request_mbuf.nwritefds= 0; + request_mbuf.nargs= 0; + request_mbuf.nvars= 0; + r= write(sfd,&request_mbuf,sizeof(request_mbuf)); + if (r==sizeof(request_mbuf)) { + endmagic= REQUEST_END_MAGIC; + write(sfd,&endmagic,sizeof(endmagic)); + } + syslog(LOG_NOTICE,"uservd[%ld] is running",(long)overlordpid); #endif exit(1); } +static void NONRETURNING startupsyscallerr(const char *what) { + fprintf(stderr, + "uservd: system call failed during startup:\n" + "uservd: %s: %s\n", + what,strerror(errno)); + exit(4); +} + int main(int argc, char *const *argv) { - int mfd, sfd, csocklen, e; + int mfd, sfd, nfd, csocklen, e, r, becomedaemon; struct sigaction sigact; struct sockaddr_un ssockname, csockname; - pid_t child; + pid_t child, parentpid, sid; #ifdef NDEBUG abort(); /* Do not disable assertions in this security-critical code ! */ #endif - if (argc>1) { fputs("usage: uservd\n",stderr); exit(3); } + becomedaemon= 0; + + if (argv[1] && !strcmp(argv[1],"-daemon")) { + becomedaemon= 1; + argv++; argc--; + } + if (argc>1) { fputs("usage: uservd [-daemon]\n",stderr); exit(3); } openlog(USERVD_LOGIDENT,LOG_NDELAY|LOG_PID,USERVD_LOGFACILITY); - if (chdir(VARDIR)) { syslog(LOG_CRIT,"cannot change to " VARDIR ": %m"); exit(4); } + if (chdir(VARDIR)) startupsyscallerr("cannot change to " VARDIR); checkstalepipes(); - overlordpid= getpid(); - if (overlordpid==-1) { syslog(LOG_CRIT,"cannot getpid: %m"); exit(4); } + overlordpid= parentpid= getpid(); + if (parentpid==-1) startupsyscallerr("cannot getpid"); mfd= socket(AF_UNIX,SOCK_STREAM,0); - if (mfd<0) { syslog(LOG_CRIT,"cannot create master socket: %m"); exit(4); } + if (mfd<0) startupsyscallerr("cannot create master socket"); assert(sizeof(ssockname.sun_path) > sizeof(RENDEZVOUS)); ssockname.sun_family= AF_UNIX; strcpy(ssockname.sun_path,RENDEZVOUS); unlink(RENDEZVOUS); - if (bind(mfd,(struct sockaddr*)&ssockname,sizeof(ssockname))) - { syslog(LOG_CRIT,"cannot bind master socket: %m"); exit(4); } - if (listen(mfd,5)) - { syslog(LOG_CRIT,"cannot listen on master socket: %m"); exit(4); } + r= bind(mfd,(struct sockaddr*)&ssockname,sizeof(ssockname)); + if (r) startupsyscallerr("cannot bind master socket"); + if (listen(mfd,5)) startupsyscallerr("cannot listen on master socket"); sigemptyset(&sigact.sa_mask); sigaddset(&sigact.sa_mask,SIGCHLD); @@ -247,20 +299,62 @@ int main(int argc, char *const *argv) { sigact.sa_flags= SA_NOCLDSTOP; sigact.sa_handler= sighandler_chld; - if (sigaction(SIGCHLD,&sigact,0)) - { syslog(LOG_CRIT,"cannot setup sigchld handler: %m"); exit(4); } + if (sigaction(SIGCHLD,&sigact,0)) startupsyscallerr("cannot setup sigchld handler"); sigact.sa_handler= sighandler_alrm; - if (sigaction(SIGALRM,&sigact,0)) - { syslog(LOG_CRIT,"cannot setup sigalrm handler: %m"); exit(4); } + if (sigaction(SIGALRM,&sigact,0)) startupsyscallerr("cannot setup sigalrm handler"); + + if (becomedaemon) { + sigact.sa_handler= sighandler_usr1; + if (sigaction(SIGUSR1,&sigact,0)) startupsyscallerr("cannot setup sigusr1 handler"); + + detachpid= fork(); if (detachpid==-1) startupsyscallerr("cannot fork to detach"); + if (detachpid) { + pause(); + fputs("uservd: pause unexpectedly returned during detach\n",stderr); + exit(4); + } + sigact.sa_handler= SIG_DFL; + if (sigaction(SIGUSR1,&sigact,0)) startupsyscallerr("cannot restore sigusr1"); + } + + sigact.sa_handler= sighandler_termint; + if (sigaction(SIGTERM,&sigact,0)) startupsyscallerr("cannot setup sigterm handler"); + if (sigaction(SIGINT,&sigact,0)) startupsyscallerr("cannot setup sigint handler"); + + if (becomedaemon) { + nfd= open("/dev/null",O_RDWR); + if (nfd<0) startupsyscallerr("cannot open /dev/null"); + sid= setsid(); if (sid == -1) startupsyscallerr("cannot create new session"); + overlordpid= getpid(); + if (overlordpid == -1) startupsyscallerr("getpid after detach"); + if (dup2(nfd,0)<0 || dup2(nfd,1)<0) + startupsyscallerr("cannot dup /dev/null for stdin/out"); + r= kill(parentpid,SIGUSR1); if (r) startupsyscallerr("send SIGUSR1 to detach"); + r= dup2(nfd,2); + if (r<0) { syslog(LOG_CRIT,"cannot dup /dev/null for stderr: %m"); exit(5); } + close(nfd); + } syslog(LOG_NOTICE,"started"); + for (;;) { if (needcheck) { assert(checkpid==-1); - checkpid= fork(); - if (checkpid==-1) { syslog(LOG_CRIT,"fork for check: %m"); exit(5); } - if (!checkpid) docheck(); + for (;;) { + checkpid= fork(); + if (checkpid!=-1) { + if (!checkpid) docheck(); + break; + } else if (errno==EAGAIN) { + syslog(LOG_ERR,"fork for check - will wait and retry: %m"); + r= alarm(USERVD_CHECKFORK_RETRY); + if (r<0) { syslog(LOG_CRIT,"set alarm for retry check: %m"); exit(5); } + break; + } else if (errno!=EINTR) { + syslog(LOG_CRIT,"fork for check: %m"); exit(5); + } + } needcheck= 0; } csocklen= sizeof(csockname); diff --git a/process.c b/process.c index 98bbeec..9f6fbc0 100644 --- a/process.c +++ b/process.c @@ -126,7 +126,7 @@ static void xfwriteerror(void) { if (errno != EPIPE) syscallerror("writing to client"); blocksignals(); ensurelogopen(USERVD_LOGFACILITY); - syslog(LOG_DEBUG,"client went away (broken pipe)"); + syslog(LOG_INFO,"client went away (broken pipe)"); disconnect(8); } @@ -151,7 +151,7 @@ static void xfread(void *p, size_t sz) { if (ferror(srfile)) syscallerror("reading from client"); blocksignals(); assert(feof(srfile)); - syslog(LOG_DEBUG,"client went away (unexpected EOF)"); + syslog(LOG_INFO,"client went away (unexpected EOF)"); swfile= 0; disconnect(8); } @@ -183,7 +183,7 @@ static void getevent(struct event_msg *event_r) { blocksignals(); syslog(LOG_ERR,"client sent bad file descriptor %d to close (max %d)", fd,fdarrayused-1); - disconnect(12); + disconnect(20); } if (fdarray[fd].holdfd!=-1) { if (close(fdarray[fd].holdfd)) syscallerror("cannot close holding fd"); @@ -192,7 +192,7 @@ static void getevent(struct event_msg *event_r) { break; case et_disconnect: blocksignals(); - syslog(LOG_DEBUG,"client disconnected"); + syslog(LOG_INFO,"client disconnected"); disconnect(4); default: return; @@ -218,7 +218,7 @@ void syscallerror(const char *what) { e= errno; blocksignals(); syslog(LOG_ERR,"system call failure: %s: %s",what,strerror(e)); - disconnect(18); + disconnect(16); } /* Functions which may be called from signal handlers. These @@ -287,7 +287,7 @@ static void NONRETURNING sighandler_chld(int ignored) { xfwrite(&progress_mbuf,sizeof(progress_mbuf),swfile); xfflush(swfile); - syslog(LOG_DEBUG,"service completed (status %d %d)",(status>>8)&0x0ff,status&0x0ff); + syslog(LOG_INFO,"service completed (status %d %d)",(status>>8)&0x0ff,status&0x0ff); _exit(0); } @@ -327,7 +327,7 @@ static void NONRETURNING generalfailure(const char *prefix, int reserveerrno, strnytcat(errmsg,strerror(errnoval),sizeof(errmsg)); } senderrmsgstderr(errmsg); - syslog(LOG_DEBUG,"service failed (%s)",errmsg); + syslog(LOG_INFO,"service failed (%s)",errmsg); disconnect(12); } @@ -412,6 +412,7 @@ static void receive_request(void) { xfread(&request_mbuf,sizeof(request_mbuf)); serviceuser= xfreadsetstring(request_mbuf.serviceuserlen); service= xfreadsetstring(request_mbuf.servicelen); + assert(request_mbuf.spoofed==0 || request_mbuf.spoofed==1); logname= xfreadsetstring(request_mbuf.lognamelen); cwd= xfreadsetstring(request_mbuf.cwdlen); if (request_mbuf.overridelen >= 0) { @@ -509,9 +510,10 @@ static void lookup_uidsgids(void) { if (initgroups(pw->pw_name,pw->pw_gid)) syscallerror("initgroups"); if (setreuid(pw->pw_uid,pw->pw_uid)) syscallerror("setreuid 1"); if (setreuid(pw->pw_uid,pw->pw_uid)) syscallerror("setreuid 2"); - if (pw->pw_uid) + if (pw->pw_uid) { if (!setreuid(pw->pw_uid,0)) miscerror("setreuid 3 unexpectedly succeeded"); - if (errno != EPERM) syscallerror("setreuid 3 failed in unexpected way"); + if (errno != EPERM) syscallerror("setreuid 3 failed in unexpected way"); + } service_ngids= getgroups(0,0); if (service_ngids == -1) syscallerror("getgroups(0,0)"); if (service_ngids > MAX_GIDS) miscerror("service user is in far too many groups"); @@ -711,9 +713,13 @@ void servicerequest(int sfd) { setup_comms(sfd); send_opening(); receive_request(); + if (request_mbuf.clientpid == (pid_t)-1) _exit(2); establish_pipes(); lookup_uidsgids(); debug_dumprequest(mypid); + syslog(LOG_INFO,"%s %s -> %s %c %s", + request_mbuf.spoofed ? "spoof" : "user", + logname, serviceuser, overridedata?'!':':', service); if (overridedata) r= parse_string(TOPLEVEL_OVERRIDDEN_CONFIGURATION, diff --git a/servexec.c b/servexec.c index 2b11012..41c6d37 100644 --- a/servexec.c +++ b/servexec.c @@ -61,7 +61,7 @@ void bisexec_version(const char *const *argv) { const unsigned char *p; int i; - printf("uservd version " VERSION "; copyright (C)1996-1997 Ian Jackson.\n" + printf("uservd version " VERSION VEREXT "; copyright (C)1996-1997 Ian Jackson.\n" #ifdef DEBUG "DEBUGGING VERSION" #else diff --git a/spec.sgml.in b/spec.sgml.in index 8757851..6d018eb 100644 --- a/spec.sgml.in +++ b/spec.sgml.in @@ -707,7 +707,7 @@ in the context of and with the privileges of the service user. Error messages will be delivered using Control structure directives -- 2.30.2